diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index a94b52a..2cf8a9e 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -41,6 +41,26 @@ function latencyChartSSR(pings: any[]): string { return '
Not enough data
'; } + const w = 800, h = 128; + const pad = { top: 8, bottom: 8 }; + const cH = h - pad.top - pad.bottom; + + // Build ordered list of unique runs, evenly spaced (matches canvas) + const runTimes: Record = {}; + for (const p of data) { + const rid = p.run_id || p.checked_at; + if (!runTimes[rid]) runTimes[rid] = []; + runTimes[rid].push(new Date(p.checked_at).getTime()); + } + const runs = Object.keys(runTimes).sort((a, b) => { + const avgA = runTimes[a].reduce((x: number, y: number) => x + y, 0) / runTimes[a].length; + const avgB = runTimes[b].reduce((x: number, y: number) => x + y, 0) / runTimes[b].length; + return avgA - avgB; + }); + const runIndex: Record = {}; + runs.forEach((rid, i) => { runIndex[rid] = i; }); + const maxIdx = Math.max(runs.length - 1, 1); + // Group by region const byRegion: Record = {}; for (const p of data) { @@ -51,17 +71,24 @@ function latencyChartSSR(pings: any[]): string { const regions = Object.keys(byRegion); const allValues = data.map((c: any) => c.latency_ms); - const max = Math.max(...allValues, 1); - const min = Math.min(...allValues, 0); - const range = max - min || 1; - const w = 800; - const h = 128; + const yMax = Math.max(...allValues, 1); + const yMin = Math.min(...allValues, 0); + const yRange = yMax - yMin || 1; - // Find the overall time range to align lines on a shared x-axis - const allTimes = data.map((c: any) => new Date(c.checked_at).getTime()); - const tMin = Math.min(...allTimes); - const tMax = Math.max(...allTimes); - const tRange = tMax - tMin || 1; + function toX(p: any): number { + const idx = runIndex[p.run_id || p.checked_at] || 0; + return (idx / maxIdx) * w; + } + function toY(v: number): number { + return pad.top + cH - ((v - yMin) / yRange) * cH; + } + + // Grid lines + let grid = ''; + for (let i = 0; i <= 4; i++) { + const y = (pad.top + (cH / 4) * i).toFixed(1); + grid += ``; + } let paths = ''; let dots = ''; @@ -70,21 +97,27 @@ function latencyChartSSR(pings: any[]): string { for (const region of regions) { const color = REGION_COLORS[region] || '#6b7280'; const label = REGION_LABELS[region] || region; - const rPings = byRegion[region].slice().sort((a: any, b: any) => - new Date(a.checked_at).getTime() - new Date(b.checked_at).getTime() - ); + const rPings = byRegion[region].slice().sort((a: any, b: any) => toX(a) - toX(b)); - const points = rPings.map((p: any) => { - const x = ((new Date(p.checked_at).getTime() - tMin) / tRange) * w; - const y = h - ((p.latency_ms - min) / range) * (h - 16) - 8; - return { x, y, up: p.up }; - }); + const pts = rPings.map((p: any) => ({ x: toX(p), y: toY(p.latency_ms), up: p.up })); + if (pts.length < 2) continue; - if (points.length < 1) continue; - - const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' '); - paths += ``; - dots += points.filter(p => !p.up).map(p => + // Catmull-Rom spline (tension 0.15, matches canvas) + const t = 0.15; + let d = `M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`; + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[Math.max(i - 1, 0)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(i + 2, pts.length - 1)]; + const cp1x = p1.x + (p2.x - p0.x) * t; + const cp1y = p1.y + (p2.y - p0.y) * t; + const cp2x = p2.x - (p3.x - p1.x) * t; + const cp2y = p2.y - (p3.y - p1.y) * t; + d += ` C${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`; + } + paths += ``; + dots += pts.filter(p => !p.up).map(p => `` ).join(''); legend += `${label}`; @@ -92,10 +125,10 @@ function latencyChartSSR(pings: any[]): string { return `
- ${paths}${dots} + ${grid}${paths}${dots} - ${max}ms - ${min}ms + ${yMax}ms + ${yMin}ms ${regions.length > 1 ? `
${legend}
` : ''}
`; }