/ concept-collection / barycentric-rational
Sign in
concept-collection / barycentric-rational
barycentric-rational / src / panels / InterpolantPanel.tsx
185 lines · 6.5 KBBlameHistoryRaw
1import { useState } from 'react'
2import Plot from '../plot/Plot.tsx'
3import Legend, { type LegendItem } from '../plot/Legend.tsx'
4import { ink, series } from '../plot/palette.ts'
5import { extent, linePath, nearestIndex, padDomain, type Frame } from '../plot/scales.ts'
6import type { ExploreOut, Num } from '../engine/types.ts'
8interface Props {
9 out: ExploreOut
10 showPoly: boolean
11 showSpline: boolean
12 onToggle: (which: 'poly' | 'spline', on: boolean) => void
15function fmtErr(v: Num): string {
16 if (v == null || !isFinite(v)) return 'off scale'
17 if (v === 0) return '0'
18 return v.toExponential(1)
21/** Largest |value| of a series, ignoring the parts that ran off to infinity. */
22function maxAbs(v: readonly Num[] | undefined, ref: readonly number[]): Num {
23 if (!v) return null
24 let m = 0
25 let any = false
26 for (let i = 0; i < v.length; i++) {
27 const d = v[i]
28 if (d == null || !isFinite(d)) continue
29 any = true
30 m = Math.max(m, Math.abs(d - ref[i]))
31 }
32 return any ? m : null
35export default function InterpolantPanel({ out, showPoly, showSpline, onToggle }: Props) {
36 const [hoverX, setHoverX] = useState<number | null>(null)
38 // The polynomial interpolant at equispaced nodes is the whole point of the
39 // paper's opening paragraph, and at n = 40 it is off scale by a factor of
40 // 10^8. Scale the axis to f and to r, and let the rest leave the frame.
41 const yd = padDomain(extent(out.ft, out.r, showSpline ? out.rspline : undefined), 0.1)
42 const errOf = (v: readonly Num[] | undefined): Num[] | undefined =>
43 v ? v.map((d, i) => (d == null ? null : d - out.ft[i])) : undefined
44 const errPoly = showPoly ? errOf(out.rpoly) : undefined
45 const errSpline = showSpline ? errOf(out.rspline) : undefined
46 const eAll = extent(out.err, errPoly, errSpline)
47 const eMax = Math.max(Math.abs(eAll[0]), Math.abs(eAll[1]), 1e-16)
48 const ed: [number, number] = [-eMax * 1.1, eMax * 1.1]
50 const hi = hoverX == null ? -1 : nearestIndex(out.t, hoverX)
51 const at = (v: readonly Num[] | undefined) => (v && hi >= 0 ? v[hi] : null)
53 const legend: LegendItem[] = [
54 { label: 'f(x)', color: ink.reference, dash: '5 4', value: undefined },
55 { label: 'rational r(x)', color: series.r, value: fmtErr(out.maxerr) },
56 ]
57 if (showPoly) {
58 legend.push({
59 label: `polynomial (degree ${out.n})`,
60 color: series.poly,
61 value: fmtErr(maxAbs(out.rpoly, out.ft)),
62 })
63 }
64 if (showSpline) {
65 legend.push({ label: 'cubic spline', color: series.spline, value: fmtErr(maxAbs(out.rspline, out.ft)) })
66 }
67 legend.push({ label: `${out.n + 1} nodes`, color: ink.node, shape: 'dot' })
69 const nodes = (f: Frame) =>
70 out.x.map((xv, i) => (
71 <circle
72 key={i}
73 cx={f.sx(xv)}
74 cy={f.sy(out.y[i])}
75 r={3.2}
76 fill={ink.node}
77 stroke="#151a21"
78 strokeWidth={1.5}
79 />
80 ))
82 return (
83 <div className="panel">
84 <p className="panel-lede">
85 The rational interpolant r of equation (1) through {out.n + 1} nodes, with blend degree d = {out.d}.
86 Turn on the degree-{out.n} polynomial to see what the paper's first page is about, and the clamped
87 C<sup>2</sup> cubic spline for the comparison of Tables 3 and 4.
88 </p>
90 <div className="row-controls">
91 <label className="check">
92 <input type="checkbox" checked={showPoly} onChange={(e) => onToggle('poly', e.target.checked)} />
93 polynomial interpolant
94 </label>
95 <label className="check">
96 <input type="checkbox" checked={showSpline} onChange={(e) => onToggle('spline', e.target.checked)} />
97 cubic spline
98 </label>
99 </div>
101 <Legend items={legend} />
103 <Plot
104 height={330}
105 xDomain={[out.t[0], out.t[out.t.length - 1]]}
106 yDomain={yd}
107 xLabel="x"
108 yLabel="f, r"
109 onHoverX={setHoverX}
110 hoverX={hoverX}
111 >
112 {(f) => (
113 <>
114 <path d={linePath(out.t, out.ft, f)} fill="none" stroke={ink.reference} strokeWidth={2} strokeDasharray="5 4" />
115 {showSpline && out.rspline && (
116 <path d={linePath(out.t, out.rspline, f)} fill="none" stroke={series.spline} strokeWidth={2} />
117 )}
118 {showPoly && out.rpoly && (
119 <path d={linePath(out.t, out.rpoly, f)} fill="none" stroke={series.poly} strokeWidth={2} />
120 )}
121 <path d={linePath(out.t, out.r, f)} fill="none" stroke={series.r} strokeWidth={2.5} />
122 {nodes(f)}
123 </>
124 )}
125 </Plot>
127 {hi >= 0 && (
128 <div className="readout">
129 <span>
130 x = <b>{out.t[hi].toFixed(3)}</b>
131 </span>
132 <span style={{ color: ink.reference }}>
133 f = <b>{out.ft[hi].toFixed(6)}</b>
134 </span>
135 <span style={{ color: series.r }}>
136 r = <b>{at(out.r)?.toFixed(6) ?? '-'}</b>
137 </span>
138 {showPoly && (
139 <span style={{ color: series.poly }}>
140 poly = <b>{fmtSigned(at(out.rpoly))}</b>
141 </span>
142 )}
143 {showSpline && (
144 <span style={{ color: series.spline }}>
145 spline = <b>{at(out.rspline)?.toFixed(6) ?? '-'}</b>
146 </span>
147 )}
148 </div>
149 )}
151 <h4 className="sub">Error</h4>
152 <p className="panel-note">
153 r(x) &minus; f(x) on the same grid. It vanishes at every node, by construction, and the largest of
154 the bumps between them is the number Tables 1 to 4 tabulate.
155 </p>
156 <Plot
157 height={190}
158 xDomain={[out.t[0], out.t[out.t.length - 1]]}
159 yDomain={ed}
160 xLabel="x"
161 yLabel="r - f"
162 onHoverX={setHoverX}
163 hoverX={hoverX}
164 under={(f) => <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1} />}
165 >
166 {(f) => (
167 <>
168 {errSpline && <path d={linePath(out.t, errSpline, f)} fill="none" stroke={series.spline} strokeWidth={1.5} />}
169 {errPoly && <path d={linePath(out.t, errPoly, f)} fill="none" stroke={series.poly} strokeWidth={1.5} />}
170 <path d={linePath(out.t, out.err, f)} fill="none" stroke={series.r} strokeWidth={2} />
171 {out.x.map((xv, i) => (
172 <circle key={i} cx={f.sx(xv)} cy={f.sy(0)} r={2.2} fill={ink.node} stroke="#151a21" strokeWidth={1} />
173 ))}
174 </>
175 )}
176 </Plot>
177 </div>
178 )
181function fmtSigned(v: Num): string {
182 if (v == null || !isFinite(v)) return 'off scale'
183 if (Math.abs(v) >= 1e5) return v.toExponential(2)
184 return v.toFixed(6)
moveopenescclose