/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / app / components / FieldView.tsx
134 lines · 4.5 KBCodeBlameHistory
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 1// Renders a scalar field on the visualization grid as a masked heatmap
2// with a colorbar. Two modes: "diverging" for signed fields (the solution)
3// and "logmag" for magnitudes on a log scale (the error).
5import { useEffect, useRef } from "react";
6import type { Laplace2dInstance } from "../../problems/laplace2d/spec";
7import { insideDomain, vizGrid, VIZ_NGRID } from "../../problems/laplace2d/exact";
8import { cssColor, divergingColor, isDarkMode, sequentialColor } from "../colors";
10export interface FieldViewProps {
11 inst: Laplace2dInstance;
12 /** Values on the viz grid, index p = ix * ngrid + iy. */
13 values: Float64Array;
14 mode: "diverging" | "logmag";
15 title: string;
16 caption?: string;
19export function FieldView({ inst, values, mode, title, caption }: FieldViewProps) {
20 const canvasRef = useRef<HTMLCanvasElement>(null);
21 const barRef = useRef<HTMLCanvasElement>(null);
22 const rangeRef = useRef<HTMLDivElement>(null);
24 useEffect(() => {
25 const canvas = canvasRef.current;
26 const bar = barRef.current;
27 if (!canvas || !bar) return;
29 const draw = () => {
30 const dark = isDarkMode();
31 const ngrid = VIZ_NGRID;
32 const { xs } = vizGrid(inst);
33 // range
34 let vmin = Infinity;
35 let vmax = -Infinity;
36 for (let ix = 0; ix < ngrid; ix++) {
37 for (let iy = 0; iy < ngrid; iy++) {
38 if (!insideDomain(inst, xs[ix], xs[iy])) continue;
39 const v = values[ix * ngrid + iy];
40 if (!isFinite(v)) continue;
41 vmin = Math.min(vmin, v);
42 vmax = Math.max(vmax, v);
43 }
44 }
45 let lo: number;
46 let hi: number;
47 let scale: (v: number) => number;
48 if (mode === "diverging") {
49 const vabs = Math.max(Math.abs(vmin), Math.abs(vmax), 1e-300);
50 lo = -vabs;
51 hi = vabs;
52 scale = (v) => v / vabs; // [-1, 1]
53 } else {
54 hi = Math.max(vmax, 1e-300);
55 lo = Math.max(vmin, hi * 1e-8, 1e-300);
56 const llo = Math.log10(lo);
57 const lhi = Math.log10(hi);
58 scale = (v) =>
59 (Math.log10(Math.min(Math.max(v, lo), hi)) - llo) / Math.max(lhi - llo, 1e-12);
60 }
62 canvas.width = ngrid;
63 canvas.height = ngrid;
64 const ctx = canvas.getContext("2d");
65 if (!ctx) return;
66 const img = ctx.createImageData(ngrid, ngrid);
67 for (let ix = 0; ix < ngrid; ix++) {
68 for (let iy = 0; iy < ngrid; iy++) {
69 const px = ix;
70 const py = ngrid - 1 - iy;
71 const o = (py * ngrid + px) * 4;
72 if (!insideDomain(inst, xs[ix], xs[iy])) {
73 img.data[o + 3] = 0;
74 continue;
75 }
76 const v = values[ix * ngrid + iy];
77 const rgb =
78 mode === "diverging"
79 ? divergingColor(scale(v), dark)
80 : sequentialColor(scale(v), dark);
81 img.data[o] = Math.round(rgb[0]);
82 img.data[o + 1] = Math.round(rgb[1]);
83 img.data[o + 2] = Math.round(rgb[2]);
84 img.data[o + 3] = 255;
85 }
86 }
87 ctx.putImageData(img, 0, 0);
89 // colorbar
90 const bw = 220;
91 const bh = 10;
92 bar.width = bw;
93 bar.height = bh;
94 const bctx = bar.getContext("2d");
95 if (!bctx) return;
96 for (let i = 0; i < bw; i++) {
97 const t = i / (bw - 1);
98 const rgb =
99 mode === "diverging"
100 ? divergingColor(2 * t - 1, dark)
101 : sequentialColor(t, dark);
102 bctx.fillStyle = cssColor(rgb);
103 bctx.fillRect(i, 0, 1, bh);
104 }
105 if (rangeRef.current) {
106 const fmt = (v: number) =>
107 mode === "logmag" ? v.toExponential(1) : v.toPrecision(3);
108 rangeRef.current.textContent = `${fmt(lo)}${fmt(hi)}`;
109 }
110 };
112 draw();
113 const mq = window.matchMedia("(prefers-color-scheme: dark)");
114 mq.addEventListener("change", draw);
115 return () => mq.removeEventListener("change", draw);
116 }, [inst, values, mode]);
118 return (
119 <figure style={{ margin: 0 }}>
120 <div className="small" style={{ fontWeight: 600, marginBottom: 4 }}>
121 {title}
122 </div>
123 <canvas
124 ref={canvasRef}
125 style={{ width: 300, height: 300, imageRendering: "auto" }}
126 />
127 <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4 }}>
128 <canvas ref={barRef} style={{ width: 220, height: 10, borderRadius: 3 }} />
129 </div>
130 <div ref={rangeRef} className="small muted" />
131 {caption && <figcaption className="field-caption" style={{ maxWidth: 300 }}>{caption}</figcaption>}
132 </figure>
133 );
moveopenescclose