import { useMemo, useState } from 'react' import Plot from '../plot/Plot.tsx' import Legend from '../plot/Legend.tsx' import { diverging, ink, series } from '../plot/palette.ts' import { extent, linePath, padDomain, type Frame } from '../plot/scales.ts' import type { ExploreOut, Num } from '../engine/types.ts' interface Props { out: ExploreOut } /** The samples of t lying in [lo, hi], as a slice of both arrays. */ function slice(t: number[], v: readonly Num[], lo: number, hi: number): [number[], Num[]] { const ts: number[] = [] const vs: Num[] = [] for (let i = 0; i < t.length; i++) { if (t[i] >= lo && t[i] <= hi) { ts.push(t[i]) vs.push(v[i]) } } return [ts, vs] } export default function BlendingPanel({ out }: Props) { const [pinned, setPinned] = useState(null) const [hoverX, setHoverX] = useState(null) const P = out.P const L = out.L const wlo = out.wlo const whi = out.whi const m = P?.length ?? 0 // which local polynomial the reader is following: the pinned one, or the one // whose window is nearest the pointer const hovered = useMemo(() => { if (!wlo || !whi || hoverX == null) return null let best = 0 let bestD = Infinity for (let i = 0; i < wlo.length; i++) { const dd = Math.abs((wlo[i] + whi[i]) / 2 - hoverX) if (dd < bestD) { bestD = dd best = i } } return best }, [wlo, whi, hoverX]) const sel = pinned ?? hovered ?? Math.floor(m / 2) const xd: [number, number] = [out.t[0], out.t[out.t.length - 1]] const yd = padDomain(extent(out.ft, out.r), 0.12) const ld = useMemo(() => { if (!L) return [0, 1] as [number, number] const e = extent(...L) return [Math.max(-1.2, Math.min(-0.15, e[0] * 1.1)), Math.min(1.6, Math.max(1.05, e[1] * 1.05))] as [ number, number, ] }, [L]) if (!out.hasBlend || !P || !L || !wlo || !whi) { return (

This script does not define local_blend, so there is nothing to draw here. Equations (4) and (5) are optional: a script only has to supply bary_weights and{' '} bary_eval. The other three tabs still work.

{out.blendError &&
{out.blendError}
}
) } const shade = (f: Frame) => ( ) return (

Equation (4) reads r = Σi λi pi / Σi{' '} λi: slide a window of d+1 = {out.d + 1} nodes along the data, fit a polynomial of degree {out.d} in each position, and blend the {m} of them together. Hover to follow one; click to pin it.

{pinned != null && ( )}
{(f) => ( <> {P.map((row, i) => { // each p_i is only drawn a little beyond the d+1 points it // interpolates: a degree-d polynomial extrapolated across the // whole interval is a wall of noise const pad = Math.max((whi[i] - wlo[i]) * 0.35, (xd[1] - xd[0]) / (out.n * 2)) const [ts, vs] = slice(out.t, row, wlo[i] - pad, whi[i] + pad) return i === sel ? null : ( ) })} {(() => { const pad = (whi[sel] - wlo[sel]) * 0.9 + (xd[1] - xd[0]) / (out.n * 2) const [ts, vs] = slice(out.t, P[sel], wlo[sel] - pad, whi[sel] + pad) return })()} {out.x.map((xv, i) => { const inWin = xv >= wlo[sel] - 1e-12 && xv <= whi[sel] + 1e-12 return ( ) })} )}

Blending functions

The normalised λi, which sum to 1 at every x. Each one is close to 1 across its own window and decays away from it, but with a tail that oscillates in sign and never quite reaches zero: these functions have no local support, which the paper names as the price of the construction. What they do have is that their denominator never vanishes, so they are infinitely smooth.

( <> {shade(f)} )} > {(f) => ( <> {L.map((row, i) => i === sel ? null : ( ), )} {out.x.map((xv, i) => ( ))} )}
) } function WeightsSection({ out }: { out: ExploreOut }) { const wmax = Math.max(...out.w.map(Math.abs), Number.MIN_VALUE) const norm = out.w.map((v) => v / wmax) const spread = Math.max(...out.wscaled) / Math.min(...out.wscaled.filter((v) => v > 0)) return ( <>

Barycentric weights

The same interpolant, written in the form of equation (1) with the weights of equation (18). Schneider and Werner proved that a pole-free barycentric rational interpolant must have weights that alternate in sign; these {out.wAlternates ? 'do' : 'do not'}.

0', color: diverging.pos }, { label: 'w_k < 0', color: diverging.neg }, ]} /> } > {(f) => norm.map((v, k) => ( = 0 ? diverging.pos : diverging.neg} strokeWidth={2} /> = 0 ? diverging.pos : diverging.neg} stroke="#151a21" strokeWidth={1} /> )) } {out.wIsInteger ? (
δk = |wk| / min |w|, which Section 4 predicts are integers on a uniform mesh:
{out.wscaled.map((v, k) => ( {Math.round(v)} ))}
) : (
The weights are not integer multiples of the smallest one, so this is not a uniform mesh. Their magnitudes span a factor of {spread < 1e5 ? spread.toFixed(0) : spread.toExponential(1)}.
)} ) }