@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.2
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/dist/index.d.ts +1154 -119
- package/dist/index.js +533 -42
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.css.map +1 -0
- package/package.json +9 -3
- package/schema/chart.schema.json +8957 -0
- package/schema/table.schema.json +861 -0
- package/schema/vizspec.schema.json +17694 -0
- package/src/accessibility/alt-text.ts +39 -1
- package/src/accessibility/aria.ts +4 -2
- package/src/colors/__tests__/colorblind.test.ts +12 -0
- package/src/colors/colorblind.ts +11 -2
- package/src/colors/contrast.ts +11 -0
- package/src/colors/index.ts +9 -1
- package/src/colors/palettes.ts +47 -0
- package/src/helpers/__tests__/deep-merge.test.ts +54 -0
- package/src/helpers/__tests__/spec-builders.test.ts +3 -3
- package/src/helpers/deep-merge.ts +29 -0
- package/src/helpers/spec-builders.ts +7 -7
- package/src/index.ts +29 -1
- package/src/labels/collision.ts +1 -0
- package/src/layout/chrome.ts +24 -0
- package/src/layout/index.ts +1 -1
- package/src/locale/__tests__/format.test.ts +347 -11
- package/src/locale/format.ts +243 -18
- package/src/locale/index.ts +10 -1
- package/src/responsive/index.ts +2 -0
- package/src/responsive/metrics.ts +77 -10
- package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
- package/src/schema/__tests__/schema.test.ts +179 -0
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
- package/src/styles/animation.css +197 -168
- package/src/styles/base.css +89 -62
- package/src/styles/chrome.css +173 -179
- package/src/styles/dark.css +38 -35
- package/src/styles/graph.css +116 -111
- package/src/styles/index.css +5 -0
- package/src/styles/keyframes.css +115 -100
- package/src/styles/legend.css +14 -12
- package/src/styles/reduced-motion.css +61 -40
- package/src/styles/series-search.css +153 -0
- package/src/styles/sparkline.css +14 -12
- package/src/styles/table-animation.css +82 -80
- package/src/styles/table.css +314 -302
- package/src/styles/token-definitions.ts +199 -0
- package/src/styles/tokens.css +132 -128
- package/src/styles/tooltip.css +109 -115
- package/src/styles/you-draw-it.css +126 -0
- package/src/types/__tests__/encoding.test.ts +86 -17
- package/src/types/__tests__/spec.test.ts +16 -12
- package/src/types/encoding.ts +69 -25
- package/src/types/events.ts +12 -0
- package/src/types/index.ts +39 -3
- package/src/types/layout.ts +414 -8
- package/src/types/schema-roots.ts +32 -0
- package/src/types/spec.ts +643 -84
- package/src/types/table.ts +3 -2
|
@@ -40,8 +40,46 @@ export function generateAltText(spec: ChartSpec, data: DataRow[]): string {
|
|
|
40
40
|
parts.push(`showing ${titleText}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Waffle: name the unit framing. Per-category "x of N units" phrasing
|
|
44
|
+
// lives on the mark aria labels; the chart-level text carries the total.
|
|
45
|
+
if (markType === 'waffle') {
|
|
46
|
+
const units = markDef.units ?? 100;
|
|
47
|
+
parts.push(`dividing ${units} units`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parliament: name the total seat count and the majority threshold, the two
|
|
51
|
+
// numbers a reader most wants. Per-party seat counts live on the seat marks'
|
|
52
|
+
// aria labels; this carries the overall framing.
|
|
53
|
+
if (markType === 'parliament') {
|
|
54
|
+
const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;
|
|
55
|
+
if (valueField) {
|
|
56
|
+
const totalSeats = data.reduce((sum, d) => {
|
|
57
|
+
const v = Number(d[valueField]);
|
|
58
|
+
return sum + (Number.isFinite(v) && v > 0 ? v : 0);
|
|
59
|
+
}, 0);
|
|
60
|
+
if (totalSeats > 0) {
|
|
61
|
+
const majority = Math.floor(totalSeats / 2) + 1;
|
|
62
|
+
parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Beeswarm: name the distribution fields (value axis + optional lane).
|
|
68
|
+
// Skips the generic x-axis description below, which would either say
|
|
69
|
+
// nothing (quantitative value axis) or repeat the lane count.
|
|
70
|
+
if (markType === 'beeswarm') {
|
|
71
|
+
const xIsValue = spec.encoding.x?.type === 'quantitative';
|
|
72
|
+
const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;
|
|
73
|
+
const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;
|
|
74
|
+
if (valueField) {
|
|
75
|
+
parts.push(
|
|
76
|
+
`plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ''}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
43
81
|
// Describe the data range
|
|
44
|
-
if (spec.encoding.x && data.length > 0) {
|
|
82
|
+
if (spec.encoding.x && markType !== 'beeswarm' && data.length > 0) {
|
|
45
83
|
const field = spec.encoding.x.field;
|
|
46
84
|
const values = data.map((d) => d[field]).filter((v) => v != null);
|
|
47
85
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* users to explore data points individually.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { defaultNumberFormatter } from '../locale/format';
|
|
8
9
|
import type { Mark } from '../types/layout';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -62,12 +63,13 @@ export function generateAriaLabels(marks: Mark[]): Map<string, string> {
|
|
|
62
63
|
return labels;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
const ariaNumberFormatter = defaultNumberFormatter();
|
|
67
|
+
|
|
66
68
|
function formatValue(value: unknown): string {
|
|
67
69
|
if (value == null) return 'N/A';
|
|
68
70
|
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
69
71
|
if (typeof value === 'number') {
|
|
70
|
-
return
|
|
72
|
+
return ariaNumberFormatter(value);
|
|
71
73
|
}
|
|
72
74
|
return String(value);
|
|
73
75
|
}
|
|
@@ -42,6 +42,18 @@ describe('simulateColorBlindness', () => {
|
|
|
42
42
|
expect(simRed).toMatch(/^#[0-9a-f]{6}$/i);
|
|
43
43
|
expect(simGreen).toMatch(/^#[0-9a-f]{6}$/i);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('achromatopsia collapses every color to a grey', () => {
|
|
47
|
+
for (const color of ['#e15759', '#59a14f', '#1b7fa3']) {
|
|
48
|
+
const sim = simulateColorBlindness(color, 'achromatopsia');
|
|
49
|
+
const match = sim.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
50
|
+
expect(match).not.toBeNull();
|
|
51
|
+
if (match) {
|
|
52
|
+
const [r, g, b] = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
|
|
53
|
+
expect(Math.max(r, g, b) - Math.min(r, g, b)).toBeLessThanOrEqual(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
45
57
|
});
|
|
46
58
|
|
|
47
59
|
describe('checkPaletteDistinguishability', () => {
|
package/src/colors/colorblind.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { rgb } from 'd3-color';
|
|
|
14
14
|
// Types
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
|
-
/** The
|
|
18
|
-
export type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia';
|
|
17
|
+
/** The four common types of color vision deficiency. */
|
|
18
|
+
export type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia' | 'achromatopsia';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Simulation matrices
|
|
@@ -49,10 +49,19 @@ const TRITAN_MATRIX: Matrix3x3 = [
|
|
|
49
49
|
[0.0, 0.475, 0.525],
|
|
50
50
|
];
|
|
51
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
|
+
|
|
52
60
|
const MATRICES: Record<ColorBlindnessType, Matrix3x3> = {
|
|
53
61
|
protanopia: PROTAN_MATRIX,
|
|
54
62
|
deuteranopia: DEUTAN_MATRIX,
|
|
55
63
|
tritanopia: TRITAN_MATRIX,
|
|
64
|
+
achromatopsia: ACHROMAT_MATRIX,
|
|
56
65
|
};
|
|
57
66
|
|
|
58
67
|
// ---------------------------------------------------------------------------
|
package/src/colors/contrast.ts
CHANGED
|
@@ -28,6 +28,17 @@ export function relativeLuminance(color: string): number {
|
|
|
28
28
|
// Public API
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
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
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Compute the WCAG contrast ratio between two colors.
|
|
33
44
|
* Returns a value between 1 (identical) and 21 (black on white).
|
package/src/colors/index.ts
CHANGED
|
@@ -8,7 +8,13 @@ export {
|
|
|
8
8
|
simulateColorBlindness,
|
|
9
9
|
} from './colorblind';
|
|
10
10
|
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
contrastRatio,
|
|
13
|
+
findAccessibleColor,
|
|
14
|
+
isOpaqueColor,
|
|
15
|
+
meetsAA,
|
|
16
|
+
pickLabelColor,
|
|
17
|
+
} from './contrast';
|
|
12
18
|
export type {
|
|
13
19
|
CategoricalPalette,
|
|
14
20
|
DivergingPalette,
|
|
@@ -20,9 +26,11 @@ export {
|
|
|
20
26
|
DIVERGING_BROWN_TEAL,
|
|
21
27
|
DIVERGING_PALETTES,
|
|
22
28
|
DIVERGING_RED_BLUE,
|
|
29
|
+
resolveSchemeName,
|
|
23
30
|
SEQUENTIAL_BLUE,
|
|
24
31
|
SEQUENTIAL_GREEN,
|
|
25
32
|
SEQUENTIAL_ORANGE,
|
|
26
33
|
SEQUENTIAL_PALETTES,
|
|
27
34
|
SEQUENTIAL_PURPLE,
|
|
35
|
+
SUPPORTED_SCHEME_NAMES,
|
|
28
36
|
} from './palettes';
|
package/src/colors/palettes.ts
CHANGED
|
@@ -157,3 +157,50 @@ export const DIVERGING_PALETTES: Record<string, string[]> = {
|
|
|
157
157
|
redBlue: [...DIVERGING_RED_BLUE.stops],
|
|
158
158
|
brownTeal: [...DIVERGING_BROWN_TEAL.stops],
|
|
159
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
|
+
];
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
});
|
|
@@ -203,12 +203,12 @@ describe('columnChart', () => {
|
|
|
203
203
|
// ---------------------------------------------------------------------------
|
|
204
204
|
|
|
205
205
|
describe('pieChart', () => {
|
|
206
|
-
it('maps category to color channel and value to
|
|
206
|
+
it('maps category to color channel and value to theta', () => {
|
|
207
207
|
const spec = pieChart(categoricalData, 'name', 'count');
|
|
208
208
|
|
|
209
209
|
expect(spec.mark).toBe('arc');
|
|
210
|
-
// Arc (pie) convention: value on
|
|
211
|
-
expect(spec.encoding.
|
|
210
|
+
// Arc (pie) convention: value on theta (canonical), category on color, no x
|
|
211
|
+
expect(spec.encoding.theta).toEqual({ field: 'count', type: 'quantitative' });
|
|
212
212
|
expect(spec.encoding.color).toEqual({ field: 'name', type: 'nominal' });
|
|
213
213
|
expect(spec.encoding.x).toBeUndefined();
|
|
214
214
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep merge for applying partial patches onto a spec-shaped object.
|
|
3
|
+
*
|
|
4
|
+
* Used by the scrollytelling story layer (`vanilla/story`) to apply
|
|
5
|
+
* cumulative deep-partial spec patches onto a base spec. Arrays REPLACE
|
|
6
|
+
* rather than merge element-by-element: predictable behavior for things
|
|
7
|
+
* like `annotations` or `data`, where index-wise merging would silently
|
|
8
|
+
* produce nonsense. Plain objects merge key-by-key, recursively.
|
|
9
|
+
*/
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing
|
|
11
|
+
export function deepMergeSpec(target: any, source: any): any {
|
|
12
|
+
if (source === undefined) return target;
|
|
13
|
+
if (source === null) return source;
|
|
14
|
+
if (
|
|
15
|
+
typeof source !== 'object' ||
|
|
16
|
+
Array.isArray(source) ||
|
|
17
|
+
typeof target !== 'object' ||
|
|
18
|
+
target === null ||
|
|
19
|
+
Array.isArray(target)
|
|
20
|
+
) {
|
|
21
|
+
return source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result = { ...target };
|
|
25
|
+
for (const key of Object.keys(source)) {
|
|
26
|
+
result[key] = deepMergeSpec(target[key], source[key]);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
@@ -250,12 +250,12 @@ export function columnChart(
|
|
|
250
250
|
/**
|
|
251
251
|
* Create a pie chart spec.
|
|
252
252
|
*
|
|
253
|
-
* Convention: category maps to the color channel, value maps to
|
|
253
|
+
* Convention: category maps to the color channel, value maps to theta.
|
|
254
254
|
* Pie charts have no x-axis.
|
|
255
255
|
*
|
|
256
256
|
* @param data - Array of data rows.
|
|
257
257
|
* @param category - Category field (color channel, nominal).
|
|
258
|
-
* @param value - Value field (
|
|
258
|
+
* @param value - Value field (theta channel, quantitative).
|
|
259
259
|
* @param options - Optional chrome, annotations, theme, etc.
|
|
260
260
|
* Note: color option is ignored since category is used for color.
|
|
261
261
|
*/
|
|
@@ -268,9 +268,9 @@ export function pieChart(
|
|
|
268
268
|
const categoryChannel = resolveField(category, data);
|
|
269
269
|
const valueChannel = resolveField(value, data);
|
|
270
270
|
|
|
271
|
-
// Pie charts: value on
|
|
271
|
+
// Pie charts: value on theta (canonical in v8), category on color (no x-axis)
|
|
272
272
|
const encoding: Encoding = {
|
|
273
|
-
|
|
273
|
+
theta: valueChannel,
|
|
274
274
|
color: categoryChannel,
|
|
275
275
|
};
|
|
276
276
|
if (options?.size && data) {
|
|
@@ -303,12 +303,12 @@ export function areaChart(
|
|
|
303
303
|
/**
|
|
304
304
|
* Create a donut chart spec.
|
|
305
305
|
*
|
|
306
|
-
* Convention: category maps to the color channel, value maps to
|
|
306
|
+
* Convention: category maps to the color channel, value maps to theta.
|
|
307
307
|
* Donut charts have no x-axis.
|
|
308
308
|
*
|
|
309
309
|
* @param data - Array of data rows.
|
|
310
310
|
* @param category - Category field (color channel, nominal).
|
|
311
|
-
* @param value - Value field (
|
|
311
|
+
* @param value - Value field (theta channel, quantitative).
|
|
312
312
|
* @param options - Optional chrome, annotations, theme, etc.
|
|
313
313
|
* Note: color option is ignored since category is used for color.
|
|
314
314
|
*/
|
|
@@ -322,7 +322,7 @@ export function donutChart(
|
|
|
322
322
|
const valueChannel = resolveField(value, data);
|
|
323
323
|
|
|
324
324
|
const encoding: Encoding = {
|
|
325
|
-
|
|
325
|
+
theta: valueChannel,
|
|
326
326
|
color: categoryChannel,
|
|
327
327
|
};
|
|
328
328
|
if (options?.size && data) {
|
package/src/index.ts
CHANGED
|
@@ -33,9 +33,12 @@ export {
|
|
|
33
33
|
contrastRatio,
|
|
34
34
|
DIVERGING_PALETTES,
|
|
35
35
|
findAccessibleColor,
|
|
36
|
+
isOpaqueColor,
|
|
36
37
|
meetsAA,
|
|
37
38
|
pickLabelColor,
|
|
39
|
+
resolveSchemeName,
|
|
38
40
|
SEQUENTIAL_PALETTES,
|
|
41
|
+
SUPPORTED_SCHEME_NAMES,
|
|
39
42
|
simulateColorBlindness,
|
|
40
43
|
} from './colors/index';
|
|
41
44
|
|
|
@@ -65,6 +68,8 @@ export {
|
|
|
65
68
|
COMPACT_WIDTH,
|
|
66
69
|
computeChrome,
|
|
67
70
|
estimateTextWidth,
|
|
71
|
+
FOOTNOTE_LINE_HEIGHT,
|
|
72
|
+
footnoteBandHeight,
|
|
68
73
|
heuristicMeasure,
|
|
69
74
|
resolveMeasurer,
|
|
70
75
|
textAscent,
|
|
@@ -110,11 +115,13 @@ export {
|
|
|
110
115
|
MAX_LEFT_LABEL_FRACTION_DEFAULT,
|
|
111
116
|
MAX_LEFT_LABEL_FRACTION_MEDIUM,
|
|
112
117
|
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
|
|
118
|
+
maxRotatedLabelWidth,
|
|
113
119
|
NARROW_VIEWPORT_MAX,
|
|
114
120
|
TICK_LABEL_OFFSET,
|
|
115
121
|
TICK_LINE_HEIGHT_FACTOR,
|
|
116
122
|
TOP_PAD_EXTRA_NARROW,
|
|
117
123
|
TOP_PAD_NARROW_MAX,
|
|
124
|
+
truncateRotatedLabel,
|
|
118
125
|
X_AXIS_BAND_HEIGHT,
|
|
119
126
|
X_AXIS_ROTATED_EXTENT_CAP,
|
|
120
127
|
X_AXIS_TITLE_BAND,
|
|
@@ -143,13 +150,21 @@ export {
|
|
|
143
150
|
// Locale: number and date formatting
|
|
144
151
|
// ---------------------------------------------------------------------------
|
|
145
152
|
|
|
146
|
-
export type { DateGranularity } from './locale/index';
|
|
153
|
+
export type { DateGranularity, FieldFormatContext, NumberFormatter } from './locale/index';
|
|
147
154
|
export {
|
|
148
155
|
abbreviateNumber,
|
|
156
|
+
buildCompactStepFormatter,
|
|
149
157
|
buildD3Formatter,
|
|
150
158
|
buildTemporalFormatter,
|
|
159
|
+
computeFieldFormatContext,
|
|
160
|
+
defaultNumberFormatter,
|
|
161
|
+
formatCurrency,
|
|
151
162
|
formatDate,
|
|
152
163
|
formatNumber,
|
|
164
|
+
formatOrdinal,
|
|
165
|
+
formatPercent,
|
|
166
|
+
isYearContext,
|
|
167
|
+
resolveNumberFormatter,
|
|
153
168
|
} from './locale/index';
|
|
154
169
|
|
|
155
170
|
// ---------------------------------------------------------------------------
|
|
@@ -185,3 +200,16 @@ export {
|
|
|
185
200
|
scatterChart,
|
|
186
201
|
tileMap,
|
|
187
202
|
} from './helpers/spec-builders';
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Helpers: deep-merge for spec patches (used by vanilla/story)
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
export { deepMergeSpec } from './helpers/deep-merge';
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// CSS token definitions (single source of truth for tokens.css / dark.css)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
export type { CssTokenDef } from './styles/token-definitions';
|
|
215
|
+
export { CSS_TOKEN_ROOT_SELECTORS, CSS_TOKENS, cssTokenDefault } from './styles/token-definitions';
|
package/src/labels/collision.ts
CHANGED
package/src/layout/chrome.ts
CHANGED
|
@@ -156,6 +156,30 @@ function estimateLineCount(
|
|
|
156
156
|
// Public API
|
|
157
157
|
// ---------------------------------------------------------------------------
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Line-height multiplier for the auto-thinning footnote list.
|
|
161
|
+
*
|
|
162
|
+
* Exported because the chrome renderer steps each footnote line down by it
|
|
163
|
+
* while the band height below is computed from it -- the two have to agree, or
|
|
164
|
+
* the last line drifts outside the band that was reserved for it.
|
|
165
|
+
*/
|
|
166
|
+
export const FOOTNOTE_LINE_HEIGHT = 1.3;
|
|
167
|
+
/** Gap below the footnote list, separating it from the source row. */
|
|
168
|
+
const FOOTNOTE_BAND_GAP = 4;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Vertical space a footnote list of `count` lines occupies below the plot.
|
|
172
|
+
*
|
|
173
|
+
* Single source of truth, deliberately: the engine reserves this band in the
|
|
174
|
+
* bottom margin, the chrome renderer shifts the source row down by it, and the
|
|
175
|
+
* brand watermark shifts by it too. When those three disagree the footnotes
|
|
176
|
+
* land on top of the watermark, which is exactly the bug this centralizes away.
|
|
177
|
+
*/
|
|
178
|
+
export function footnoteBandHeight(count: number, theme: ResolvedTheme): number {
|
|
179
|
+
if (count <= 0) return 0;
|
|
180
|
+
return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
/**
|
|
160
184
|
* Compute resolved chrome layout from a Chrome spec and resolved theme.
|
|
161
185
|
*
|