@opendata-ai/openchart-core 8.0.0-rc.9 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +403 -45
  3. package/dist/index.js +50 -18
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.css.map +1 -1
  7. package/package.json +1 -2
  8. package/schema/chart.schema.json +375 -69
  9. package/schema/table.schema.json +35 -1
  10. package/schema/vizspec.schema.json +992 -171
  11. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  12. package/src/accessibility/__tests__/aria.test.ts +0 -125
  13. package/src/accessibility/alt-text.ts +0 -155
  14. package/src/accessibility/aria.ts +0 -75
  15. package/src/accessibility/index.ts +0 -6
  16. package/src/colors/__tests__/colorblind.test.ts +0 -75
  17. package/src/colors/__tests__/contrast.test.ts +0 -119
  18. package/src/colors/__tests__/palettes.test.ts +0 -74
  19. package/src/colors/colorblind.ts +0 -131
  20. package/src/colors/contrast.ts +0 -143
  21. package/src/colors/index.ts +0 -36
  22. package/src/colors/palettes.ts +0 -206
  23. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  24. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  25. package/src/helpers/deep-merge.ts +0 -29
  26. package/src/helpers/spec-builders.ts +0 -479
  27. package/src/index.ts +0 -215
  28. package/src/labels/__tests__/collision.test.ts +0 -310
  29. package/src/labels/collision.ts +0 -249
  30. package/src/labels/index.ts +0 -13
  31. package/src/layout/__tests__/chrome.test.ts +0 -208
  32. package/src/layout/__tests__/text-measure.test.ts +0 -57
  33. package/src/layout/chrome.ts +0 -470
  34. package/src/layout/index.ts +0 -18
  35. package/src/layout/text-measure.ts +0 -404
  36. package/src/layout/text-wrap.ts +0 -93
  37. package/src/locale/__tests__/format.test.ts +0 -606
  38. package/src/locale/format.ts +0 -459
  39. package/src/locale/index.ts +0 -21
  40. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  41. package/src/responsive/__tests__/metrics.test.ts +0 -52
  42. package/src/responsive/breakpoints.ts +0 -177
  43. package/src/responsive/index.ts +0 -53
  44. package/src/responsive/metrics.ts +0 -299
  45. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  46. package/src/schema/__tests__/schema.test.ts +0 -179
  47. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  48. package/src/styles/animation.css +0 -213
  49. package/src/styles/base.css +0 -102
  50. package/src/styles/chrome.css +0 -174
  51. package/src/styles/dark.css +0 -44
  52. package/src/styles/graph.css +0 -135
  53. package/src/styles/index.css +0 -28
  54. package/src/styles/keyframes.css +0 -151
  55. package/src/styles/legend.css +0 -18
  56. package/src/styles/reduced-motion.css +0 -67
  57. package/src/styles/series-search.css +0 -153
  58. package/src/styles/sparkline.css +0 -16
  59. package/src/styles/table-animation.css +0 -93
  60. package/src/styles/table.css +0 -398
  61. package/src/styles/token-definitions.ts +0 -199
  62. package/src/styles/tokens.css +0 -156
  63. package/src/styles/tooltip.css +0 -126
  64. package/src/styles/you-draw-it.css +0 -126
  65. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  66. package/src/theme/__tests__/defaults.test.ts +0 -73
  67. package/src/theme/__tests__/resolve.test.ts +0 -277
  68. package/src/theme/dark-mode.ts +0 -258
  69. package/src/theme/defaults.ts +0 -110
  70. package/src/theme/index.ts +0 -8
  71. package/src/theme/presets.ts +0 -64
  72. package/src/theme/resolve.ts +0 -312
  73. package/src/types/__tests__/encoding.test.ts +0 -368
  74. package/src/types/__tests__/gradient.test.ts +0 -95
  75. package/src/types/__tests__/spec.test.ts +0 -499
  76. package/src/types/encoding.ts +0 -280
  77. package/src/types/events.ts +0 -236
  78. package/src/types/index.ts +0 -290
  79. package/src/types/layout.ts +0 -2024
  80. package/src/types/schema-roots.ts +0 -32
  81. package/src/types/spec.ts +0 -2925
  82. package/src/types/table.ts +0 -108
  83. package/src/types/theme.ts +0 -209
@@ -1,74 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { adaptColorForDarkMode } from '../../theme/dark-mode';
3
- import {
4
- ACHROMATIC_RAMP,
5
- CATEGORICAL_PALETTE,
6
- DIVERGING_PALETTES,
7
- DIVERGING_RED_BLUE,
8
- SEQUENTIAL_BLUE,
9
- SEQUENTIAL_PALETTES,
10
- } from '../palettes';
11
-
12
- describe('palettes', () => {
13
- it('categorical palette has 9 colors (cyan-led OKLCH ramp)', () => {
14
- expect(CATEGORICAL_PALETTE).toHaveLength(9);
15
- });
16
-
17
- it('categorical palette leads with cyan #06b6d4', () => {
18
- expect(CATEGORICAL_PALETTE[0]).toBe('#06b6d4');
19
- });
20
-
21
- it('categorical palette colors are valid hex', () => {
22
- for (const color of CATEGORICAL_PALETTE) {
23
- expect(color).toMatch(/^#[0-9a-f]{6}$/i);
24
- }
25
- });
26
-
27
- it('every categorical color round-trips cleanly through the dark-mode adapter', () => {
28
- // Guard against hex strings that fail to parse (e.g. accidental oklch).
29
- for (const color of CATEGORICAL_PALETTE) {
30
- const adapted = adaptColorForDarkMode(color, '#ffffff', ACHROMATIC_RAMP.bg);
31
- expect(adapted).toMatch(/^#[0-9a-f]{6}$/i);
32
- }
33
- });
34
-
35
- it('exports an achromatic ramp keyed by token name', () => {
36
- expect(ACHROMATIC_RAMP.bg).toBe('#09090b');
37
- expect(ACHROMATIC_RAMP.fg).toBe('#f7f8f8');
38
- expect(ACHROMATIC_RAMP.fgMuted).toBe('#a1a1aa');
39
- });
40
-
41
- it('sequential palettes have expected keys', () => {
42
- expect(Object.keys(SEQUENTIAL_PALETTES)).toEqual(
43
- expect.arrayContaining(['blue', 'green', 'orange', 'purple']),
44
- );
45
- });
46
-
47
- it('sequential palettes have 6 stops each', () => {
48
- for (const [_name, stops] of Object.entries(SEQUENTIAL_PALETTES)) {
49
- expect(stops.length).toBeGreaterThanOrEqual(5);
50
- expect(stops.length).toBeLessThanOrEqual(7);
51
- // Verify all stops are hex
52
- for (const stop of stops) {
53
- expect(stop).toMatch(/^#[0-9a-f]{6}$/i);
54
- }
55
- }
56
- });
57
-
58
- it('diverging palettes have expected keys', () => {
59
- expect(Object.keys(DIVERGING_PALETTES)).toEqual(
60
- expect.arrayContaining(['redBlue', 'brownTeal']),
61
- );
62
- });
63
-
64
- it('diverging palettes have 7 stops (neutral midpoint)', () => {
65
- for (const stops of Object.values(DIVERGING_PALETTES)) {
66
- expect(stops).toHaveLength(7);
67
- }
68
- });
69
-
70
- it('named palette objects have matching names', () => {
71
- expect(SEQUENTIAL_BLUE.name).toBe('blue');
72
- expect(DIVERGING_RED_BLUE.name).toBe('redBlue');
73
- });
74
- });
@@ -1,131 +0,0 @@
1
- /**
2
- * Color blindness simulation and palette distinguishability checks.
3
- *
4
- * Uses Brettel, Vienot, and Mollon (1997) simulation matrices for
5
- * protanopia, deuteranopia, and tritanopia.
6
- *
7
- * These are approximations suitable for checking palette accessibility,
8
- * not medical-grade simulations.
9
- */
10
-
11
- import { rgb } from 'd3-color';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Types
15
- // ---------------------------------------------------------------------------
16
-
17
- /** The four common types of color vision deficiency. */
18
- export type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia' | 'achromatopsia';
19
-
20
- // ---------------------------------------------------------------------------
21
- // Simulation matrices
22
- // ---------------------------------------------------------------------------
23
-
24
- // 3x3 color transformation matrices for each deficiency type.
25
- // Applied in linear RGB space to simulate how colors appear.
26
- // Source: Brettel, Vienot & Mollon (1997), simplified.
27
-
28
- type Matrix3x3 = readonly [
29
- readonly [number, number, number],
30
- readonly [number, number, number],
31
- readonly [number, number, number],
32
- ];
33
-
34
- const PROTAN_MATRIX: Matrix3x3 = [
35
- [0.567, 0.433, 0.0],
36
- [0.558, 0.442, 0.0],
37
- [0.0, 0.242, 0.758],
38
- ];
39
-
40
- const DEUTAN_MATRIX: Matrix3x3 = [
41
- [0.625, 0.375, 0.0],
42
- [0.7, 0.3, 0.0],
43
- [0.0, 0.3, 0.7],
44
- ];
45
-
46
- const TRITAN_MATRIX: Matrix3x3 = [
47
- [0.95, 0.05, 0.0],
48
- [0.0, 0.433, 0.567],
49
- [0.0, 0.475, 0.525],
50
- ];
51
-
52
- // Total color blindness: every channel collapses to Rec. 709 relative
53
- // luminance, computed in linear RGB like the other matrices.
54
- const ACHROMAT_MATRIX: Matrix3x3 = [
55
- [0.2126, 0.7152, 0.0722],
56
- [0.2126, 0.7152, 0.0722],
57
- [0.2126, 0.7152, 0.0722],
58
- ];
59
-
60
- const MATRICES: Record<ColorBlindnessType, Matrix3x3> = {
61
- protanopia: PROTAN_MATRIX,
62
- deuteranopia: DEUTAN_MATRIX,
63
- tritanopia: TRITAN_MATRIX,
64
- achromatopsia: ACHROMAT_MATRIX,
65
- };
66
-
67
- // ---------------------------------------------------------------------------
68
- // sRGB linearization helpers
69
- // ---------------------------------------------------------------------------
70
-
71
- function linearize(v: number): number {
72
- const s = v / 255;
73
- return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
74
- }
75
-
76
- function delinearize(v: number): number {
77
- const s = v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;
78
- return Math.round(Math.max(0, Math.min(255, s * 255)));
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Public API
83
- // ---------------------------------------------------------------------------
84
-
85
- /**
86
- * Simulate how a color appears under a given color blindness type.
87
- * Returns a hex color string.
88
- */
89
- export function simulateColorBlindness(color: string, type: ColorBlindnessType): string {
90
- const c = rgb(color);
91
- if (c == null) return color;
92
-
93
- const lin = [linearize(c.r), linearize(c.g), linearize(c.b)];
94
- const m = MATRICES[type];
95
-
96
- const r = m[0][0] * lin[0] + m[0][1] * lin[1] + m[0][2] * lin[2];
97
- const g = m[1][0] * lin[0] + m[1][1] * lin[1] + m[1][2] * lin[2];
98
- const b = m[2][0] * lin[0] + m[2][1] * lin[1] + m[2][2] * lin[2];
99
-
100
- return rgb(delinearize(r), delinearize(g), delinearize(b)).formatHex();
101
- }
102
-
103
- /**
104
- * Check if colors in a palette are distinguishable under a given
105
- * color blindness type.
106
- *
107
- * Uses a minimum perceptual distance threshold in simulated space.
108
- * Returns true if all pairs of colors are sufficiently different.
109
- */
110
- export function checkPaletteDistinguishability(
111
- colors: string[],
112
- type: ColorBlindnessType,
113
- minDistance = 30,
114
- ): boolean {
115
- const simulated = colors.map((c) => {
116
- const s = rgb(simulateColorBlindness(c, type));
117
- return s ? [s.r, s.g, s.b] : [0, 0, 0];
118
- });
119
-
120
- for (let i = 0; i < simulated.length; i++) {
121
- for (let j = i + 1; j < simulated.length; j++) {
122
- const dr = simulated[i][0] - simulated[j][0];
123
- const dg = simulated[i][1] - simulated[j][1];
124
- const db = simulated[i][2] - simulated[j][2];
125
- const dist = Math.sqrt(dr * dr + dg * dg + db * db);
126
- if (dist < minDistance) return false;
127
- }
128
- }
129
-
130
- return true;
131
- }
@@ -1,143 +0,0 @@
1
- /**
2
- * WCAG contrast ratio utilities.
3
- *
4
- * Uses d3-color for color space parsing and manipulation.
5
- * All functions accept CSS color strings (hex, rgb, hsl, named colors).
6
- */
7
-
8
- import { rgb } from 'd3-color';
9
-
10
- // ---------------------------------------------------------------------------
11
- // Relative luminance (WCAG 2.1)
12
- // ---------------------------------------------------------------------------
13
-
14
- /**
15
- * Compute the relative luminance of a color per WCAG 2.1 definition.
16
- * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
17
- */
18
- export function relativeLuminance(color: string): number {
19
- const c = rgb(color);
20
- if (c == null) return 0;
21
-
22
- const srgb = [c.r / 255, c.g / 255, c.b / 255];
23
- const linear = srgb.map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
24
- return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
- // Public API
29
- // ---------------------------------------------------------------------------
30
-
31
- /**
32
- * Check whether a color string parses to a fully opaque color.
33
- * Contrast ratios are only meaningful against opaque backgrounds; the
34
- * default chart background ('transparent') defers to the host page and
35
- * can't be checked at compile time.
36
- */
37
- export function isOpaqueColor(color: string): boolean {
38
- const c = rgb(color);
39
- return c != null && !Number.isNaN(c.r) && c.opacity === 1;
40
- }
41
-
42
- /**
43
- * Compute the WCAG contrast ratio between two colors.
44
- * Returns a value between 1 (identical) and 21 (black on white).
45
- */
46
- export function contrastRatio(fg: string, bg: string): number {
47
- const l1 = relativeLuminance(fg);
48
- const l2 = relativeLuminance(bg);
49
- const lighter = Math.max(l1, l2);
50
- const darker = Math.min(l1, l2);
51
- return (lighter + 0.05) / (darker + 0.05);
52
- }
53
-
54
- /**
55
- * Pick a legible label color (white or near-black) for text placed on top of `bg`.
56
- *
57
- * Uses a perceptual luminance threshold rather than a strict 4.5:1 contrast
58
- * gate. WCAG's 4.5:1 ratio is calibrated for body text on a page; bold value
59
- * labels sitting on a saturated filled bar read fine at lower ratios. A pure
60
- * contrast-ratio gate sends mid-tone fills (e.g. slate `#94a3b8`, L≈0.36) to
61
- * dark text even though white reads cleaner on them.
62
- *
63
- * The threshold is mode-dependent because of simultaneous contrast: the same
64
- * bar fill looks lighter against a dark canvas than against a white page, so
65
- * dark text reads more grounded on it. Dark mode therefore uses a lower
66
- * threshold (L < 0.30) to pivot mid-tone fills to dark text sooner.
67
- *
68
- * - Light mode (L < 0.42 → white): white on saturated and mid-tone fills;
69
- * dark text only on genuinely light fills (`#b0b0b0` and lighter).
70
- * - Dark mode (L < 0.30 → white): saturated fills keep white; mid-tone fills
71
- * (slate `#94a3b8`, cyan `#06b6d4`, mid-grey) pivot to dark text.
72
- */
73
- export function pickLabelColor(bg: string, darkMode = false): string {
74
- const WHITE = '#ffffff';
75
- const DARK = '#111111';
76
- const threshold = darkMode ? 0.3 : 0.42;
77
- return relativeLuminance(bg) < threshold ? WHITE : DARK;
78
- }
79
-
80
- /**
81
- * Check if two colors meet WCAG AA contrast requirements.
82
- * Normal text: 4.5:1, large text (18px+ bold or 24px+): 3:1.
83
- */
84
- export function meetsAA(fg: string, bg: string, largeText = false): boolean {
85
- const ratio = contrastRatio(fg, bg);
86
- return largeText ? ratio >= 3 : ratio >= 4.5;
87
- }
88
-
89
- /**
90
- * Find an accessible variant of `baseColor` against `bg`.
91
- *
92
- * Preserves the hue and saturation of baseColor but adjusts lightness
93
- * until the target contrast ratio is met. Returns the original color
94
- * if it already meets the target.
95
- */
96
- export function findAccessibleColor(baseColor: string, bg: string, targetRatio = 4.5): string {
97
- if (contrastRatio(baseColor, bg) >= targetRatio) {
98
- return baseColor;
99
- }
100
-
101
- const c = rgb(baseColor);
102
- if (c == null) return baseColor;
103
-
104
- const bgLum = relativeLuminance(bg);
105
- const baseLum = relativeLuminance(baseColor);
106
-
107
- // Try both directions: prefer the one matching bg luminance, but fall back
108
- // to the other if the base color is already at the extreme (e.g. white on
109
- // a medium-luminance background can't be lightened, so darken instead).
110
- const preferDarken = bgLum > 0.5;
111
- const directions = preferDarken ? [true, false] : [false, true];
112
-
113
- for (const darken of directions) {
114
- // Skip impossible directions: can't lighten white or darken black.
115
- if (!darken && baseLum > 0.95) continue;
116
- if (darken && baseLum < 0.05) continue;
117
-
118
- let lo = 0;
119
- let hi = 1;
120
- let best: string | null = null;
121
-
122
- for (let i = 0; i < 20; i++) {
123
- const mid = (lo + hi) / 2;
124
- const adjusted = darken
125
- ? rgb(c.r * (1 - mid), c.g * (1 - mid), c.b * (1 - mid))
126
- : rgb(c.r + (255 - c.r) * mid, c.g + (255 - c.g) * mid, c.b + (255 - c.b) * mid);
127
-
128
- const hex = adjusted.formatHex();
129
- const ratio = contrastRatio(hex, bg);
130
-
131
- if (ratio >= targetRatio) {
132
- best = hex;
133
- hi = mid;
134
- } else {
135
- lo = mid;
136
- }
137
- }
138
-
139
- if (best) return best;
140
- }
141
-
142
- return baseColor;
143
- }
@@ -1,36 +0,0 @@
1
- /**
2
- * Color system barrel export.
3
- */
4
-
5
- export type { ColorBlindnessType } from './colorblind';
6
- export {
7
- checkPaletteDistinguishability,
8
- simulateColorBlindness,
9
- } from './colorblind';
10
-
11
- export {
12
- contrastRatio,
13
- findAccessibleColor,
14
- isOpaqueColor,
15
- meetsAA,
16
- pickLabelColor,
17
- } from './contrast';
18
- export type {
19
- CategoricalPalette,
20
- DivergingPalette,
21
- SequentialPalette,
22
- } from './palettes';
23
- export {
24
- ACHROMATIC_RAMP,
25
- CATEGORICAL_PALETTE,
26
- DIVERGING_BROWN_TEAL,
27
- DIVERGING_PALETTES,
28
- DIVERGING_RED_BLUE,
29
- resolveSchemeName,
30
- SEQUENTIAL_BLUE,
31
- SEQUENTIAL_GREEN,
32
- SEQUENTIAL_ORANGE,
33
- SEQUENTIAL_PALETTES,
34
- SEQUENTIAL_PURPLE,
35
- SUPPORTED_SCHEME_NAMES,
36
- } from './palettes';
@@ -1,206 +0,0 @@
1
- /**
2
- * Color palettes for @opendata-ai.
3
- *
4
- * Categorical palette leads with cyan and uses an OKLCH-balanced multi-hue
5
- * ramp tuned to L≈0.70, C≈0.13–0.15. Hex values are precomputed sRGB
6
- * (rather than raw `oklch()` strings) because raw OKLCH strings parse
7
- * unreliably through d3-color and the dark-mode adapter's hsl-based
8
- * binary search. Source OKLCH values are documented inline so the ramp
9
- * can be regenerated if needed.
10
- *
11
- * Convention for low-cardinality charts (1 / 2-4 / 5+ series) is available
12
- * as an opt-in via `theme.seriesStrategy`. The default strategy is 'palette'
13
- * (full categorical ramp always). The accent-neutral convention is:
14
- * - 1 series: cyan only
15
- * - 2-4 series: cyan + zinc-400/500/600 (achromatic)
16
- * - 5+ series: full multi-hue ramp below
17
- *
18
- * Sequential palettes: 5-7 stops from light to dark.
19
- * Diverging palettes: 7 stops with a neutral midpoint.
20
- */
21
-
22
- // ---------------------------------------------------------------------------
23
- // Achromatic ramp (zinc-based, dark-mode canonical)
24
- // ---------------------------------------------------------------------------
25
-
26
- /**
27
- * Achromatic surface and supporting-series ramp. Values are the canonical
28
- * dark-mode tokens; light-mode equivalents are derived per token in
29
- * `theme/dark-mode.ts` and the CSS partials, not mirrored here.
30
- */
31
- export const ACHROMATIC_RAMP = {
32
- fg: '#f7f8f8', // primary text
33
- fg2: '#d0d6e0', // body text
34
- fgMuted: '#a1a1aa', // secondary series (zinc-400)
35
- fgSubtle: '#71717a', // tertiary series (zinc-500)
36
- fgFaint: '#52525b', // quaternary series (zinc-600)
37
- secondary: '#27272a', // hover / raised surface
38
- card: '#111113', // card surface
39
- bg: '#09090b', // canvas
40
- } as const;
41
-
42
- // ---------------------------------------------------------------------------
43
- // Categorical
44
- // ---------------------------------------------------------------------------
45
-
46
- /**
47
- * Default categorical palette. Cyan-led OKLCH-balanced multi-hue ramp.
48
- *
49
- * Hex values precomputed from OKLCH via the standard OKLab -> linear sRGB
50
- * -> sRGB pipeline. Documented OKLCH source values are the contract; if
51
- * the conversion math changes, regenerate from the source rather than
52
- * editing hex literals directly.
53
- *
54
- * Tuned at L≈0.65, C≈0.20 (vs prior L≈0.70, C≈0.15) for more vivid,
55
- * saturated color on dark backgrounds where the lighter pastels read soft.
56
- */
57
- // Order interleaves hues so adjacent slots sit at least ~70° apart on the
58
- // OKLCH wheel. Cyan and teal differ by only ~10° and read as the same color
59
- // when placed side-by-side (e.g. on a pie slice or stacked area), so the
60
- // teal slot is pushed deep into the ramp where it won't neighbor cyan.
61
- export const CATEGORICAL_PALETTE = [
62
- '#06b6d4', // cyan, primary accent (sRGB literal, ~205°)
63
- '#ee4a73', // rose — oklch(65% 0.20 10)
64
- '#00b054', // emerald — oklch(65% 0.20 155)
65
- '#a46bf5', // violet — oklch(65% 0.20 300)
66
- '#e07d00', // amber — oklch(68% 0.19 70)
67
- '#0091ff', // sky — oklch(65% 0.20 250)
68
- '#f36000', // orange — oklch(67% 0.20 45)
69
- '#6f7dff', // indigo — oklch(65% 0.20 275)
70
- '#00afbf', // teal — oklch(65% 0.20 200)
71
- ] as const;
72
-
73
- export type CategoricalPalette = typeof CATEGORICAL_PALETTE;
74
-
75
- // ---------------------------------------------------------------------------
76
- // Sequential
77
- // ---------------------------------------------------------------------------
78
-
79
- /** Sequential palette definition: an array of color stops from light to dark. */
80
- export interface SequentialPalette {
81
- readonly name: string;
82
- readonly stops: readonly string[];
83
- }
84
-
85
- export const SEQUENTIAL_BLUE: SequentialPalette = {
86
- name: 'blue',
87
- stops: ['#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c'],
88
- } as const;
89
-
90
- export const SEQUENTIAL_GREEN: SequentialPalette = {
91
- name: 'green',
92
- stops: ['#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c'],
93
- } as const;
94
-
95
- export const SEQUENTIAL_ORANGE: SequentialPalette = {
96
- name: 'orange',
97
- stops: ['#fee6ce', '#fdd0a2', '#fdae6b', '#fd8d3c', '#e6550d', '#a63603'],
98
- } as const;
99
-
100
- export const SEQUENTIAL_PURPLE: SequentialPalette = {
101
- name: 'purple',
102
- stops: ['#efedf5', '#dadaeb', '#bcbddc', '#9e9ac8', '#756bb1', '#54278f'],
103
- } as const;
104
-
105
- export const SEQUENTIAL_TEAL: SequentialPalette = {
106
- name: 'teal',
107
- stops: ['#06b6d4', '#05a3be', '#0490a8', '#037d92', '#026a7c', '#015766', '#004450'],
108
- } as const;
109
-
110
- /** All sequential palettes keyed by name. */
111
- export const SEQUENTIAL_PALETTES: Record<string, string[]> = {
112
- blue: [...SEQUENTIAL_BLUE.stops],
113
- green: [...SEQUENTIAL_GREEN.stops],
114
- orange: [...SEQUENTIAL_ORANGE.stops],
115
- purple: [...SEQUENTIAL_PURPLE.stops],
116
- teal: [...SEQUENTIAL_TEAL.stops],
117
- };
118
-
119
- // ---------------------------------------------------------------------------
120
- // Diverging
121
- // ---------------------------------------------------------------------------
122
-
123
- /** Diverging palette definition: an array of color stops with a neutral midpoint. */
124
- export interface DivergingPalette {
125
- readonly name: string;
126
- readonly stops: readonly string[];
127
- }
128
-
129
- export const DIVERGING_RED_BLUE: DivergingPalette = {
130
- name: 'redBlue',
131
- stops: [
132
- '#b2182b', // strong red
133
- '#d6604d', // medium red
134
- '#f4a582', // light red
135
- '#f7f7f7', // neutral
136
- '#92c5de', // light blue
137
- '#4393c3', // medium blue
138
- '#2166ac', // strong blue
139
- ],
140
- } as const;
141
-
142
- export const DIVERGING_BROWN_TEAL: DivergingPalette = {
143
- name: 'brownTeal',
144
- stops: [
145
- '#8c510a', // strong brown
146
- '#bf812d', // medium brown
147
- '#dfc27d', // light brown
148
- '#f6e8c3', // neutral
149
- '#80cdc1', // light teal
150
- '#35978f', // medium teal
151
- '#01665e', // strong teal
152
- ],
153
- } as const;
154
-
155
- /** All diverging palettes keyed by name. */
156
- export const DIVERGING_PALETTES: Record<string, string[]> = {
157
- redBlue: [...DIVERGING_RED_BLUE.stops],
158
- brownTeal: [...DIVERGING_BROWN_TEAL.stops],
159
- };
160
-
161
- // ---------------------------------------------------------------------------
162
- // Scheme name resolution (spec-level `scale: { scheme }` support)
163
- // ---------------------------------------------------------------------------
164
-
165
- /**
166
- * Vega-Lite scheme-name aliases mapped onto the openchart palettes.
167
- * Keys are lowercase. Only schemes with a faithful local equivalent are
168
- * aliased; unknown names fail validation with the supported list rather
169
- * than silently substituting an unrelated ramp.
170
- */
171
- const VL_SCHEME_ALIASES: Record<string, string> = {
172
- blues: 'blue',
173
- greens: 'green',
174
- oranges: 'orange',
175
- purples: 'purple',
176
- teals: 'teal',
177
- redblue: 'redBlue',
178
- brownteal: 'brownTeal',
179
- // ColorBrewer BrBG, which brownTeal mirrors
180
- brownbluegreen: 'brownTeal',
181
- category10: 'categorical',
182
- tableau10: 'categorical',
183
- };
184
-
185
- /**
186
- * Resolve a scheme name to its palette stops. Accepts openchart palette names
187
- * (blue, green, orange, purple, teal, redBlue, brownTeal, categorical) and
188
- * common Vega-Lite aliases (blues, greens, ..., redblue, category10).
189
- * Case-insensitive. Returns undefined for unknown names.
190
- */
191
- export function resolveSchemeName(name: string): string[] | undefined {
192
- const canonical =
193
- SEQUENTIAL_PALETTES[name] || DIVERGING_PALETTES[name]
194
- ? name
195
- : (VL_SCHEME_ALIASES[name.toLowerCase()] ?? name.toLowerCase());
196
- if (canonical === 'categorical') return [...CATEGORICAL_PALETTE];
197
- return SEQUENTIAL_PALETTES[canonical] ?? DIVERGING_PALETTES[canonical];
198
- }
199
-
200
- /** Scheme names accepted by `scale: { scheme }`, for validation messages. */
201
- export const SUPPORTED_SCHEME_NAMES: readonly string[] = [
202
- ...Object.keys(SEQUENTIAL_PALETTES),
203
- ...Object.keys(DIVERGING_PALETTES),
204
- 'categorical',
205
- ...Object.keys(VL_SCHEME_ALIASES),
206
- ];
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { deepMergeSpec } from '../deep-merge';
3
-
4
- describe('deepMergeSpec', () => {
5
- it('merges nested plain objects key by key', () => {
6
- const base = { encoding: { x: { field: 'a' }, y: { field: 'b' } }, chrome: { title: 'X' } };
7
- const patch = { encoding: { y: { field: 'c' } } };
8
- expect(deepMergeSpec(base, patch)).toEqual({
9
- encoding: { x: { field: 'a' }, y: { field: 'c' } },
10
- chrome: { title: 'X' },
11
- });
12
- });
13
-
14
- it('replaces arrays wholesale rather than merging by index', () => {
15
- const base = { annotations: [{ type: 'text', text: 'old' }] };
16
- const patch = { annotations: [{ type: 'refline', y: 5 }] };
17
- expect(deepMergeSpec(base, patch)).toEqual({
18
- annotations: [{ type: 'refline', y: 5 }],
19
- });
20
- });
21
-
22
- it('leaves target untouched when patch value is undefined', () => {
23
- const base = { chrome: { title: 'X' } };
24
- expect(deepMergeSpec(base, { chrome: { title: undefined } })).toEqual({
25
- chrome: { title: 'X' },
26
- });
27
- });
28
-
29
- it('overwrites with null explicitly', () => {
30
- const base = { encoding: { color: { field: 'a' } } };
31
- expect(deepMergeSpec(base, { encoding: { color: null } })).toEqual({
32
- encoding: { color: null },
33
- });
34
- });
35
-
36
- it('does not mutate the source objects', () => {
37
- const base = { encoding: { x: { field: 'a' } } };
38
- const patch = { encoding: { x: { field: 'b' } } };
39
- const result = deepMergeSpec(base, patch);
40
- expect(result).not.toBe(base);
41
- expect(base.encoding.x.field).toBe('a');
42
- });
43
-
44
- it('cumulative patches compose left to right', () => {
45
- let spec: Record<string, unknown> = { mark: 'line', encoding: { y: { field: 'a' } } };
46
- const patches = [{ chrome: { title: 'Step 1' } }, { encoding: { y: { field: 'b' } } }];
47
- for (const p of patches) spec = deepMergeSpec(spec, p);
48
- expect(spec).toEqual({
49
- mark: 'line',
50
- encoding: { y: { field: 'b' } },
51
- chrome: { title: 'Step 1' },
52
- });
53
- });
54
- });