concept-collection / fastandaccurate
fastandaccurate / src / app / components / WorkPrecisionChart.tsx
287 lines · 8.7 KBBlameHistoryRaw
1// The work-precision chart: relative max error against median solve time,
2// both log scale. One curve per (solver, environment); color follows the
3// solver, line style distinguishes committed / local / loaded origins.
5import { useMemo, useRef, useState } from "react";
6import type { ResultPoint } from "../../harness/resultSchema";
8export interface ChartCurve {
9 key: string;
10 solverId: string;
11 label: string;
12 color: string;
13 /** stroke-dasharray, undefined for solid (committed results). */
14 dash?: string;
15 /** Open markers (used for runs made in this browser). */
16 open?: boolean;
17 points: ResultPoint[];
20interface Hover {
21 px: number;
22 py: number;
23 curve: ChartCurve;
24 point: ResultPoint;
27const W = 760;
28const H = 470;
29const M = { l: 64, r: 150, t: 14, b: 50 };
31function decades(min: number, max: number): number[] {
32 const lo = Math.floor(Math.log10(min));
33 const hi = Math.ceil(Math.log10(max));
34 const out: number[] = [];
35 for (let e = lo; e <= hi; e++) out.push(e);
36 return out;
39function fmtPow(e: number): string {
40 const sup = String(e)
41 .split("")
42 .map(
43 (c) =>
44 ({ "-": "⁻", "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹" })[c] ?? c
45 )
46 .join("");
47 return `10${sup}`;
50export function WorkPrecisionChart({ curves }: { curves: ChartCurve[] }) {
51 const wrapRef = useRef<HTMLDivElement>(null);
52 const [hover, setHover] = useState<Hover | null>(null);
54 const nonEmpty = curves.filter((c) => c.points.length > 0);
56 const scales = useMemo(() => {
57 let xMin = Infinity,
58 xMax = -Infinity,
59 yMin = Infinity,
60 yMax = -Infinity;
61 for (const c of nonEmpty) {
62 for (const p of c.points) {
63 const x = Math.max(p.solveSeconds, 1e-8);
64 const y = Math.max(p.relMax, 1e-17);
65 xMin = Math.min(xMin, x);
66 xMax = Math.max(xMax, x);
67 yMin = Math.min(yMin, y);
68 yMax = Math.max(yMax, y);
69 }
70 }
71 if (!isFinite(xMin)) {
72 xMin = 1e-4; xMax = 1; yMin = 1e-12; yMax = 1;
73 }
74 const xE = decades(xMin, xMax * 1.0001);
75 const yE = decades(yMin, yMax * 1.0001);
76 const xLo = xE[0];
77 const xHi = xE[xE.length - 1];
78 const yLo = yE[0];
79 const yHi = yE[yE.length - 1];
80 const sx = (v: number) =>
81 M.l + ((Math.log10(Math.max(v, 1e-17)) - xLo) / Math.max(xHi - xLo, 1)) * (W - M.l - M.r);
82 const sy = (v: number) =>
83 H - M.b - ((Math.log10(Math.max(v, 1e-17)) - yLo) / Math.max(yHi - yLo, 1)) * (H - M.t - M.b);
84 return { sx, sy, xE, yE };
85 }, [nonEmpty]);
87 const { sx, sy, xE, yE } = scales;
88 const xStep = xE.length > 8 ? 2 : 1;
89 const yStep = yE.length > 8 ? 2 : 1;
91 function onMove(e: React.MouseEvent<SVGSVGElement>) {
92 const svg = e.currentTarget;
93 const rect = svg.getBoundingClientRect();
94 const px = ((e.clientX - rect.left) / rect.width) * W;
95 const py = ((e.clientY - rect.top) / rect.height) * H;
96 let best: Hover | null = null;
97 let bestD = 26 * 26;
98 for (const c of nonEmpty) {
99 for (const p of c.points) {
100 const dx = sx(p.solveSeconds) - px;
101 const dy = sy(Math.max(p.relMax, 1e-17)) - py;
102 const d = dx * dx + dy * dy;
103 if (d < bestD) {
104 bestD = d;
105 best = { px: sx(p.solveSeconds), py: sy(Math.max(p.relMax, 1e-17)), curve: c, point: p };
106 }
107 }
108 }
109 setHover(best);
110 }
112 if (nonEmpty.length === 0) {
113 return (
114 <div className="panel muted" style={{ padding: "40px 20px", textAlign: "center" }}>
115 No results for this selection yet. Run a solver below, or load a
116 result file produced by the command line.
117 </div>
118 );
119 }
121 return (
122 <div ref={wrapRef} style={{ position: "relative", maxWidth: 860 }}>
123 <svg
124 viewBox={`0 0 ${W} ${H}`}
125 style={{ width: "100%", height: "auto", display: "block" }}
126 onMouseMove={onMove}
127 onMouseLeave={() => setHover(null)}
128 role="img"
129 aria-label="Work-precision chart: relative max error versus median solve time, log-log"
130 >
131 {/* grid */}
132 {xE.map((ex) => (
133 <line
134 key={`gx${ex}`}
135 x1={sx(10 ** ex)}
136 x2={sx(10 ** ex)}
137 y1={M.t}
138 y2={H - M.b}
139 stroke="var(--grid)"
140 strokeWidth={1}
141 />
142 ))}
143 {yE.map((ey) => (
144 <line
145 key={`gy${ey}`}
146 x1={M.l}
147 x2={W - M.r}
148 y1={sy(10 ** ey)}
149 y2={sy(10 ** ey)}
150 stroke="var(--grid)"
151 strokeWidth={1}
152 />
153 ))}
154 {/* axes */}
155 <line x1={M.l} x2={W - M.r} y1={H - M.b} y2={H - M.b} stroke="var(--border)" />
156 <line x1={M.l} x2={M.l} y1={M.t} y2={H - M.b} stroke="var(--border)" />
157 {xE.map(
158 (ex, i) =>
159 i % xStep === 0 && (
160 <text
161 key={`tx${ex}`}
162 x={sx(10 ** ex)}
163 y={H - M.b + 18}
164 textAnchor="middle"
165 fontSize={12}
166 fill="var(--text-2)"
167 >
168 {fmtPow(ex)}
169 </text>
170 )
171 )}
172 {yE.map(
173 (ey, i) =>
174 i % yStep === 0 && (
175 <text
176 key={`ty${ey}`}
177 x={M.l - 8}
178 y={sy(10 ** ey) + 4}
179 textAnchor="end"
180 fontSize={12}
181 fill="var(--text-2)"
182 >
183 {fmtPow(ey)}
184 </text>
185 )
186 )}
187 <text
188 x={(M.l + W - M.r) / 2}
189 y={H - 10}
190 textAnchor="middle"
191 fontSize={13}
192 fill="var(--text-2)"
193 >
194 median solve time (seconds)
195 </text>
196 <text
197 x={16}
198 y={(M.t + H - M.b) / 2}
199 textAnchor="middle"
200 fontSize={13}
201 fill="var(--text-2)"
202 transform={`rotate(-90 16 ${(M.t + H - M.b) / 2})`}
203 >
204 relative max error
205 </text>
206 {/* curves */}
207 {nonEmpty.map((c) => {
208 // Connected in order of the solver's own resolution parameter,
209 // which is what traces the curve. Time need not increase with n
210 // (a solver can get both slower and less accurate as n falls),
211 // so the curve may double back; ordering by time instead would
212 // produce a meaningless zigzag.
213 const pts = [...c.points].sort((a, b) => a.n - b.n);
214 const path = pts
215 .map(
216 (p, i) =>
217 `${i === 0 ? "M" : "L"}${sx(p.solveSeconds).toFixed(1)},${sy(Math.max(p.relMax, 1e-17)).toFixed(1)}`
218 )
219 .join("");
220 // Label at the rightmost point, which need not be the last one.
221 const last = pts.reduce((a, b) =>
222 b.solveSeconds > a.solveSeconds ? b : a
223 );
224 return (
225 <g key={c.key}>
226 <path
227 d={path}
228 fill="none"
229 stroke={c.color}
230 strokeWidth={2}
231 strokeDasharray={c.dash}
232 />
233 {pts.map((p, i) => (
234 <circle
235 key={i}
236 cx={sx(p.solveSeconds)}
237 cy={sy(Math.max(p.relMax, 1e-17))}
238 r={4}
239 fill={c.open ? "var(--surface)" : c.color}
240 stroke={c.color}
241 strokeWidth={c.open ? 2 : 0}
242 />
243 ))}
244 {nonEmpty.length <= 4 && (
245 <text
246 x={sx(last.solveSeconds) + 9}
247 y={sy(Math.max(last.relMax, 1e-17)) + 4}
248 fontSize={12}
249 fill="var(--text-2)"
250 >
251 {c.label}
252 </text>
253 )}
254 </g>
255 );
256 })}
257 {hover && (
258 <circle
259 cx={hover.px}
260 cy={hover.py}
261 r={7}
262 fill="none"
263 stroke={hover.curve.color}
264 strokeWidth={2}
265 />
266 )}
267 </svg>
268 {hover && wrapRef.current && (
269 <div
270 className="chart-tooltip"
271 style={{
272 left: `${(hover.px / W) * wrapRef.current.clientWidth + 12}px`,
273 top: `${(hover.py / H) * (wrapRef.current.clientWidth * (H / W)) - 10}px`,
274 }}
275 >
276 <div>
277 <strong>{hover.curve.label}</strong>
278 </div>
279 <div>n = {hover.point.n}</div>
280 <div>rel max error = {hover.point.relMax.toExponential(2)}</div>
281 <div>rel L2 error = {hover.point.relL2.toExponential(2)}</div>
282 <div>solve = {(hover.point.solveSeconds * 1000).toPrecision(3)} ms</div>
283 </div>
284 )}
285 </div>
286 );