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 (
<>