58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* LineMovementChart (Session 28).
|
|
*
|
|
* Dependency-free SVG sparkline of a prop's line through the day. Green
|
|
* when the line rose, red when it dropped. Renders open + current labels.
|
|
* Degrades to a flat midline for <2 points.
|
|
*/
|
|
|
|
export interface Snapshot {
|
|
time?: number;
|
|
line: number;
|
|
}
|
|
|
|
export interface LineMovementChartProps {
|
|
snapshots: Snapshot[];
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
export default function LineMovementChart({ snapshots, width = 120, height = 32 }: LineMovementChartProps) {
|
|
const pts = (snapshots || []).map((s) => Number(s.line)).filter((n) => Number.isFinite(n));
|
|
if (pts.length === 0) return null;
|
|
|
|
const opening = pts[0];
|
|
const current = pts[pts.length - 1];
|
|
const delta = current - opening;
|
|
const stroke = Math.abs(delta) < 0.5 ? 'var(--text-tertiary, #6B6B7B)' : delta > 0 ? '#00D4A0' : '#FF4D4D';
|
|
|
|
const min = Math.min(...pts);
|
|
const max = Math.max(...pts);
|
|
const range = max - min || 1;
|
|
const pad = 4;
|
|
const innerH = height - pad * 2;
|
|
const stepX = pts.length > 1 ? width / (pts.length - 1) : 0;
|
|
const yScale = (v: number) => pad + innerH - ((v - min) / range) * innerH;
|
|
|
|
const polyline =
|
|
pts.length > 1
|
|
? pts.map((v, i) => `${(i * stepX).toFixed(1)},${yScale(v).toFixed(1)}`).join(' ')
|
|
: `0,${(height / 2).toFixed(1)} ${width},${(height / 2).toFixed(1)}`;
|
|
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${width} ${height}`}
|
|
width={width}
|
|
height={height}
|
|
role="img"
|
|
aria-label={`Line moved from ${opening} to ${current}`}
|
|
style={{ display: 'block' }}
|
|
>
|
|
<polyline points={polyline} fill="none" stroke={stroke} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />
|
|
{pts.length > 1 && <circle cx={width} cy={yScale(current)} r={2.5} fill={stroke} />}
|
|
</svg>
|
|
);
|
|
}
|