From c9df948eba788c0b5a4683f335819a2782284f08 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 17:30:10 +0100 Subject: [PATCH] fix(webapp): use stable chart bucket timestamps --- .../app/components/metrics/MiniLineChart.tsx | 15 +++++++++------ .../app/components/primitives/UsageSparkline.tsx | 10 ++++------ apps/webapp/app/routes/storybook.charts/route.tsx | 3 +++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/webapp/app/components/metrics/MiniLineChart.tsx b/apps/webapp/app/components/metrics/MiniLineChart.tsx index f758ad3370d..bdd9c587a5e 100644 --- a/apps/webapp/app/components/metrics/MiniLineChart.tsx +++ b/apps/webapp/app/components/metrics/MiniLineChart.tsx @@ -44,7 +44,7 @@ export type MiniLineChartProps = { throttled?: number[]; /** Tooltip wording for the overlay buckets. Null omits the overlay line. */ overlayLabel?: string | null; - /** Epoch ms of the first bucket's start. When omitted, the last bucket is anchored to now. */ + /** Epoch ms of the first bucket's start. */ bucketStartMs?: number; /** Width of each bucket in ms. Defaults to one hour. */ bucketIntervalMs?: number; @@ -92,7 +92,12 @@ export function MiniLineChart({ showPeak = true, }: MiniLineChartProps) { const hasPeakOverride = peakOverride !== undefined; - if (!data || data.length === 0 || (data.every((v) => v === 0) && !hasPeakOverride)) { + if ( + !data || + data.length === 0 || + bucketStartMs === undefined || + (data.every((v) => v === 0) && !hasPeakOverride) + ) { return ; } @@ -103,11 +108,9 @@ export function MiniLineChart({ const max = Math.max(...data); const peak = peakOverride ?? max; - // Map each bucket to a dated point so the tooltip can show the window it represents. Buckets are - // `intervalMs` wide; if the caller didn't pass the first bucket's start, anchor the last bucket to - // now (hourly default). + // Map each bucket to a dated point so the tooltip can show the window it represents. const intervalMs = bucketIntervalMs ?? 3600_000; - const startMs = bucketStartMs ?? Date.now() - (data.length - 1) * intervalMs; + const startMs = bucketStartMs; const chartData: MiniLineChartDatum[] = data.map((count, i) => { const t = throttled?.[i] ?? 0; // Extend the mask one bucket forward (a segment needs both endpoints non-null), so even a diff --git a/apps/webapp/app/components/primitives/UsageSparkline.tsx b/apps/webapp/app/components/primitives/UsageSparkline.tsx index ac80fff01ec..1124fb05c6b 100644 --- a/apps/webapp/app/components/primitives/UsageSparkline.tsx +++ b/apps/webapp/app/components/primitives/UsageSparkline.tsx @@ -19,8 +19,8 @@ type UnitLabel = { singular: string; plural: string }; export type UsageSparklineProps = { /** Equal-width time buckets, oldest first. */ data?: number[]; - /** Epoch ms of the first bucket's start. When omitted, the last bucket is anchored to now. */ - bucketStartMs?: number; + /** Epoch ms of the first bucket's start. */ + bucketStartMs: number; /** Width of each bucket in ms. Defaults to one hour. */ bucketIntervalMs?: number; /** Bar colour. Defaults to blue. */ @@ -64,11 +64,9 @@ export function UsageSparkline({ const total = totalOverride ?? data.reduce((a, b) => a + b, 0); const max = Math.max(...data); - // Map each bucket to a dated point so the tooltip can show the window it - // represents. Buckets are `intervalMs` wide; if the caller didn't pass the - // first bucket's start, anchor the last bucket to now (hourly default). + // Map each bucket to a dated point so the tooltip can show the window it represents. const intervalMs = bucketIntervalMs ?? 3600_000; - const startMs = bucketStartMs ?? Date.now() - (data.length - 1) * intervalMs; + const startMs = bucketStartMs; const chartData: UsageDatum[] = data.map((count, i) => ({ date: new Date(startMs + i * intervalMs), count, diff --git a/apps/webapp/app/routes/storybook.charts/route.tsx b/apps/webapp/app/routes/storybook.charts/route.tsx index 040211437ad..dfcabf4c577 100644 --- a/apps/webapp/app/routes/storybook.charts/route.tsx +++ b/apps/webapp/app/routes/storybook.charts/route.tsx @@ -21,6 +21,7 @@ import SegmentedControl from "~/components/primitives/SegmentedControl"; // Date formatters for chart display const xAxisTickFormatter = (value: string) => formatISODate(value); const tooltipLabelFormatter = (label: string) => formatISODateLong(label); +const MINI_LINE_BUCKET_START_MS = Date.UTC(2025, 0, 1); /** * Helper function to filter chart data by date range. @@ -361,6 +362,7 @@ function ChartsDashboard() {