// Colours, assigned by the job each one does, taken unchanged from the // reference data-viz palette (dark steps, dark surface #1a1a19) and validated // as a set: // // node scripts/validate_palette.js \ // "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9,#e66767" --mode dark // lightness band PASS chroma floor PASS CVD separation PASS (worst 8.4) // normal-vision floor PASS (worst 19.3) contrast vs surface PASS // // The worst adjacent pair sits in the 6-8 CVD band's neighbourhood, so every // plot that shows more than two methods also carries a legend, a distinct // marker shape per method, and direct end-labels: identity is never colour // alone. If you add a series, re-run the validator rather than picking a hex // that looks nice. export const surface = '#1a1a19' /** * Identity: which method produced a curve. Fixed slots in the palette's * documented order; never cycled, never reassigned when a plot shows a subset. */ export const method = { /** second-order finite differences */ fd2: '#3987e5', /** fourth-order finite differences (1-D) and the compact 9-point stencil (2-D) */ fd4: '#d95926', /** Chebyshev spectral collocation */ spectral: '#199e70', /** the FFT-based fast Poisson solver */ fast: '#c98500', /** Krylov and multigrid iterations */ iterative: '#d55181', /** the method of particular solutions */ mps: '#9085e9', /** boundary integral equations */ bie: '#e66767', } as const export type MethodKey = keyof typeof method /** Marker shapes, so that identity survives greyscale and colour blindness. */ export const marker: Record = { fd2: 'circle', fd4: 'square', spectral: 'triangle', fast: 'diamond', iterative: 'cross', mps: 'plus', bie: 'star', } /** State. Reserved: never reused as a series colour. */ export const status = { good: '#008300', warning: '#c98500', critical: '#e66767', } as const /** Sequential ramp for magnitude (a field, an error, a residual): one hue. */ export const sequential = [ '#cde2fb', '#b7d3f6', '#9ec5f4', '#86b6ef', '#6da7ec', '#5598e7', '#3987e5', '#2a78d6', '#256abf', '#1c5cab', '#184f95', '#104281', '#0d366b', ] as const /** Diverging pair for a signed field: two poles, neutral grey midpoint. */ export const diverging = { low: '#3987e5', mid: '#383835', high: '#e66767', } as const /** Chart chrome and ink. Text always wears these, never a series colour. */ export const ink = { primary: '#e8e6df', secondary: '#c3c2b7', muted: '#8a8880', grid: '#272724', axis: '#3c3c38', /** the exact solution: the reference everything is measured against */ reference: '#8a8880', /** grid nodes, boundary points, collocation points */ node: '#e8e6df', /** a family of curves drawn as a mass */ family: '#4a4a45', } as const function hexToRgb(hex: string): [number, number, number] { const v = parseInt(hex.slice(1), 16) return [(v >> 16) & 255, (v >> 8) & 255, v & 255] } function rgbToHex(r: number, g: number, b: number): string { const c = (x: number) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0') return `#${c(r)}${c(g)}${c(b)}` } function mix(a: string, b: string, t: number): string { const [r1, g1, b1] = hexToRgb(a) const [r2, g2, b2] = hexToRgb(b) return rgbToHex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t) } /** Sequential colour for t in [0, 1]: light at 0 (near zero), dark at 1. */ export function seqColor(t: number): string { if (!isFinite(t)) return ink.grid const u = Math.max(0, Math.min(1, t)) * (sequential.length - 1) const i = Math.floor(u) return i >= sequential.length - 1 ? sequential[sequential.length - 1] : mix(sequential[i], sequential[i + 1], u - i) } /** Diverging colour for t in [-1, 1]: blue below, grey at zero, red above. */ export function divColor(t: number): string { if (!isFinite(t)) return ink.grid const u = Math.max(-1, Math.min(1, t)) return u < 0 ? mix(diverging.mid, diverging.low, -u) : mix(diverging.mid, diverging.high, u) }