Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | 21x 20x 20x 20x 20x 20x 20x 21x 260x 260x 260x 21x 21x 21x 21x 12x 12x | "use client";
/**
* TrendChart — a tiny zero-dependency SVG line+area sparkline for personal-health
* trends (normalised to the series min/max so small variations are visible).
* Optional `markers` overlay vertical event annotations (rendered as HTML so
* they aren't distorted by the stretched SVG). Pure presentational; synthetic.
*/
export interface TrendMarker {
/** position along the x-axis, 0 (oldest) … 1 (newest) */
x: number;
color: string;
label: string;
}
export function TrendChart({
points,
color,
height = 64,
markers = [],
}: {
points: number[];
color: string;
height?: number;
markers?: TrendMarker[];
}) {
if (!points || points.length < 2) return null;
const W = 260;
const H = 64;
const pad = 6;
const min = Math.min(...points);
const max = Math.max(...points);
const range = max - min || 1;
const xy = points.map((v, i) => {
const x = pad + (i / (points.length - 1)) * (W - 2 * pad);
const y = pad + (1 - (v - min) / range) * (H - 2 * pad);
return `${x.toFixed(1)},${y.toFixed(1)}`;
});
const line = xy.join(" ");
const area = `${pad},${H - pad} ${line} ${W - pad},${H - pad}`;
const gid = `tg-${color.replace("#", "")}`;
return (
<div className="relative w-full" style={{ height }}>
<svg
viewBox={`0 0 ${W} ${H}`}
preserveAspectRatio="none"
role="img"
aria-hidden="true"
className="block w-full h-full"
>
<defs>
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.28" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
<polygon points={area} fill={`url(#${gid})`} />
<polyline
points={line}
fill="none"
stroke={color}
strokeWidth="2"
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
{markers.map((m, i) => {
const left = `${Math.max(0, Math.min(1, m.x)) * 100}%`;
return (
<div
key={i}
className="absolute top-0 bottom-0 pointer-events-none"
style={{ left }}
title={m.label}
>
<div
className="absolute top-1.5 bottom-1.5 -translate-x-1/2 border-l border-dashed"
style={{ borderColor: m.color, opacity: 0.75 }}
/>
<div
className="absolute top-0 -translate-x-1/2 w-2 h-2 rounded-full ring-1 ring-white"
style={{ background: m.color }}
/>
</div>
);
})}
</div>
);
}
|