import { useMemo, useState } from 'react' import Plot from '../plot/Plot.tsx' import Legend from '../plot/Legend.tsx' import More from './More.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 the construction cannot be drawn here. That is allowed: a script only has to supply bary_weights and bary_eval, and the other three tabs still work.

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

The interpolant is built from {m} simple pieces: a polynomial of degree {out.d} fitted to each run of{' '} {out.d + 1} neighbouring points, all blended into one smooth curve. Hover the plot to follow a single piece; click to pin it.

This is equation (4), r = Σi λi pi / Σ i λi: each pi interpolates the d+1 points xi, …, xi+d, and the blending functions λi of equation (5) carry the alternating signs that make the poles cancel.

{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

How much each piece counts at each x. The blending functions sum to 1 everywhere, and each is largest over its own window.

Each normalised λi decays away from its window, 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 common 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 whole construction collapses to a single weight per node. Alternating signs are what pole-freedom requires, and these weights {out.wAlternates ? 'alternate' : 'do not alternate'}.

This is the barycentric form of equation (1) with the weights of equation (18), which is how the interpolant is actually evaluated. Schneider and Werner proved that a barycentric rational interpolant with no poles in the interval must have weights that alternate in sign, so the sign pattern here is not a coincidence.

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)}.
)} ) }