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 `
-
${max}ms
-
${min}ms
+
${yMax}ms
+
${yMin}ms
${regions.length > 1 ? `
${legend}
` : ''}
`;
}