/ tutorials / laplace-dirichlet
Sign in
tutorials / laplace-dirichlet
laplace-dirichlet / src / plot / palette.ts
127 lines · 4.1 KBBlameHistoryRaw
1// Colours, assigned by the job each one does, taken unchanged from the
2// reference data-viz palette (dark steps, dark surface #1a1a19) and validated
3// as a set:
4//
5// node scripts/validate_palette.js \
6// "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9,#e66767" --mode dark
7// lightness band PASS chroma floor PASS CVD separation PASS (worst 8.4)
8// normal-vision floor PASS (worst 19.3) contrast vs surface PASS
9//
10// The worst adjacent pair sits in the 6-8 CVD band's neighbourhood, so every
11// plot that shows more than two methods also carries a legend, a distinct
12// marker shape per method, and direct end-labels: identity is never colour
13// alone. If you add a series, re-run the validator rather than picking a hex
14// that looks nice.
16export const surface = '#1a1a19'
18/**
19 * Identity: which method produced a curve. Fixed slots in the palette's
20 * documented order; never cycled, never reassigned when a plot shows a subset.
21 */
22export const method = {
23 /** second-order finite differences */
24 fd2: '#3987e5',
25 /** fourth-order finite differences (1-D) and the compact 9-point stencil (2-D) */
26 fd4: '#d95926',
27 /** Chebyshev spectral collocation */
28 spectral: '#199e70',
29 /** the FFT-based fast Poisson solver */
30 fast: '#c98500',
31 /** Krylov and multigrid iterations */
32 iterative: '#d55181',
33 /** the method of particular solutions */
34 mps: '#9085e9',
35 /** boundary integral equations */
36 bie: '#e66767',
37} as const
39export type MethodKey = keyof typeof method
41/** Marker shapes, so that identity survives greyscale and colour blindness. */
42export const marker: Record<MethodKey, 'circle' | 'square' | 'triangle' | 'diamond' | 'cross' | 'plus' | 'star'> = {
43 fd2: 'circle',
44 fd4: 'square',
45 spectral: 'triangle',
46 fast: 'diamond',
47 iterative: 'cross',
48 mps: 'plus',
49 bie: 'star',
52/** State. Reserved: never reused as a series colour. */
53export const status = {
54 good: '#008300',
55 warning: '#c98500',
56 critical: '#e66767',
57} as const
59/** Sequential ramp for magnitude (a field, an error, a residual): one hue. */
60export const sequential = [
61 '#cde2fb',
62 '#b7d3f6',
63 '#9ec5f4',
64 '#86b6ef',
65 '#6da7ec',
66 '#5598e7',
67 '#3987e5',
68 '#2a78d6',
69 '#256abf',
70 '#1c5cab',
71 '#184f95',
72 '#104281',
73 '#0d366b',
74] as const
76/** Diverging pair for a signed field: two poles, neutral grey midpoint. */
77export const diverging = {
78 low: '#3987e5',
79 mid: '#383835',
80 high: '#e66767',
81} as const
83/** Chart chrome and ink. Text always wears these, never a series colour. */
84export const ink = {
85 primary: '#e8e6df',
86 secondary: '#c3c2b7',
87 muted: '#8a8880',
88 grid: '#272724',
89 axis: '#3c3c38',
90 /** the exact solution: the reference everything is measured against */
91 reference: '#8a8880',
92 /** grid nodes, boundary points, collocation points */
93 node: '#e8e6df',
94 /** a family of curves drawn as a mass */
95 family: '#4a4a45',
96} as const
98function hexToRgb(hex: string): [number, number, number] {
99 const v = parseInt(hex.slice(1), 16)
100 return [(v >> 16) & 255, (v >> 8) & 255, v & 255]
103function rgbToHex(r: number, g: number, b: number): string {
104 const c = (x: number) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0')
105 return `#${c(r)}${c(g)}${c(b)}`
108function mix(a: string, b: string, t: number): string {
109 const [r1, g1, b1] = hexToRgb(a)
110 const [r2, g2, b2] = hexToRgb(b)
111 return rgbToHex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t)
114/** Sequential colour for t in [0, 1]: light at 0 (near zero), dark at 1. */
115export function seqColor(t: number): string {
116 if (!isFinite(t)) return ink.grid
117 const u = Math.max(0, Math.min(1, t)) * (sequential.length - 1)
118 const i = Math.floor(u)
119 return i >= sequential.length - 1 ? sequential[sequential.length - 1] : mix(sequential[i], sequential[i + 1], u - i)
122/** Diverging colour for t in [-1, 1]: blue below, grey at zero, red above. */
123export function divColor(t: number): string {
124 if (!isFinite(t)) return ink.grid
125 const u = Math.max(-1, Math.min(1, t))
126 return u < 0 ? mix(diverging.mid, diverging.low, -u) : mix(diverging.mid, diverging.high, u)
↑↓ move↵openescclose