088be4cAn interactive tutorial on the Dirichlet problem, accuracy against costJeremy Magland 1// A scalar field on a rectangle, drawn as an image with a colour bar.
2//
3// The field is painted into a canvas one pixel per grid cell and then scaled to
4// the frame, which keeps a 512-by-512 grid as cheap to draw as a 16-by-16 one.
5// Cells outside the domain (an L-shaped region, the outside of a curve) are
6// passed as null and left as surface.
7import { useEffect, useRef, useState, useCallback } from 'react'
8import type { Num } from '../engine/types.ts'
9import { divColor, ink, seqColor, surface } from './palette.ts'
10import { formatTick, niceTicks } from './scales.ts'
12export interface FieldProps {
13 /** z[i][j] is the value at (x_j, y_i); null is "not in the domain" */
14 z: Num[][]
15 xDomain: [number, number]
16 yDomain: [number, number]
17 height?: number
18 /** 'seq' for a magnitude, 'div' for a signed quantity, 'log' for |value| over decades */
19 kind?: 'seq' | 'div' | 'log'
20 /** upper end of the colour scale; taken from the data when omitted */
21 zmax?: number
22 /** how many decades the 'log' scale spans below zmax */
23 decades?: number
24 title?: string
25 /** polylines in data coordinates, drawn over the field (a boundary, a mesh) */
26 curves?: { x: number[]; y: number[]; color?: string; dash?: string }[]
27 points?: { x: number[]; y: number[]; color?: string; r?: number }[]
28 formatValue?: (v: number) => string
29}
31function useWidth(): [(el: HTMLDivElement | null) => void, number] {
32 const [width, setWidth] = useState(560)
33 const obs = useRef<ResizeObserver | null>(null)
34 const ref = useCallback((el: HTMLDivElement | null) => {
35 obs.current?.disconnect()
36 if (!el) return
37 const ro = new ResizeObserver((entries) => {
38 const w = entries[0]?.contentRect.width
39 if (w && w > 0) setWidth(w)
40 })
41 ro.observe(el)
42 obs.current = ro
43 setWidth(el.clientWidth || 560)
44 }, [])
45 useEffect(() => () => obs.current?.disconnect(), [])
46 return [ref, width]
47}
49const M = { l: 44, r: 74, t: 18, b: 30 }
51export default function Field(props: FieldProps) {
52 const { z, xDomain, yDomain, height = 340, kind = 'seq', decades = 8, title, curves, points } = props
53 const [hostRef, width] = useWidth()
54 const canvasRef = useRef<HTMLCanvasElement>(null)
56 const nrow = z.length
57 const ncol = nrow > 0 ? z[0].length : 0
59 // colour scale limits
60 let lo = Infinity
61 let hi = -Infinity
62 for (const row of z) {
63 for (const v of row) {
64 if (v == null || !isFinite(v)) continue
65 if (v < lo) lo = v
66 if (v > hi) hi = v
67 }
68 }
69 if (!isFinite(lo)) {
70 lo = 0
71 hi = 1
72 }
73 const amax = props.zmax ?? Math.max(Math.abs(lo), Math.abs(hi))
74 const logHi = props.zmax ?? (hi > 0 ? Math.log10(hi) : 0)
75 const logLo = logHi - decades
77 const colorOf = (v: Num): string | null => {
78 if (v == null || !isFinite(v)) return null
79 if (kind === 'div') return divColor(amax > 0 ? v / amax : 0)
80 if (kind === 'log') {
81 const l = Math.log10(Math.max(Math.abs(v), Number.MIN_VALUE))
82 return seqColor((l - logLo) / (logHi - logLo))
83 }
84 return seqColor(hi > lo ? (v - lo) / (hi - lo) : 0.5)
85 }
87 const iw = Math.max(10, width - M.l - M.r)
88 const ih = Math.max(10, height - M.t - M.b)
90 useEffect(() => {
91 const cv = canvasRef.current
92 if (!cv || ncol === 0) return
93 cv.width = ncol
94 cv.height = nrow
95 const ctx = cv.getContext('2d')
96 if (!ctx) return
97 const img = ctx.createImageData(ncol, nrow)
98 for (let i = 0; i < nrow; i++) {
99 // row 0 of z is the bottom of the picture
100 const dst = (nrow - 1 - i) * ncol
101 for (let j = 0; j < ncol; j++) {
102 const c = colorOf(z[i][j])
103 const o = (dst + j) * 4
104 if (c == null) {
105 img.data[o] = 26
106 img.data[o + 1] = 26
107 img.data[o + 2] = 25
108 img.data[o + 3] = 255
109 } else {
110 const n = parseInt(c.slice(1), 16)
111 img.data[o] = (n >> 16) & 255
112 img.data[o + 1] = (n >> 8) & 255
113 img.data[o + 2] = n & 255
114 img.data[o + 3] = 255
115 }
116 }
117 }
118 ctx.putImageData(img, 0, 0)
119 })
121 const sx = (v: number) => ((v - xDomain[0]) / (xDomain[1] - xDomain[0])) * iw
122 const sy = (v: number) => ih - ((v - yDomain[0]) / (yDomain[1] - yDomain[0])) * ih
124 const xt = niceTicks(xDomain[0], xDomain[1], 5)
125 const yt = niceTicks(yDomain[0], yDomain[1], 5)
127 // colour bar: 24 steps from the same scale
128 const bar = Array.from({ length: 24 }, (_, k) => k / 23)
129 const barLabel = (t: number): string => {
130 if (kind === 'div') return formatTick(Number(((2 * t - 1) * amax).toPrecision(2)))
131 if (kind === 'log') {
132 const e = logLo + t * (logHi - logLo)
133 return `1e${Math.round(e)}`
134 }
135 return formatTick(Number((lo + t * (hi - lo)).toPrecision(2)))
136 }
138 return (
139 <div className="plot-host field-host" ref={hostRef}>
140 <div style={{ position: 'relative', width, height }}>
141 <canvas
142 ref={canvasRef}
143 style={{
144 position: 'absolute',
145 left: M.l,
146 top: M.t,
147 width: iw,
148 height: ih,
149 imageRendering: ncol < 96 ? 'pixelated' : 'auto',
150 }}
151 />
152 <svg width={width} height={height} style={{ position: 'absolute', left: 0, top: 0 }} role="img">
153 <g transform={`translate(${M.l},${M.t})`}>
154 {curves?.map((c, k) => (
155 <path
156 key={`c${k}`}
157 d={c.x.map((xv, i) => `${i ? 'L' : 'M'}${sx(xv).toFixed(2)} ${sy(c.y[i]).toFixed(2)}`).join('')}
158 fill="none"
159 stroke={c.color ?? ink.node}
160 strokeWidth={1.5}
161 strokeDasharray={c.dash}
162 />
163 ))}
164 {points?.map((p, k) => (
165 <g key={`p${k}`}>
166 {p.x.map((xv, i) => (
167 <circle key={i} cx={sx(xv)} cy={sy(p.y[i])} r={p.r ?? 1.8} fill={p.color ?? ink.node} />
168 ))}
169 </g>
170 ))}
171 <rect x={0} y={0} width={iw} height={ih} fill="none" stroke={ink.axis} strokeWidth={1} />
172 {xt.map((v) => (
173 <text key={`x${v}`} x={sx(v)} y={ih + 15} fill={ink.muted} fontSize={11} textAnchor="middle">
174 {formatTick(v)}
175 </text>
176 ))}
177 {yt.map((v) => (
178 <text key={`y${v}`} x={-7} y={sy(v) + 4} fill={ink.muted} fontSize={11} textAnchor="end">
179 {formatTick(v)}
180 </text>
181 ))}
182 {title && (
183 <text x={0} y={-6} fill={ink.secondary} fontSize={11}>
184 {title}
185 </text>
186 )}
187 {/* colour bar */}
188 <g transform={`translate(${iw + 12},0)`}>
189 {bar.map((t, k) => (
190 <rect
191 key={k}
192 x={0}
193 y={ih - ((k + 1) * ih) / bar.length}
194 width={12}
195 height={ih / bar.length + 0.5}
196 fill={kind === 'div' ? divColor(2 * t - 1) : seqColor(t)}
197 />
198 ))}
199 <rect x={0} y={0} width={12} height={ih} fill="none" stroke={ink.axis} strokeWidth={1} />
200 {[0, 0.5, 1].map((t) => (
201 <text key={t} x={16} y={ih - t * ih + 4} fill={ink.muted} fontSize={10}>
202 {barLabel(t)}
203 </text>
204 ))}
205 </g>
206 </g>
207 </svg>
208 </div>
209 </div>
210 )
211}
213export { surface }