/ concept-collection / remote-hdf5-lazy-read
Sign in
concept-collection / remote-hdf5-lazy-read
remote-hdf5-lazy-read / src / components / LinePlot.tsx
156 lines · 3.5 KBBlameHistoryRaw
1type Series = {
2 x: number[] | Float64Array | Float32Array;
3 y: number[] | Float64Array | Float32Array;
4 color: string;
5 label: string;
6};
8type Props = {
9 series: Series[];
10 width?: number;
11 height?: number;
12 xLabel?: string;
13 yLabel?: string;
14};
16// Minimal dependency-free line plot rendered as SVG.
17export const LinePlot = ({
18 series,
19 width = 560,
20 height = 220,
21 xLabel,
22 yLabel,
23}: Props) => {
24 const padL = 48;
25 const padR = 12;
26 const padT = 10;
27 const padB = 34;
28 const innerW = width - padL - padR;
29 const innerH = height - padT - padB;
31 let xMin = Infinity;
32 let xMax = -Infinity;
33 let yMin = Infinity;
34 let yMax = -Infinity;
35 for (const s of series) {
36 for (let i = 0; i < s.x.length; i++) {
37 const xv = s.x[i];
38 const yv = s.y[i];
39 if (xv < xMin) xMin = xv;
40 if (xv > xMax) xMax = xv;
41 if (yv < yMin) yMin = yv;
42 if (yv > yMax) yMax = yv;
43 }
44 }
45 if (!isFinite(xMin)) {
46 xMin = 0;
47 xMax = 1;
48 yMin = 0;
49 yMax = 1;
50 }
51 if (xMax === xMin) xMax = xMin + 1;
52 if (yMax === yMin) yMax = yMin + 1;
54 const sx = (x: number) => padL + ((x - xMin) / (xMax - xMin)) * innerW;
55 const sy = (y: number) => padT + innerH - ((y - yMin) / (yMax - yMin)) * innerH;
57 const pathFor = (s: Series) => {
58 let d = "";
59 for (let i = 0; i < s.x.length; i++) {
60 d += `${i === 0 ? "M" : "L"}${sx(s.x[i]).toFixed(1)},${sy(s.y[i]).toFixed(1)} `;
61 }
62 return d;
63 };
65 const fmt = (v: number) =>
66 Math.abs(v) >= 1000 || (v !== 0 && Math.abs(v) < 0.01)
67 ? v.toExponential(1)
68 : v.toFixed(2);
70 return (
71 <svg
72 width={width}
73 height={height}
74 style={{ background: "#fff", border: "1px solid #e2e2e2" }}
75 >
76 {/* axes */}
77 <line x1={padL} y1={padT} x2={padL} y2={padT + innerH} stroke="#999" />
78 <line
79 x1={padL}
80 y1={padT + innerH}
81 x2={padL + innerW}
82 y2={padT + innerH}
83 stroke="#999"
84 />
85 {/* y ticks */}
86 <text x={padL - 6} y={padT + 4} textAnchor="end" fontSize={10} fill="#555">
87 {fmt(yMax)}
88 </text>
89 <text
90 x={padL - 6}
91 y={padT + innerH}
92 textAnchor="end"
93 fontSize={10}
94 fill="#555"
95 >
96 {fmt(yMin)}
97 </text>
98 {/* x ticks */}
99 <text
100 x={padL}
101 y={padT + innerH + 14}
102 textAnchor="start"
103 fontSize={10}
104 fill="#555"
105 >
106 {fmt(xMin)}
107 </text>
108 <text
109 x={padL + innerW}
110 y={padT + innerH + 14}
111 textAnchor="end"
112 fontSize={10}
113 fill="#555"
114 >
115 {fmt(xMax)}
116 </text>
117 {/* axis labels */}
118 {xLabel && (
119 <text
120 x={padL + innerW / 2}
121 y={height - 4}
122 textAnchor="middle"
123 fontSize={11}
124 fill="#333"
125 >
126 {xLabel}
127 </text>
128 )}
129 {yLabel && (
130 <text
131 x={12}
132 y={padT + innerH / 2}
133 textAnchor="middle"
134 fontSize={11}
135 fill="#333"
136 transform={`rotate(-90 12 ${padT + innerH / 2})`}
137 >
138 {yLabel}
139 </text>
140 )}
141 {/* data */}
142 {series.map((s, i) => (
143 <path key={i} d={pathFor(s)} fill="none" stroke={s.color} strokeWidth={1} />
144 ))}
145 {/* legend */}
146 {series.map((s, i) => (
147 <g key={`l${i}`}>
148 <rect x={padL + 8} y={padT + 6 + i * 14} width={10} height={3} fill={s.color} />
149 <text x={padL + 22} y={padT + 10 + i * 14} fontSize={10} fill="#333">
150 {s.label}
151 </text>
152 </g>
153 ))}
154 </svg>
155 );
156};
moveopenescclose