From e2ad82411005d61ebc904b7514f715676c9df955 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Thu, 16 Jul 2026 13:51:39 +0200 Subject: [PATCH] chart: red band overlay for null-bucket downtime windows Companion to #1195 (series null gaps). The gaps themselves render as line breaks but a break can read as 'the chart missed a sample'. A soft red column behind the break spans the exact outage window and makes the reader understand it was a real feed-side silence. Per line + per contiguous gap range, extended by one step so 1-bucket outages stay visible instead of collapsing to a zero-width sliver. Hidden for excluded (legend-toggled) providers. --- src/components/time-series-chart/chart.tsx | 7 ++- src/components/time-series-chart/series.tsx | 61 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/components/time-series-chart/chart.tsx b/src/components/time-series-chart/chart.tsx index 6998b116..29d88f38 100644 --- a/src/components/time-series-chart/chart.tsx +++ b/src/components/time-series-chart/chart.tsx @@ -6,7 +6,7 @@ import { type LineWithColor, } from "./scales"; import { YAxis, XAxis } from "./axis"; -import { SeriesGradients, SeriesPaths, HoverMarkers, type DrawnLine } from "./series"; +import { SeriesGradients, SeriesPaths, DowntimeBands, HoverMarkers, type DrawnLine } from "./series"; import { Legend } from "./legend"; import { Tooltip } from "./tooltip"; @@ -386,6 +386,11 @@ export function Chart({ {/* X tick labels */} + {/* Downtime bands. Rendered before the lines so the stroke stays + on top of the highlight. Contiguous null-buckets (from spec + `unless changes == 0`) become visible red columns. */} + + {/* Areas + lines */} diff --git a/src/components/time-series-chart/series.tsx b/src/components/time-series-chart/series.tsx index 22e685cd..7769bca2 100644 --- a/src/components/time-series-chart/series.tsx +++ b/src/components/time-series-chart/series.tsx @@ -16,6 +16,67 @@ type SeriesPathsProps = { unit: string; }; +type DowntimeBandsProps = { + drawn: DrawnLine[]; + padT: number; + innerH: number; +}; + +/** Semi-transparent red rectangles that highlight the exact window each + * line was silent (contiguous gap indices). Rendered under SeriesPaths + * so the line stroke stays on top; drawn only for visible (non-excluded) + * lines so toggling a provider off in the legend hides its band with + * it. A single-point gap (1 bucket) still renders as a narrow band by + * extending one step to the right — makes a short outage visible + * instead of being an invisible zero-width sliver. */ +export function DowntimeBands({ drawn, padT, innerH }: DowntimeBandsProps) { + return ( + <> + {drawn.map((d) => { + if (d.excluded) return null; + // Collect contiguous gap ranges as [startX, endX] pairs. endX is + // extended by one step (or the innerH) so a 1-point gap has a + // real visible width. + const bands: { x: number; w: number }[] = []; + let runStart: number | null = null; + for (let i = 0; i < d.pts.length; i++) { + const p = d.pts[i]; + if (p.gap) { + if (runStart == null) runStart = i; + } else if (runStart != null) { + const startX = d.pts[runStart].x; + const endX = d.pts[i].x; + bands.push({ x: startX, w: Math.max(2, endX - startX) }); + runStart = null; + } + } + // Trailing gap that runs to the current time. + if (runStart != null) { + const startX = d.pts[runStart].x; + const endX = d.pts[d.pts.length - 1].x; + bands.push({ x: startX, w: Math.max(2, endX - startX) }); + } + if (bands.length === 0) return null; + return ( + + {bands.map((b, i) => ( + + ))} + + ); + })} + + ); +} + export function SeriesPaths({ drawn, unit }: SeriesPathsProps) { return ( <>