import { useState } from 'react' import Plot from '../plot/Plot.tsx' import Legend, { type LegendItem } from '../plot/Legend.tsx' import { ink, series } from '../plot/palette.ts' import { extent, linePath, nearestIndex, padDomain, type Frame } from '../plot/scales.ts' import type { ExploreOut, Num } from '../engine/types.ts' interface Props { out: ExploreOut showPoly: boolean showSpline: boolean onToggle: (which: 'poly' | 'spline', on: boolean) => void } function fmtErr(v: Num): string { if (v == null || !isFinite(v)) return 'off scale' if (v === 0) return '0' return v.toExponential(1) } /** Largest |value| of a series, ignoring the parts that ran off to infinity. */ function maxAbs(v: readonly Num[] | undefined, ref: readonly number[]): Num { if (!v) return null let m = 0 let any = false for (let i = 0; i < v.length; i++) { const d = v[i] if (d == null || !isFinite(d)) continue any = true m = Math.max(m, Math.abs(d - ref[i])) } return any ? m : null } export default function InterpolantPanel({ out, showPoly, showSpline, onToggle }: Props) { const [hoverX, setHoverX] = useState(null) // The polynomial interpolant at equispaced nodes is the whole point of the // paper's opening paragraph, and at n = 40 it is off scale by a factor of // 10^8. Scale the axis to f and to r, and let the rest leave the frame. const yd = padDomain(extent(out.ft, out.r, showSpline ? out.rspline : undefined), 0.1) const errOf = (v: readonly Num[] | undefined): Num[] | undefined => v ? v.map((d, i) => (d == null ? null : d - out.ft[i])) : undefined const errPoly = showPoly ? errOf(out.rpoly) : undefined const errSpline = showSpline ? errOf(out.rspline) : undefined const eAll = extent(out.err, errPoly, errSpline) const eMax = Math.max(Math.abs(eAll[0]), Math.abs(eAll[1]), 1e-16) const ed: [number, number] = [-eMax * 1.1, eMax * 1.1] const hi = hoverX == null ? -1 : nearestIndex(out.t, hoverX) const at = (v: readonly Num[] | undefined) => (v && hi >= 0 ? v[hi] : null) const legend: LegendItem[] = [ { label: 'f(x)', color: ink.reference, dash: '5 4', value: undefined }, { label: 'rational r(x)', color: series.r, value: fmtErr(out.maxerr) }, ] if (showPoly) { legend.push({ label: `polynomial (degree ${out.n})`, color: series.poly, value: fmtErr(maxAbs(out.rpoly, out.ft)), }) } if (showSpline) { legend.push({ label: 'cubic spline', color: series.spline, value: fmtErr(maxAbs(out.rspline, out.ft)) }) } legend.push({ label: `${out.n + 1} nodes`, color: ink.node, shape: 'dot' }) const nodes = (f: Frame) => out.x.map((xv, i) => ( )) return (

The rational interpolant r of equation (1) through {out.n + 1} nodes, with blend degree d = {out.d}. Turn on the degree-{out.n} polynomial to see what the paper's first page is about, and the clamped C2 cubic spline for the comparison of Tables 3 and 4.

{(f) => ( <> {showSpline && out.rspline && ( )} {showPoly && out.rpoly && ( )} {nodes(f)} )} {hi >= 0 && (
x = {out.t[hi].toFixed(3)} f = {out.ft[hi].toFixed(6)} r = {at(out.r)?.toFixed(6) ?? '-'} {showPoly && ( poly = {fmtSigned(at(out.rpoly))} )} {showSpline && ( spline = {at(out.rspline)?.toFixed(6) ?? '-'} )}
)}

Error

r(x) − f(x) on the same grid. It vanishes at every node, by construction, and the largest of the bumps between them is the number Tables 1 to 4 tabulate.

} > {(f) => ( <> {errSpline && } {errPoly && } {out.x.map((xv, i) => ( ))} )}
) } function fmtSigned(v: Num): string { if (v == null || !isFinite(v)) return 'off scale' if (Math.abs(v) >= 1e5) return v.toExponential(2) return v.toFixed(6) }