From 3069b4692d546a3f7913f5ec58a6a6a7f6790159 Mon Sep 17 00:00:00 2001
From: sneak
Date: Sat, 31 Jan 2026 14:22:35 -0800
Subject: [PATCH] Add fixed axes, color-coded lines, summary stats, and >1000ms
clamping
- Fixed Y-axis (0-1000ms) with tick labels
- Fixed X-axis showing seconds ago (-0s to -300s)
- Sparkline segments color-coded by latency value
- Summary line showing reachable count, min/max/avg across hosts
- Latencies >1000ms now clamped to unreachable/timeout
- Canvas height increased to 80px for axis labels
---
src/main.js | 253 ++++++++++++++++++++++++++++++++++------------------
1 file changed, 164 insertions(+), 89 deletions(-)
diff --git a/src/main.js b/src/main.js
index 24209fb..2940d34 100644
--- a/src/main.js
+++ b/src/main.js
@@ -53,6 +53,10 @@ async function measureLatency(url) {
const latency = Math.round(performance.now() - start)
clearTimeout(timeoutId)
+ // Clamp >1000ms to unreachable
+ if (latency > 1000) {
+ return { latency: null, error: 'timeout' }
+ }
return { latency, error: null }
} catch (err) {
clearTimeout(timeoutId)
@@ -65,16 +69,14 @@ async function measureLatency(url) {
}
}
-// Get latency color based on value
-function getLatencyColor(latency, status) {
- if (status === 'offline' || status === 'error' || latency === null) {
- return 'var(--color-latency-offline)'
- }
- if (latency < 50) return 'var(--color-latency-excellent)'
- if (latency < 100) return 'var(--color-latency-good)'
- if (latency < 200) return 'var(--color-latency-moderate)'
- if (latency < 500) return 'var(--color-latency-poor)'
- return 'var(--color-latency-bad)'
+// Get latency color (raw hex for canvas)
+function getLatencyColor(latency) {
+ if (latency === null) return '#6b7280' // gray
+ if (latency < 50) return '#22c55e' // green
+ if (latency < 100) return '#84cc16' // lime
+ if (latency < 200) return '#eab308' // yellow
+ if (latency < 500) return '#f97316' // orange
+ return '#ef4444' // red
}
// Get Tailwind class for latency
@@ -89,95 +91,73 @@ function getLatencyClass(latency, status) {
return 'text-red-500'
}
+// Fixed axis constants
+const Y_AXIS_MAX = 1000 // ms
+const Y_AXIS_TICKS = [0, 250, 500, 750, 1000]
+const X_AXIS_TICKS = [0, 60, 120, 180, 240, 300] // seconds ago
+
// Draw sparkline on canvas - latest point at right edge, growing left
-function drawSparkline(canvas, history, hostColor) {
+function drawSparkline(canvas, history) {
const ctx = canvas.getContext('2d')
- const width = canvas.width
- const height = canvas.height
- const padding = 4
+ const dpr = window.devicePixelRatio || 1
+ const width = canvas.width / dpr
+ const height = canvas.height / dpr
+
+ // Margins for axis labels
+ const marginLeft = 35
+ const marginRight = 10
+ const marginTop = 5
+ const marginBottom = 18
+
+ const chartWidth = width - marginLeft - marginRight
+ const chartHeight = height - marginTop - marginBottom
// Clear canvas
ctx.clearRect(0, 0, width, height)
- // Filter valid latency points
- const validPoints = history.filter(p => p.latency !== null)
+ // Draw Y-axis labels and grid lines
+ ctx.font = '9px monospace'
+ ctx.textAlign = 'right'
+ ctx.textBaseline = 'middle'
- if (validPoints.length < 2) {
- // Draw placeholder line
+ Y_AXIS_TICKS.forEach(tick => {
+ const y = marginTop + chartHeight - (tick / Y_AXIS_MAX) * chartHeight
+
+ // Grid line
ctx.strokeStyle = 'rgba(255,255,255,0.1)'
ctx.lineWidth = 1
ctx.beginPath()
- ctx.moveTo(padding, height / 2)
- ctx.lineTo(width - padding, height / 2)
+ ctx.moveTo(marginLeft, y)
+ ctx.lineTo(width - marginRight, y)
ctx.stroke()
- return
- }
- // Calculate min/max for scaling
- const latencies = validPoints.map(p => p.latency)
- const minLatency = Math.min(...latencies)
- const maxLatency = Math.max(...latencies)
- const range = maxLatency - minLatency || 1
-
- // Draw background grid lines
- ctx.strokeStyle = 'rgba(255,255,255,0.05)'
- ctx.lineWidth = 1
- for (let i = 0; i <= 4; i++) {
- const y = padding + (i / 4) * (height - 2 * padding)
- ctx.beginPath()
- ctx.moveTo(0, y)
- ctx.lineTo(width, y)
- ctx.stroke()
- }
-
- // Draw the sparkline - right-aligned (latest at right edge)
- ctx.strokeStyle = hostColor
- ctx.lineWidth = 1.5
- ctx.lineCap = 'round'
- ctx.lineJoin = 'round'
- ctx.beginPath()
-
- const pointWidth = (width - 2 * padding) / (MAX_HISTORY_POINTS - 1)
- const historyLen = history.length
-
- // Calculate x position: latest point at right edge
- const getX = (i) => width - padding - (historyLen - 1 - i) * pointWidth
-
- // Map all history points to x positions
- let firstPoint = true
- history.forEach((point, i) => {
- if (point.latency === null) return
-
- const x = getX(i)
- const normalizedY = (point.latency - minLatency) / range
- const y = height - padding - normalizedY * (height - 2 * padding)
-
- if (firstPoint) {
- ctx.moveTo(x, y)
- firstPoint = false
- } else {
- ctx.lineTo(x, y)
- }
+ // Label
+ ctx.fillStyle = 'rgba(255,255,255,0.5)'
+ ctx.fillText(`${tick}`, marginLeft - 4, y)
})
- ctx.stroke()
+ // Draw X-axis labels
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'top'
- // Draw latest point indicator (always at right edge)
- const lastValidPoint = [...history].reverse().find(p => p.latency !== null)
- if (lastValidPoint) {
- const lastIndex = history.lastIndexOf(lastValidPoint)
- const x = getX(lastIndex)
- const normalizedY = (lastValidPoint.latency - minLatency) / range
- const y = height - padding - normalizedY * (height - 2 * padding)
+ X_AXIS_TICKS.forEach(tick => {
+ // tick is seconds ago, 0 = now (right edge), 300 = 5min ago (left edge)
+ const x = marginLeft + chartWidth - (tick / HISTORY_DURATION) * chartWidth
- ctx.fillStyle = hostColor
- ctx.beginPath()
- ctx.arc(x, y, 3, 0, Math.PI * 2)
- ctx.fill()
- }
+ // Label
+ ctx.fillStyle = 'rgba(255,255,255,0.5)'
+ ctx.fillText(`-${tick}s`, x, height - marginBottom + 4)
+ })
- // Draw error regions (red zones where connectivity was lost)
- ctx.fillStyle = 'rgba(239, 68, 68, 0.3)'
+ // Helper functions for chart coordinates
+ const historyLen = history.length
+ const pointWidth = chartWidth / (MAX_HISTORY_POINTS - 1)
+
+ const getX = (i) => marginLeft + chartWidth - (historyLen - 1 - i) * pointWidth
+ const getY = (latency) => marginTop + chartHeight - (latency / Y_AXIS_MAX) * chartHeight
+
+ // Draw error regions first (behind the line)
+ ctx.fillStyle = 'rgba(239, 68, 68, 0.2)'
let inErrorRegion = false
let errorStart = 0
@@ -189,13 +169,57 @@ function drawSparkline(canvas, history, hostColor) {
errorStart = x
} else if (point.latency !== null && inErrorRegion) {
inErrorRegion = false
- ctx.fillRect(errorStart, 0, x - errorStart, height)
+ ctx.fillRect(errorStart, marginTop, x - errorStart, chartHeight)
}
})
if (inErrorRegion) {
const lastX = getX(historyLen - 1)
- ctx.fillRect(errorStart, 0, lastX - errorStart + padding, height)
+ ctx.fillRect(errorStart, marginTop, lastX - errorStart + 5, chartHeight)
+ }
+
+ // Draw the sparkline segments - color coded by latency
+ ctx.lineWidth = 2
+ ctx.lineCap = 'round'
+ ctx.lineJoin = 'round'
+
+ let prevPoint = null
+ let prevX = 0
+ let prevY = 0
+
+ history.forEach((point, i) => {
+ if (point.latency === null) {
+ prevPoint = null
+ return
+ }
+
+ const x = getX(i)
+ const y = getY(point.latency)
+
+ if (prevPoint !== null) {
+ ctx.strokeStyle = getLatencyColor(point.latency)
+ ctx.beginPath()
+ ctx.moveTo(prevX, prevY)
+ ctx.lineTo(x, y)
+ ctx.stroke()
+ }
+
+ prevPoint = point
+ prevX = x
+ prevY = y
+ })
+
+ // Draw latest point indicator
+ const lastValidPoint = [...history].reverse().find(p => p.latency !== null)
+ if (lastValidPoint) {
+ const lastIndex = history.lastIndexOf(lastValidPoint)
+ const x = getX(lastIndex)
+ const y = getY(lastValidPoint.latency)
+
+ ctx.fillStyle = getLatencyColor(lastValidPoint.latency)
+ ctx.beginPath()
+ ctx.arc(x, y, 3, 0, Math.PI * 2)
+ ctx.fill()
}
}
@@ -228,6 +252,15 @@ function createUI() {
History: ${HISTORY_DURATION}s |
Running
+
+ Reachable: --/--
+ |
+ Min: --ms
+ |
+ Max: --ms
+ |
+ Avg: --ms
+
@@ -258,7 +291,7 @@ function createUI() {
@@ -285,7 +318,7 @@ function createUI() {
document.querySelectorAll('.sparkline-canvas').forEach(canvas => {
const rect = canvas.getBoundingClientRect()
canvas.width = rect.width * window.devicePixelRatio
- canvas.height = 60 * window.devicePixelRatio
+ canvas.height = 80 * window.devicePixelRatio
canvas.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio)
})
})
@@ -326,7 +359,7 @@ function updateHostDisplay(index) {
// Update sparkline
const canvasRect = canvas.getBoundingClientRect()
- drawSparkline(canvas, host.history, host.color)
+ drawSparkline(canvas, host.history)
}
// Calculate average latency from history
@@ -338,6 +371,45 @@ function calculateAverageLatency(history) {
return Math.round(sum / validPoints.length)
}
+// Update summary line with current host stats
+function updateSummary() {
+ const reachableHosts = hostState.filter(h => h.lastLatency !== null)
+ const reachableCount = reachableHosts.length
+ const totalCount = hostState.length
+
+ const reachableEl = document.getElementById('summary-reachable')
+ const minEl = document.getElementById('summary-min')
+ const maxEl = document.getElementById('summary-max')
+ const avgEl = document.getElementById('summary-avg')
+
+ if (!reachableEl) return
+
+ reachableEl.textContent = `${reachableCount}/${totalCount}`
+ reachableEl.className = reachableCount === totalCount ? 'text-green-400' :
+ reachableCount === 0 ? 'text-red-400' : 'text-yellow-400'
+
+ if (reachableCount > 0) {
+ const latencies = reachableHosts.map(h => h.lastLatency)
+ const min = Math.min(...latencies)
+ const max = Math.max(...latencies)
+ const avg = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length)
+
+ minEl.textContent = `${min}ms`
+ minEl.className = getLatencyClass(min, 'online')
+ maxEl.textContent = `${max}ms`
+ maxEl.className = getLatencyClass(max, 'online')
+ avgEl.textContent = `${avg}ms`
+ avgEl.className = getLatencyClass(avg, 'online')
+ } else {
+ minEl.textContent = '--ms'
+ minEl.className = 'text-gray-500'
+ maxEl.textContent = '--ms'
+ maxEl.className = 'text-gray-500'
+ avgEl.textContent = '--ms'
+ avgEl.className = 'text-gray-500'
+ }
+}
+
// Main measurement loop
async function measureAll() {
const timestamp = Date.now()
@@ -376,6 +448,9 @@ async function measureAll() {
// Update display
updateHostDisplay(index)
})
+
+ // Update summary
+ updateSummary()
}
// Handle window resize
@@ -383,14 +458,14 @@ function handleResize() {
document.querySelectorAll('.sparkline-canvas').forEach((canvas, index) => {
const rect = canvas.getBoundingClientRect()
canvas.width = rect.width * window.devicePixelRatio
- canvas.height = 60 * window.devicePixelRatio
+ canvas.height = 80 * window.devicePixelRatio
const ctx = canvas.getContext('2d')
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
// Redraw
if (hostState[index]) {
- drawSparkline(canvas, hostState[index].history, hostState[index].color)
+ drawSparkline(canvas, hostState[index].history)
}
})
}