import { useState } from "react";
/** Grouped/stacked daily bars: ok vs bad. Thin marks, rounded ends, 2px gaps, hover tooltip. */
export function DailyBars({ data, width = 640, height = 170 }: { data: { day: string; ok: number; bad: number; n: number }[]; width?: number; height?: number }) {
const [tip, setTip] = useState<{ x: number; y: number; d: (typeof data)[number] } | null>(null);
if (!data.length) return
No executions in the last 14 days.
;
const m = { l: 34, r: 8, t: 10, b: 24 };
const w = width - m.l - m.r, h = height - m.t - m.b;
const max = Math.max(1, ...data.map((d) => d.ok + d.bad));
const bw = w / data.length;
const ticks = [0, 0.5, 1].map((f) => Math.round(max * f));
return (
succeededfailed / blocked
{tip &&
{tip.d.day}
succeeded {tip.d.ok} · failed/blocked {tip.d.bad} · total {tip.d.n}
}
);
}
/** Horizontal breakdown bars for categorical counts (one hue, labels in text tokens). */
export function Breakdown({ items, color = "var(--s1)" }: { items: { label: string; n: number; tone?: string }[]; color?: string }) {
if (!items.length) return Nothing yet.
;
const max = Math.max(1, ...items.map((i) => i.n));
return (
{items.map((it) => (
{it.label}
{it.n}
))}
);
}
export function Sparkline({ points, width = 320, height = 48, label }: { points: { x: string; y: number }[]; width?: number; height?: number; label: string }) {
const [tip, setTip] = useState<{ x: number; y: number; p: (typeof points)[number] } | null>(null);
if (points.length < 2) return not enough data for {label}
;
const max = Math.max(1, ...points.map((p) => p.y));
const step = width / (points.length - 1);
const path = points.map((p, i) => `${i === 0 ? "M" : "L"}${i * step},${height - 4 - (p.y / max) * (height - 8)}`).join(" ");
return (
{tip &&
{tip.p.x}: {tip.p.y}
}
);
}