Import source: web/src/components/Sparkline

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:20:08 +03:30
parent 484665e51c
commit 854634d1b5
+164
View File
@@ -0,0 +1,164 @@
import { useMemo, useState } from "react";
import { motion } from "motion/react";
import type { CheckPoint } from "../lib/types";
import { fmtLatency, fmtTime } from "../lib/format";
import { useUI } from "../lib/ui";
const W = 460;
const H = 90;
const PAD = 4;
interface SparklineProps {
series: CheckPoint[];
/** Re-keys the head pulse so each landing probe blips. */
pulse: number;
}
/** Latency over time — brass line, failures as red drops to the floor. */
export function Sparkline({ series, pulse }: SparklineProps) {
const { t, lang, reduced } = useUI();
const [probe, setProbe] = useState<CheckPoint | null>(null);
const ok = series.filter((p) => p.ok);
const { path, area, posFor, maxLat } = useMemo(() => {
if (series.length < 2 || ok.length === 0) {
return { path: "", area: "", posFor: () => null, maxLat: 0 };
}
const t0 = series[0].ts;
const t1 = series[series.length - 1].ts;
const span = Math.max(1, t1 - t0);
const maxLat = Math.max(...ok.map((p) => p.latency_ms)) * 1.15;
const x = (ts: number) => PAD + ((ts - t0) / span) * (W - PAD * 2);
const y = (lat: number) => H - PAD - (lat / maxLat) * (H - PAD * 2);
let path = "";
for (const p of ok) {
path += `${path ? "L" : "M"} ${x(p.ts).toFixed(2)} ${y(p.latency_ms).toFixed(2)} `;
}
const area = `${path} L ${x(ok[ok.length - 1].ts).toFixed(2)} ${H - PAD} L ${x(ok[0].ts).toFixed(2)} ${H - PAD} Z`;
const posFor = (p: CheckPoint) => ({
x: x(p.ts),
y: p.ok ? y(p.latency_ms) : H - PAD,
});
return { path, area, posFor, maxLat };
}, [series, ok]);
if (!path) {
return (
<div className="h-[90px] flex items-center justify-center microlabel">
{t.noData}
</div>
);
}
const head = posFor(series[series.length - 1]);
const onMove = (e: React.MouseEvent<SVGSVGElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const mx = ((e.clientX - rect.left) / rect.width) * W;
let best: CheckPoint | null = null;
let bestD = Infinity;
for (const p of series) {
const pos = posFor(p);
if (!pos) continue;
const d = Math.abs(pos.x - mx);
if (d < bestD) {
bestD = d;
best = p;
}
}
setProbe(best);
};
const probePos = probe ? posFor(probe) : null;
return (
<div className="relative" dir="ltr">
<svg
viewBox={`0 0 ${W} ${H}`}
className="w-full block"
onMouseMove={onMove}
onMouseLeave={() => setProbe(null)}
>
<defs>
<linearGradient id="spark-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-brass)" stopOpacity="0.16" />
<stop offset="100%" stopColor="var(--color-brass)" stopOpacity="0" />
</linearGradient>
</defs>
{/* latency grid: max line */}
<line x1={PAD} y1={PAD + 6} x2={W - PAD} y2={PAD + 6} stroke="var(--color-ink-800)" strokeWidth={0.5} strokeDasharray="3 5" />
<text x={W - PAD} y={PAD + 3} textAnchor="end" fontSize={7.5} fontFamily="var(--font-mono)" fill="var(--color-paper-faint)" className="tabular">
{fmtLatency(lang, maxLat / 1.15)}
</text>
<path d={area} fill="url(#spark-fill)" />
<motion.path
d={path}
fill="none"
stroke="var(--color-brass)"
strokeWidth={1.4}
strokeLinejoin="round"
initial={reduced ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={reduced ? { duration: 0 } : { duration: 0.9, ease: [0.65, 0, 0.35, 1] }}
/>
{/* failures drop to the floor as red ticks */}
{series.filter((p) => !p.ok).map((p) => {
const pos = posFor(p)!;
return (
<line
key={p.ts}
x1={pos.x}
y1={H - PAD - 7}
x2={pos.x}
y2={H - PAD}
stroke="var(--color-down)"
strokeWidth={1.5}
/>
);
})}
{/* live head — blips when a probe lands */}
{!reduced && (
<motion.circle
key={pulse}
cx={head?.x}
cy={head?.y}
fill="var(--color-brass)"
initial={{ r: 1.5, opacity: 1 }}
animate={{ r: 6, opacity: 0 }}
transition={{ duration: 0.9, ease: "easeOut" }}
/>
)}
<circle cx={head?.x} cy={head?.y} r={2} fill="var(--color-brass)" />
{/* hover probe */}
{probe && probePos && (
<g>
<line x1={probePos.x} y1={PAD} x2={probePos.x} y2={H - PAD} stroke="var(--color-paper-faint)" strokeWidth={0.5} opacity={0.5} />
<circle cx={probePos.x} cy={probePos.y} r={3} fill="none" stroke={probe.ok ? "var(--color-brass)" : "var(--color-down)"} strokeWidth={1} />
</g>
)}
</svg>
<div className="mt-1 h-4 flex justify-between microlabel tabular">
{probe ? (
<>
<span style={{ color: probe.ok ? "var(--color-brass)" : "var(--color-down)" }}>
{probe.ok ? fmtLatency(lang, probe.latency_ms) : probe.error || t.failed}
</span>
<span>{fmtTime(lang, probe.ts)}</span>
</>
) : (
<>
<span>{fmtTime(lang, series[0].ts)}</span>
<span>{fmtTime(lang, series[series.length - 1].ts)}</span>
</>
)}
</div>
</div>
);
}