/** * Chart palette. * * Categorical slots are assigned in fixed order and never cycled — a series keeps its * hue when a filter removes its neighbours. Both modes are separately stepped for their * own surface, not an automatic flip of the light values. * * Validated against this app's card surfaces (light `#ffffff`, dark `#0f1729`) with the * dataviz six-check validator, six slots, adjacent pairlist: * light — CVD ΔE 9.1, normal-vision ΔE 19.6, contrast WARN on aqua/yellow/magenta * dark — CVD ΔE 8.4, normal-vision ΔE 19.3, contrast all ≥ 3:1 * The light-mode contrast WARN obliges *relief*: every chart using these slots ships a * legend with visible text labels and a table view of the same numbers. */ export interface ChartPalette { /** Categorical slots, in fixed assignment order. */ categorical: readonly string[]; /** Single hue for magnitude — one colour for every bar in a one-series chart. */ sequential: string; /** Recessive chrome. */ grid: string; axis: string; /** Text tokens — labels never wear the data colour. */ textMuted: string; /** Surface, for the 2px gaps and rings that separate marks. */ surface: string; tooltipBg: string; tooltipBorder: string; } const LIGHT: ChartPalette = { categorical: ['#2a78d6', '#eb6834', '#1baf7a', '#eda100', '#e87ba4', '#008300'], sequential: '#2a78d6', grid: '#e1e0d9', axis: '#c3c2b7', textMuted: '#898781', surface: '#ffffff', tooltipBg: '#ffffff', tooltipBorder: 'rgba(11,11,11,0.10)', }; const DARK: ChartPalette = { categorical: ['#3987e5', '#d95926', '#199e70', '#c98500', '#d55181', '#008300'], sequential: '#3987e5', grid: '#2c2c2a', axis: '#383835', textMuted: '#898781', surface: '#0f1729', tooltipBg: '#0f1729', tooltipBorder: 'rgba(255,255,255,0.10)', }; export function getChartPalette(isDark: boolean): ChartPalette { return isDark ? DARK : LIGHT; } /** * Colour for a categorical member, keyed by its position in a **stable** ordering of the * whole domain — never by its rank in the current filtered view, so filtering does not * repaint the survivors. Past the last slot everything folds into one neutral bucket * rather than inventing a hue no CVD check would pass. */ export function categoricalColor(palette: ChartPalette, index: number): string { return palette.categorical[index] ?? palette.textMuted; }