1import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2import ScriptEditor from './editor/ScriptEditor.tsx'
3import Controls, { type Settings } from './panels/Controls.tsx'
4import InterpolantPanel from './panels/InterpolantPanel.tsx'
5import BlendingPanel from './panels/BlendingPanel.tsx'
6import PolesPanel from './panels/PolesPanel.tsx'
7import ConvergencePanel from './panels/ConvergencePanel.tsx'
8import { Engine } from './engine/runner.ts'
9import type { ConvergeOut, ConvergeParams, ExploreOut, ExploreParams, Want } from './engine/types.ts'
10import { DEFAULT_METHOD, METHODS, findMethod } from './methods/index.ts'
12type TabId = 'interpolant' | 'blending' | 'poles' | 'convergence'
14const TABS: { id: TabId; label: string }[] = [
15 { id: 'interpolant', label: 'Interpolant' },
16 { id: 'blending', label: 'Blending & weights' },
17 { id: 'poles', label: 'Poles' },
18 { id: 'convergence', label: 'Convergence' },
19]
21const NGRID = 1200
22const NGRID_WIDE = 1400
23const ROOTS_MAX_N = 40
25function nSeries(maxN: number): number[] {
26 const all = [10, 20, 40, 80, 160, 320, 640]
27 return all.filter((n) => n <= maxN)
28}
30export default function App() {
31 const [methodId, setMethodId] = useState(DEFAULT_METHOD.id)
32 const [script, setScript] = useState(() => DEFAULT_METHOD.source)
33 const [dirty, setDirty] = useState(false)
34 const [tab, setTab] = useState<TabId>('interpolant')
35 // The editor is the point of the app, so it starts open; anyone who would
36 // rather have the plots alone can hide it, and that choice persists. The key
37 // is versioned because an earlier build defaulted to collapsed and wrote a '0'
38 // that would otherwise keep overriding this default for returning visitors.
39 const [editorOpen, setEditorOpen] = useState(() => {
40 try {
41 return localStorage.getItem('br-editor-open-2') !== '0'
42 } catch {
43 return true
44 }
45 })
46 useEffect(() => {
47 try {
48 localStorage.setItem('br-editor-open-2', editorOpen ? '1' : '0')
49 } catch {
50 /* private mode */
51 }
52 }, [editorOpen])
53 const [settings, setSettings] = useState<Settings>({
54 f: 'runge',
55 fexpr: 'exp(-x.^2) .* cos(3*x)',
56 a: -5,
57 b: 5,
58 n: 20,
59 d: 3,
60 nodes: 'uniform',
61 seed: 1,
62 })
64 const [showPoly, setShowPoly] = useState(false)
65 const [showSpline, setShowSpline] = useState(false)
66 const [showClassical, setShowClassical] = useState(true)
68 const [explore, setExplore] = useState<ExploreOut | null>(null)
69 const [error, setError] = useState<string | null>(null)
70 const [output, setOutput] = useState('')
71 const [busy, setBusy] = useState(true)
72 const [ms, setMs] = useState<number | null>(null)
74 // the convergence study runs only when asked
75 const [convDs, setConvDs] = useState([0, 1, 2, 3, 4])
76 const [convMaxN, setConvMaxN] = useState(320)
77 const [convSpline, setConvSpline] = useState(true)
78 const [convPoly, setConvPoly] = useState(false)
79 const [conv, setConv] = useState<ConvergeOut | null>(null)
80 const [convKey, setConvKey] = useState<string | null>(null)
81 const [convRunning, setConvRunning] = useState(false)
83 const engineRef = useRef<Engine | null>(null)
84 if (!engineRef.current) engineRef.current = new Engine()
85 useEffect(() => () => engineRef.current?.dispose(), [])
87 const want: Want = useMemo(
88 () => ({
89 poly: tab === 'interpolant' && showPoly,
90 spline: tab === 'interpolant' && showSpline,
91 blend: tab === 'blending',
92 poles: tab === 'poles',
93 classical: tab === 'poles' && showClassical,
94 }),
95 [tab, showPoly, showSpline, showClassical],
96 )
98 const exploreParams: ExploreParams = useMemo(
99 () => ({
100 mode: 'explore',
101 f: settings.f,
102 fexpr: settings.fexpr,
103 a: settings.a,
104 b: settings.b,
105 n: settings.n,
106 d: Math.min(settings.d, settings.n),
107 nodes: settings.nodes,
108 seed: settings.seed,
109 ngrid: NGRID,
110 ngridwide: NGRID_WIDE,
111 rootsMaxN: ROOTS_MAX_N,
112 want,
113 }),
114 [settings, want],
115 )
117 // Re-run whenever the script or any parameter changes. The debounce keeps a
118 // dragged slider from queueing a run per pixel; the engine serialises what
119 // does get through, so the last one always wins.
120 const [runToken, setRunToken] = useState(0)
121 const paramsKey = JSON.stringify(exploreParams)
122 useEffect(() => {
123 if (tab === 'convergence') return
124 let cancelled = false
125 setBusy(true)
126 const timer = setTimeout(async () => {
127 const res = await engineRef.current!.run<ExploreOut>(script, exploreParams)
128 if (cancelled) return
129 setBusy(false)
130 setMs(res.ms)
131 setOutput(res.output)
132 if (res.ok) {
133 setExplore(res.data)
134 setError(null)
135 } else {
136 setError(res.error)
137 }
138 }, 110)
139 return () => {
140 cancelled = true
141 clearTimeout(timer)
142 }
143 // paramsKey stands in for exploreParams, which is rebuilt every render
144 // eslint-disable-next-line react-hooks/exhaustive-deps
145 }, [script, paramsKey, tab, runToken])
147 const convParams: ConvergeParams = useMemo(
148 () => ({
149 mode: 'converge',
150 f: settings.f,
151 fexpr: settings.fexpr,
152 a: settings.a,
153 b: settings.b,
154 nodes: settings.nodes,
155 seed: settings.seed,
156 ngrid: 4001,
157 ns: nSeries(convMaxN),
158 ds: convDs,
159 want: { poly: convPoly, spline: convSpline, blend: false, poles: false, classical: false },
160 }),
161 [settings, convMaxN, convDs, convPoly, convSpline],
162 )
163 const convParamsKey = JSON.stringify(convParams) + script
165 const runConvergence = useCallback(async () => {
166 setConvRunning(true)
167 setError(null)
168 const key = convParamsKey
169 const res = await engineRef.current!.run<ConvergeOut>(script, convParams)
170 setConvRunning(false)
171 setMs(res.ms)
172 setOutput(res.output)
173 if (res.ok) {
174 setConv(res.data)
175 setConvKey(key)
176 setError(null)
177 } else {
178 setError(res.error)
179 }
180 }, [convParams, convParamsKey, script])
182 const pickMethod = (id: string) => {
183 const m = findMethod(id)
184 if (!m) return
185 setMethodId(id)
186 setScript(m.source)
187 setDirty(false)
188 }
190 const patch = (p: Partial<Settings>) => setSettings((s) => ({ ...s, ...p }))
191 const method = findMethod(methodId)
193 return (
194 <div className="app">
195 <header className="header">
196 <div className="title">
197 <h1>Barycentric rational interpolation</h1>
198 <p>
199 Floater & Hormann,{' '}
200 <a href="https://doi.org/10.1007/s00211-007-0093-y" target="_blank" rel="noreferrer">
201 Numer. Math. <b>107</b> (2007) 315–331
202 </a>
203 . Every plot is computed live by the short MATLAB script on the left, which you can
204 edit; it runs in your browser through{' '}
205 <a href="https://numbl.org" target="_blank" rel="noreferrer">
206 numbl
207 </a>
208 .
209 </p>
210 </div>
211 <a
212 className="repo-link"
213 href="https://github.com/concept-collection/barycentric-rational"
214 target="_blank"
215 rel="noreferrer"
216 >
217 source
218 </a>
219 </header>
221 {!editorOpen && (
222 <button className="editor-strip" onClick={() => setEditorOpen(true)}>
223 <span aria-hidden="true">▸</span>
224 <span>
225 Open the MATLAB script that computes these plots
226 {dirty && <span className="strip-note"> (edited)</span>}
227 </span>
228 {error && <span className="strip-alert">the script failed</span>}
229 </button>
230 )}
232 <div className="body">
233 {editorOpen && (
234 <section className="left">
235 <div className="script-head">
236 <label className="field">
237 <span className="field-label">method</span>
238 <select value={dirty ? '' : methodId} onChange={(e) => pickMethod(e.target.value)}>
239 {dirty && <option value="">(edited)</option>}
240 {METHODS.map((m) => (
241 <option key={m.id} value={m.id}>
242 {m.name}
243 </option>
244 ))}
245 </select>
246 </label>
247 <div className="script-actions">
248 <button
249 className="primary"
250 onClick={() => setRunToken((v) => v + 1)}
251 disabled={busy || convRunning}
252 title="⌘/Ctrl+Enter"
253 >
254 {busy || convRunning ? 'Running…' : 'Run ▶'}
255 </button>
256 <button className="ghost" onClick={() => setEditorOpen(false)} title="collapse the editor">
257 hide ◂
258 </button>
259 </div>
260 </div>
261 {method && !dirty && <p className="method-blurb">{method.blurb}</p>}
262 {dirty && <p className="method-blurb edited">Edited. Pick a method above to start over.</p>}
264 <ScriptEditor
265 value={script}
266 onChange={(s) => {
267 setScript(s)
268 setDirty(s !== findMethod(methodId)?.source)
269 }}
270 onRun={() => setRunToken((v) => v + 1)}
271 />
273 <div className="contract">
274 <div className="contract-head">What the app calls</div>
275 <code>w = bary_weights(x, d)</code>
276 <code>r = bary_eval(x, y, w, t)</code>
277 <code className="opt">[P, L] = local_blend(x, y, d, t)</code>
278 <span className="contract-note">the third is optional; without it the second tab is empty</span>
279 </div>
281 {error && (
282 <div className="error-box">
283 <div className="error-head">The script failed</div>
284 <pre>{error}</pre>
285 </div>
286 )}
287 {output.trim() && !error && (
288 <details className="console">
289 <summary>console output</summary>
290 <pre>{output}</pre>
291 </details>
292 )}
293 </section>
294 )}
296 <section className="right">
297 <nav className="tabs">
298 {TABS.map((t) => (
299 <button key={t.id} className={`tab ${tab === t.id ? 'on' : ''}`} onClick={() => setTab(t.id)}>
300 {t.label}
301 </button>
302 ))}
303 <span className="run-status">
304 {busy || convRunning ? 'running…' : ms != null ? `${Math.round(ms)} ms` : ''}
305 </span>
306 </nav>
308 <Controls value={settings} onChange={patch} showN={tab !== 'convergence'} busy={false} />
310 <div className="panel-scroll">
311 {tab === 'convergence' ? (
312 <ConvergencePanel
313 out={conv}
314 running={convRunning}
315 stale={conv != null && convKey !== convParamsKey}
316 f={settings.f}
317 nodes={settings.nodes}
318 ds={convDs}
319 maxN={convMaxN}
320 showSpline={convSpline}
321 showPoly={convPoly}
322 onChange={(p) => {
323 if (p.ds) setConvDs(p.ds)
324 if (p.maxN) setConvMaxN(p.maxN)
325 if (p.showSpline !== undefined) setConvSpline(p.showSpline)
326 if (p.showPoly !== undefined) setConvPoly(p.showPoly)
327 }}
328 onRun={runConvergence}
329 />
330 ) : explore == null ? (
331 <div className="panel">
332 <p className="panel-note muted">{error ? 'Fix the script to see the plots.' : 'Starting numbl…'}</p>
333 </div>
334 ) : tab === 'interpolant' ? (
335 <InterpolantPanel
336 out={explore}
337 showPoly={showPoly}
338 showSpline={showSpline}
339 onToggle={(which, on) => (which === 'poly' ? setShowPoly(on) : setShowSpline(on))}
340 />
341 ) : tab === 'blending' ? (
342 <BlendingPanel out={explore} />
343 ) : (
344 <PolesPanel out={explore} showClassical={showClassical} onToggleClassical={setShowClassical} />
345 )}
346 </div>
347 </section>
348 </div>
349 </div>
350 )
351}