/ concept-collection / barycentric-rational
Sign in
concept-collection / barycentric-rational
barycentric-rational / src / panels / InterpolantPanel.tsx
199 lines · 7.2 KBBlameHistoryRaw
1import { useState } from 'react'
2import Plot from '../plot/Plot.tsx'
3import Legend, { type LegendItem } from '../plot/Legend.tsx'
4import More from './More.tsx'
5import SeriesToggle from './SeriesToggle.tsx'
6import { ink, series } from '../plot/palette.ts'
7import { extent, linePath, nearestIndex, padDomain, type Frame } from '../plot/scales.ts'
8import type { ExploreOut, Num } from '../engine/types.ts'
10interface Props {
11 out: ExploreOut
12 showPoly: boolean
13 showSpline: boolean
14 onToggle: (which: 'poly' | 'spline', on: boolean) => void
17function fmtErr(v: Num): string {
18 if (v == null || !isFinite(v)) return 'off scale'
19 if (v === 0) return '0'
20 return v.toExponential(1)
23/** Largest |value| of a series, ignoring the parts that ran off to infinity. */
24function maxAbs(v: readonly Num[] | undefined, ref: readonly number[]): Num {
25 if (!v) return null
26 let m = 0
27 let any = false
28 for (let i = 0; i < v.length; i++) {
29 const d = v[i]
30 if (d == null || !isFinite(d)) continue
31 any = true
32 m = Math.max(m, Math.abs(d - ref[i]))
33 }
34 return any ? m : null
37export default function InterpolantPanel({ out, showPoly, showSpline, onToggle }: Props) {
38 const [hoverX, setHoverX] = useState<number | null>(null)
40 // The polynomial interpolant at equispaced nodes is the whole point of the
41 // paper's opening paragraph, and at n = 40 it is off scale by a factor of
42 // 10^8. Scale the axis to f and to r, and let the rest leave the frame.
43 const yd = padDomain(extent(out.ft, out.r, showSpline ? out.rspline : undefined), 0.1)
44 const errOf = (v: readonly Num[] | undefined): Num[] | undefined =>
45 v ? v.map((d, i) => (d == null ? null : d - out.ft[i])) : undefined
46 const errPoly = showPoly ? errOf(out.rpoly) : undefined
47 const errSpline = showSpline ? errOf(out.rspline) : undefined
48 const eAll = extent(out.err, errPoly, errSpline)
49 const eMax = Math.max(Math.abs(eAll[0]), Math.abs(eAll[1]), 1e-16)
50 const ed: [number, number] = [-eMax * 1.1, eMax * 1.1]
52 const hi = hoverX == null ? -1 : nearestIndex(out.t, hoverX)
53 const at = (v: readonly Num[] | undefined) => (v && hi >= 0 ? v[hi] : null)
55 const legend: LegendItem[] = [
56 { label: 'f(x)', color: ink.reference, dash: '5 4', value: undefined },
57 { label: 'rational r(x)', color: series.r, value: fmtErr(out.maxerr) },
58 ]
59 if (showPoly) {
60 legend.push({
61 label: `polynomial (degree ${out.n})`,
62 color: series.poly,
63 value: fmtErr(maxAbs(out.rpoly, out.ft)),
64 })
65 }
66 if (showSpline) {
67 legend.push({ label: 'cubic spline', color: series.spline, value: fmtErr(maxAbs(out.rspline, out.ft)) })
68 }
69 legend.push({ label: `${out.n + 1} nodes`, color: ink.node, shape: 'dot' })
71 const nodes = (f: Frame) =>
72 out.x.map((xv, i) => (
73 <circle
74 key={i}
75 cx={f.sx(xv)}
76 cy={f.sy(out.y[i])}
77 r={3.2}
78 fill={ink.node}
79 stroke="#151a21"
80 strokeWidth={1.5}
81 />
82 ))
84 return (
85 <div className="panel">
86 <p className="panel-lede">
87 The rational interpolant r passes exactly through all {out.n + 1} data points and stays close to the
88 function f everywhere in between. The controls above change the data; the buttons below add two
89 standard alternatives fitted to the same points.
90 </p>
91 <More>
92 <p>
93 This is r of equation (1) with the weights of equation (18), using blend degree d = {out.d}. The
94 degree-{out.n} polynomial through the same points is the paper's opening example: on equally spaced
95 nodes it diverges as n grows, which is Runge's phenomenon. The clamped C<sup>2</sup> cubic spline is
96 the standard the paper measures itself against in Tables 3 and 4.
97 </p>
98 </More>
100 <div className="row-controls">
101 <span className="field-label">compare with</span>
102 <SeriesToggle color={series.poly} on={showPoly} onChange={(on) => onToggle('poly', on)}>
103 polynomial (degree {out.n})
104 </SeriesToggle>
105 <SeriesToggle color={series.spline} on={showSpline} onChange={(on) => onToggle('spline', on)}>
106 cubic spline
107 </SeriesToggle>
108 </div>
110 <Legend items={legend} />
112 <Plot
113 height={330}
114 xDomain={[out.t[0], out.t[out.t.length - 1]]}
115 yDomain={yd}
116 xLabel="x"
117 yLabel="f, r"
118 onHoverX={setHoverX}
119 hoverX={hoverX}
120 >
121 {(f) => (
122 <>
123 <path d={linePath(out.t, out.ft, f)} fill="none" stroke={ink.reference} strokeWidth={2} strokeDasharray="5 4" />
124 {showSpline && out.rspline && (
125 <path d={linePath(out.t, out.rspline, f)} fill="none" stroke={series.spline} strokeWidth={2} />
126 )}
127 {showPoly && out.rpoly && (
128 <path d={linePath(out.t, out.rpoly, f)} fill="none" stroke={series.poly} strokeWidth={2} />
129 )}
130 <path d={linePath(out.t, out.r, f)} fill="none" stroke={series.r} strokeWidth={2.5} />
131 {nodes(f)}
132 </>
133 )}
134 </Plot>
136 {/* always rendered, so the error plot below never jumps when hovering */}
137 <div className="readout">
138 {hi >= 0 ? (
139 <>
140 <span>
141 x = <b>{out.t[hi].toFixed(3)}</b>
142 </span>
143 <span style={{ color: ink.reference }}>
144 f = <b>{out.ft[hi].toFixed(6)}</b>
145 </span>
146 <span style={{ color: series.r }}>
147 r = <b>{at(out.r)?.toFixed(6) ?? '-'}</b>
148 </span>
149 {showPoly && (
150 <span style={{ color: series.poly }}>
151 poly = <b>{fmtSigned(at(out.rpoly))}</b>
152 </span>
153 )}
154 {showSpline && (
155 <span style={{ color: series.spline }}>
156 spline = <b>{at(out.rspline)?.toFixed(6) ?? '-'}</b>
157 </span>
158 )}
159 </>
160 ) : (
161 <span className="readout-hint">hover a plot to read values</span>
162 )}
163 </div>
165 <h4 className="sub">Error</h4>
166 <p className="panel-note">
167 The difference r &minus; f. It is zero at every node by construction; the largest bump between nodes
168 is the number the paper's tables report.
169 </p>
170 <Plot
171 height={190}
172 xDomain={[out.t[0], out.t[out.t.length - 1]]}
173 yDomain={ed}
174 xLabel="x"
175 yLabel="r - f"
176 onHoverX={setHoverX}
177 hoverX={hoverX}
178 under={(f) => <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1} />}
179 >
180 {(f) => (
181 <>
182 {errSpline && <path d={linePath(out.t, errSpline, f)} fill="none" stroke={series.spline} strokeWidth={1.5} />}
183 {errPoly && <path d={linePath(out.t, errPoly, f)} fill="none" stroke={series.poly} strokeWidth={1.5} />}
184 <path d={linePath(out.t, out.err, f)} fill="none" stroke={series.r} strokeWidth={2} />
185 {out.x.map((xv, i) => (
186 <circle key={i} cx={f.sx(xv)} cy={f.sy(0)} r={2.2} fill={ink.node} stroke="#151a21" strokeWidth={1} />
187 ))}
188 </>
189 )}
190 </Plot>
191 </div>
192 )
195function fmtSigned(v: Num): string {
196 if (v == null || !isFinite(v)) return 'off scale'
197 if (Math.abs(v) >= 1e5) return v.toExponential(2)
198 return v.toFixed(6)
moveopenescclose