@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.
- package/README.md +1 -1
- package/dist/index.d.ts +403 -45
- package/dist/index.js +50 -18
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -2
- package/schema/chart.schema.json +375 -69
- package/schema/table.schema.json +35 -1
- package/schema/vizspec.schema.json +992 -171
- package/src/accessibility/__tests__/alt-text.test.ts +0 -110
- package/src/accessibility/__tests__/aria.test.ts +0 -125
- package/src/accessibility/alt-text.ts +0 -155
- package/src/accessibility/aria.ts +0 -75
- package/src/accessibility/index.ts +0 -6
- package/src/colors/__tests__/colorblind.test.ts +0 -75
- package/src/colors/__tests__/contrast.test.ts +0 -119
- package/src/colors/__tests__/palettes.test.ts +0 -74
- package/src/colors/colorblind.ts +0 -131
- package/src/colors/contrast.ts +0 -143
- package/src/colors/index.ts +0 -36
- package/src/colors/palettes.ts +0 -206
- package/src/helpers/__tests__/deep-merge.test.ts +0 -54
- package/src/helpers/__tests__/spec-builders.test.ts +0 -338
- package/src/helpers/deep-merge.ts +0 -29
- package/src/helpers/spec-builders.ts +0 -479
- package/src/index.ts +0 -215
- package/src/labels/__tests__/collision.test.ts +0 -310
- package/src/labels/collision.ts +0 -249
- package/src/labels/index.ts +0 -13
- package/src/layout/__tests__/chrome.test.ts +0 -208
- package/src/layout/__tests__/text-measure.test.ts +0 -57
- package/src/layout/chrome.ts +0 -470
- package/src/layout/index.ts +0 -18
- package/src/layout/text-measure.ts +0 -404
- package/src/layout/text-wrap.ts +0 -93
- package/src/locale/__tests__/format.test.ts +0 -606
- package/src/locale/format.ts +0 -459
- package/src/locale/index.ts +0 -21
- package/src/responsive/__tests__/breakpoints.test.ts +0 -146
- package/src/responsive/__tests__/metrics.test.ts +0 -52
- package/src/responsive/breakpoints.ts +0 -177
- package/src/responsive/index.ts +0 -53
- package/src/responsive/metrics.ts +0 -299
- package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
- package/src/schema/__tests__/schema.test.ts +0 -179
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
- package/src/styles/animation.css +0 -213
- package/src/styles/base.css +0 -102
- package/src/styles/chrome.css +0 -174
- package/src/styles/dark.css +0 -44
- package/src/styles/graph.css +0 -135
- package/src/styles/index.css +0 -28
- package/src/styles/keyframes.css +0 -151
- package/src/styles/legend.css +0 -18
- package/src/styles/reduced-motion.css +0 -67
- package/src/styles/series-search.css +0 -153
- package/src/styles/sparkline.css +0 -16
- package/src/styles/table-animation.css +0 -93
- package/src/styles/table.css +0 -398
- package/src/styles/token-definitions.ts +0 -199
- package/src/styles/tokens.css +0 -156
- package/src/styles/tooltip.css +0 -126
- package/src/styles/you-draw-it.css +0 -126
- package/src/theme/__tests__/dark-mode.test.ts +0 -232
- package/src/theme/__tests__/defaults.test.ts +0 -73
- package/src/theme/__tests__/resolve.test.ts +0 -277
- package/src/theme/dark-mode.ts +0 -258
- package/src/theme/defaults.ts +0 -110
- package/src/theme/index.ts +0 -8
- package/src/theme/presets.ts +0 -64
- package/src/theme/resolve.ts +0 -312
- package/src/types/__tests__/encoding.test.ts +0 -368
- package/src/types/__tests__/gradient.test.ts +0 -95
- package/src/types/__tests__/spec.test.ts +0 -499
- package/src/types/encoding.ts +0 -280
- package/src/types/events.ts +0 -236
- package/src/types/index.ts +0 -290
- package/src/types/layout.ts +0 -2024
- package/src/types/schema-roots.ts +0 -32
- package/src/types/spec.ts +0 -2925
- package/src/types/table.ts +0 -108
- 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
|
-
});
|
package/src/colors/colorblind.ts
DELETED
|
@@ -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
|
-
}
|
package/src/colors/contrast.ts
DELETED
|
@@ -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
|
-
}
|
package/src/colors/index.ts
DELETED
|
@@ -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';
|
package/src/colors/palettes.ts
DELETED
|
@@ -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
|
-
});
|