sadar / frontend /src /components /ScoreTimeline.tsx
Ruperth's picture
feat: polish mobile responsive layout across all dashboard screens
96bba34
Raw
History Blame Contribute Delete
2.95 kB
import { useEffect, useRef, useState } from "react";
import { useT } from "../i18n";
interface Props {
scores: number[];
threshold: number;
onsetIndex?: number;
stepSeconds?: number;
width?: number;
height?: number;
}
const NARROW_TIMELINE_WIDTH = 420;
const NARROW_TIMELINE_PADDING = 22;
const DESKTOP_TIMELINE_PADDING = 28;
export default function ScoreTimeline({
scores,
threshold,
onsetIndex,
stepSeconds = 10,
width,
height,
}: Props) {
const t = useT();
const ref = useRef<HTMLDivElement>(null);
const [box, setBox] = useState({ w: 600, h: 180 });
useEffect(() => {
if (width != null && height != null) return;
if (!ref.current) return;
const node = ref.current;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width: w, height: h } = entry.contentRect;
if (w <= 0 || h <= 0) continue;
setBox({ w: Math.max(280, w), h: Math.max(140, h) });
}
});
observer.observe(node);
return () => observer.disconnect();
}, [width, height]);
const w = width ?? box.w;
const h = height ?? box.h;
const pad = w < NARROW_TIMELINE_WIDTH ? NARROW_TIMELINE_PADDING : DESKTOP_TIMELINE_PADDING;
const count = scores.length;
const maxScore = Math.max(threshold, ...scores) * 1.15 || 1;
const x = (i: number) => pad + (count > 1 ? (i / (count - 1)) * (w - 2 * pad) : 0);
const y = (s: number) => h - pad - (s / maxScore) * (h - 2 * pad);
const line = scores
.map((s, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(s).toFixed(1)}`)
.join(" ");
const thresholdY = y(threshold);
return (
<div ref={ref} style={{ width: "100%", height: "100%" }}>
<svg width={w} height={h} className="panel" style={{ display: "block" }}>
<line x1={pad} y1={h - pad} x2={w - pad} y2={h - pad} stroke="var(--panel-edge)" />
<line
x1={pad}
y1={thresholdY}
x2={w - pad}
y2={thresholdY}
stroke="var(--alert)"
strokeDasharray="4 3"
/>
<text x={w - pad} y={thresholdY - 5} textAnchor="end" fontSize={10} fill="var(--alert)">
{t.timeline.threshold}
</text>
{onsetIndex != null && (
<line
x1={x(onsetIndex)}
y1={pad}
x2={x(onsetIndex)}
y2={h - pad}
stroke="var(--warn)"
strokeDasharray="2 3"
/>
)}
<path d={line} fill="none" stroke="var(--info)" strokeWidth={1.7} />
{scores.map((s, i) =>
s >= threshold ? <circle key={i} cx={x(i)} cy={y(s)} r={2.4} fill="var(--alert)" /> : null,
)}
<text x={pad} y={h - 8} fontSize={10} fill="var(--muted)">
0s
</text>
<text x={w - pad} y={h - 8} textAnchor="end" fontSize={10} fill="var(--muted)">
{(count - 1) * stepSeconds}s
</text>
</svg>
</div>
);
}