type Series = { x: number[] | Float64Array | Float32Array; y: number[] | Float64Array | Float32Array; color: string; label: string; }; type Props = { series: Series[]; width?: number; height?: number; xLabel?: string; yLabel?: string; }; // Minimal dependency-free line plot rendered as SVG. export const LinePlot = ({ series, width = 560, height = 220, xLabel, yLabel, }: Props) => { const padL = 48; const padR = 12; const padT = 10; const padB = 34; const innerW = width - padL - padR; const innerH = height - padT - padB; let xMin = Infinity; let xMax = -Infinity; let yMin = Infinity; let yMax = -Infinity; for (const s of series) { for (let i = 0; i < s.x.length; i++) { const xv = s.x[i]; const yv = s.y[i]; if (xv < xMin) xMin = xv; if (xv > xMax) xMax = xv; if (yv < yMin) yMin = yv; if (yv > yMax) yMax = yv; } } if (!isFinite(xMin)) { xMin = 0; xMax = 1; yMin = 0; yMax = 1; } if (xMax === xMin) xMax = xMin + 1; if (yMax === yMin) yMax = yMin + 1; const sx = (x: number) => padL + ((x - xMin) / (xMax - xMin)) * innerW; const sy = (y: number) => padT + innerH - ((y - yMin) / (yMax - yMin)) * innerH; const pathFor = (s: Series) => { let d = ""; for (let i = 0; i < s.x.length; i++) { d += `${i === 0 ? "M" : "L"}${sx(s.x[i]).toFixed(1)},${sy(s.y[i]).toFixed(1)} `; } return d; }; const fmt = (v: number) => Math.abs(v) >= 1000 || (v !== 0 && Math.abs(v) < 0.01) ? v.toExponential(1) : v.toFixed(2); return ( {/* axes */} {/* y ticks */} {fmt(yMax)} {fmt(yMin)} {/* x ticks */} {fmt(xMin)} {fmt(xMax)} {/* axis labels */} {xLabel && ( {xLabel} )} {yLabel && ( {yLabel} )} {/* data */} {series.map((s, i) => ( ))} {/* legend */} {series.map((s, i) => ( {s.label} ))} ); };