// Figures for the one-dimensional labs. Every lab in Part I sends back one of
// these two shapes, so the same two panels serve all of them.
import { Fragment, useState } from 'react'
import Plot, { type PointerPx } from '../../plot/Plot.tsx'
import Legend from '../../plot/Legend.tsx'
import { EndLabel, Line, Markers, PointTip, RefSlope } from '../../plot/Curves.tsx'
import { ink, marker, method, type MethodKey } from '../../plot/palette.ts'
import { extent, padDomain, type Frame } from '../../plot/scales.ts'
import type { Num } from '../../engine/types.ts'
export interface SingleOut {
names: string[]
ms: number
n: number
x: number[]
u: Num[]
uex: number[]
err: Num[]
maxerr: Num
xf: number[]
uf: number[]
}
export interface SweepOut {
names: string[]
ns: number[]
/** per-method resolutions, when the methods do not share a ladder */
NS?: Num[][]
E: Num[][]
T: Num[][]
orders: Num[][]
}
/** The x values for series k: its own ladder if it has one. */
function xsOf(data: SweepOut, k: number): number[] {
const row = data.NS?.[k]
if (!row) return data.ns
return row.map((v) => (v == null ? NaN : v))
}
export function fmtErr(v: Num): string {
if (v == null || !isFinite(v)) return '--'
if (v === 0) return '0'
return v.toExponential(2)
}
/** A time in milliseconds, at three significant figures either side of a second. */
export function fmtMs(v: Num): string {
if (v == null || !isFinite(v)) return '--'
if (v >= 1000) return `${(v / 1000).toPrecision(3)} s`
return `${v.toPrecision(3)} ms`
}
/** The computed solution over the exact one, and the error underneath. */
export function Solution1D({ data, slot = 'fd2' }: { data: SingleOut; slot?: MethodKey }) {
const color = method[slot]
const yd = padDomain(extent(data.uf, data.u))
const ed = padDomain(extent(data.err))
const showNodes = data.x.length <= 130
return (
<>
the solution: exact, and computed at the grid points
{(f) => (
<>
{showNodes && }
>
)}
the error, computed minus exact, at the grid points
{(f) => (
<>
{showNodes && }
>
)}
>
)
}
export interface SweepStyle {
/** palette slot per method, in the order the script lists them */
slots: MethodKey[]
/** dashed reference lines: exponent, label, and an optional anchor point */
refs?: { p: number; label: string; at?: [number, number] }[]
/** per-series dash pattern, when two series share a colour on purpose */
dashes?: (string | undefined)[]
}
function seriesExtent(rows: Num[][]): [number, number] {
let lo = Infinity
let hi = -Infinity
for (const r of rows) {
for (const v of r) {
if (v == null || !isFinite(v) || v <= 0) continue
if (v < lo) lo = v
if (v > hi) hi = v
}
}
return isFinite(lo) ? [lo / 2, hi * 2] : [1e-16, 1]
}
/** Max error against resolution, on log-log axes. */
export function Convergence({
data,
style,
height = 300,
xLabel = 'N',
}: {
data: SweepOut
style: SweepStyle
height?: number
xLabel?: string
}) {
const yd = seriesExtent(data.E)
const allx = data.NS ? data.NS.flat().filter((v): v is number => v != null && v > 0) : data.ns
const xd: [number, number] = [Math.min(...allx) / 1.3, Math.max(...allx) * 1.3]
// reference lines are anchored a factor of 5 below the first data point, so
// that the reader compares slopes rather than trying to see through them
const e0 = data.E[0]?.find((v) => v != null && isFinite(v) && v > 0) ?? 1
return (
<>
max error against resolution
{(f) => (
<>
{style.refs?.map((r) => (
))}
{data.E.map((row, k) => {
const slot = style.slots[k] ?? 'fd2'
return (
)
})}
>
)}