@opendata-ai/openchart-core 7.6.1 → 7.8.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/dist/index.d.ts +468 -179
- package/dist/index.js +155 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/colors/palettes.ts +3 -3
- package/src/helpers/spec-builders.ts +5 -2
- package/src/index.ts +5 -0
- package/src/styles/dark.css +5 -1
- package/src/styles/tokens.css +6 -0
- package/src/theme/__tests__/dark-mode.test.ts +80 -0
- package/src/theme/__tests__/resolve.test.ts +114 -0
- package/src/theme/dark-mode.ts +56 -26
- package/src/theme/defaults.ts +2 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/presets.ts +64 -0
- package/src/theme/resolve.ts +85 -12
- package/src/types/index.ts +8 -0
- package/src/types/layout.ts +57 -1
- package/src/types/spec.ts +158 -29
- package/src/types/theme.ts +36 -0
package/src/theme/resolve.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* enabling darkMode.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { ThemeConfig } from '../types/spec';
|
|
14
|
-
import type { ResolvedTheme, Theme } from '../types/theme';
|
|
13
|
+
import type { ChromeThemeOverride, ThemeConfig } from '../types/spec';
|
|
14
|
+
import type { ChromeDefaults, ResolvedTheme, Theme, TokenValue } from '../types/theme';
|
|
15
15
|
import { DEFAULT_THEME } from './defaults';
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -44,6 +44,11 @@ function deepMerge(target: any, source: any): any {
|
|
|
44
44
|
return result;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Extract the light-mode string from a TokenValue. */
|
|
48
|
+
function resolveTokenLight(v: TokenValue): string {
|
|
49
|
+
return typeof v === 'string' ? v : v.light;
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* Convert a user-facing ThemeConfig (partial) into the full Theme shape
|
|
49
54
|
* that deepMerge can work with.
|
|
@@ -60,10 +65,16 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
|
|
|
60
65
|
if (config.colors.categorical) colors.categorical = config.colors.categorical;
|
|
61
66
|
if (config.colors.sequential) colors.sequential = config.colors.sequential;
|
|
62
67
|
if (config.colors.diverging) colors.diverging = config.colors.diverging;
|
|
63
|
-
if (config.colors.background) colors.background = config.colors.background;
|
|
64
|
-
if (config.colors.text) colors.text = config.colors.text;
|
|
65
|
-
if (config.colors.gridline) colors.gridline = config.colors.gridline;
|
|
66
|
-
if (config.colors.axis) colors.axis = config.colors.axis;
|
|
68
|
+
if (config.colors.background) colors.background = resolveTokenLight(config.colors.background);
|
|
69
|
+
if (config.colors.text) colors.text = resolveTokenLight(config.colors.text);
|
|
70
|
+
if (config.colors.gridline) colors.gridline = resolveTokenLight(config.colors.gridline);
|
|
71
|
+
if (config.colors.axis) colors.axis = resolveTokenLight(config.colors.axis);
|
|
72
|
+
if (config.colors.annotationFill)
|
|
73
|
+
colors.annotationFill = resolveTokenLight(config.colors.annotationFill);
|
|
74
|
+
if (config.colors.annotationText)
|
|
75
|
+
colors.annotationText = resolveTokenLight(config.colors.annotationText);
|
|
76
|
+
if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
|
|
77
|
+
if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
|
|
67
78
|
}
|
|
68
79
|
partial.colors = colors as Theme['colors'];
|
|
69
80
|
}
|
|
@@ -73,6 +84,7 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
|
|
|
73
84
|
if (config.fonts.family) fonts.family = config.fonts.family;
|
|
74
85
|
if (config.fonts.mono) fonts.mono = config.fonts.mono;
|
|
75
86
|
if (config.fonts.sizes) fonts.sizes = config.fonts.sizes as Theme['fonts']['sizes'];
|
|
87
|
+
if (config.fonts.weights) fonts.weights = config.fonts.weights as Theme['fonts']['weights'];
|
|
76
88
|
partial.fonts = fonts as Theme['fonts'];
|
|
77
89
|
}
|
|
78
90
|
|
|
@@ -80,6 +92,11 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
|
|
|
80
92
|
const spacing: Partial<Theme['spacing']> = {};
|
|
81
93
|
if (config.spacing.padding !== undefined) spacing.padding = config.spacing.padding;
|
|
82
94
|
if (config.spacing.chromeGap !== undefined) spacing.chromeGap = config.spacing.chromeGap;
|
|
95
|
+
if (config.spacing.chromeToChart !== undefined)
|
|
96
|
+
spacing.chromeToChart = config.spacing.chromeToChart;
|
|
97
|
+
if (config.spacing.chartToFooter !== undefined)
|
|
98
|
+
spacing.chartToFooter = config.spacing.chartToFooter;
|
|
99
|
+
if (config.spacing.axisMargin !== undefined) spacing.axisMargin = config.spacing.axisMargin;
|
|
83
100
|
if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;
|
|
84
101
|
if (config.spacing.xAxisLabelPadding !== undefined)
|
|
85
102
|
spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;
|
|
@@ -91,19 +108,74 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
|
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
if (config.chrome) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
const chrome: Record<string, Partial<ChromeDefaults>> = {};
|
|
112
|
+
for (const [element, value] of Object.entries(config.chrome)) {
|
|
113
|
+
if (value === undefined) continue;
|
|
114
|
+
if (typeof value === 'string') {
|
|
115
|
+
chrome[element] = { color: value };
|
|
116
|
+
} else {
|
|
117
|
+
const override: Partial<ChromeDefaults> = {};
|
|
118
|
+
const obj = value as ChromeThemeOverride;
|
|
119
|
+
if (obj.color !== undefined) override.color = resolveTokenLight(obj.color);
|
|
120
|
+
if (obj.fontSize !== undefined) override.fontSize = obj.fontSize;
|
|
121
|
+
if (obj.fontWeight !== undefined) override.fontWeight = obj.fontWeight;
|
|
122
|
+
if (obj.lineHeight !== undefined) override.lineHeight = obj.lineHeight;
|
|
123
|
+
chrome[element] = override;
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
partial.chrome = chrome as unknown as Theme['chrome'];
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
if (config.seriesStrategy) {
|
|
130
|
+
partial.seriesStrategy = config.seriesStrategy;
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
return partial;
|
|
105
134
|
}
|
|
106
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Collect TokenValue pairs from the user's ThemeConfig so adaptTheme()
|
|
138
|
+
* can use explicit dark values instead of algorithmic adaptation.
|
|
139
|
+
*/
|
|
140
|
+
function collectTokenPairs(
|
|
141
|
+
config: ThemeConfig,
|
|
142
|
+
): Record<string, { light: string; dark: string }> | undefined {
|
|
143
|
+
const pairs: Record<string, { light: string; dark: string }> = {};
|
|
144
|
+
if (config.colors && !Array.isArray(config.colors)) {
|
|
145
|
+
const colorFields = [
|
|
146
|
+
'background',
|
|
147
|
+
'text',
|
|
148
|
+
'gridline',
|
|
149
|
+
'axis',
|
|
150
|
+
'annotationFill',
|
|
151
|
+
'annotationText',
|
|
152
|
+
'positive',
|
|
153
|
+
'negative',
|
|
154
|
+
] as const;
|
|
155
|
+
for (const field of colorFields) {
|
|
156
|
+
const v = config.colors[field];
|
|
157
|
+
if (v && typeof v === 'object' && 'light' in v) {
|
|
158
|
+
pairs[`colors.${field}`] = v;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (config.chrome) {
|
|
163
|
+
for (const [element, value] of Object.entries(config.chrome)) {
|
|
164
|
+
if (
|
|
165
|
+
value &&
|
|
166
|
+
typeof value === 'object' &&
|
|
167
|
+
'color' in value &&
|
|
168
|
+
value.color &&
|
|
169
|
+
typeof value.color === 'object' &&
|
|
170
|
+
'light' in value.color
|
|
171
|
+
) {
|
|
172
|
+
pairs[`chrome.${element}.color`] = value.color;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return Object.keys(pairs).length > 0 ? pairs : undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
107
179
|
/**
|
|
108
180
|
* Parse a hex color to sRGB relative luminance (WCAG 2.1).
|
|
109
181
|
* Returns 0 for invalid/unparseable colors.
|
|
@@ -235,5 +307,6 @@ export function resolveTheme(userTheme?: ThemeConfig, base: Theme = DEFAULT_THEM
|
|
|
235
307
|
return {
|
|
236
308
|
...merged,
|
|
237
309
|
isDark: dark,
|
|
310
|
+
_tokenPairs: userTheme ? collectTokenPairs(userTheme) : undefined,
|
|
238
311
|
} as ResolvedTheme;
|
|
239
312
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { elementRef } from './events';
|
|
|
28
28
|
// Layout types (engine output)
|
|
29
29
|
export type {
|
|
30
30
|
A11yMetadata,
|
|
31
|
+
AnnotationFootnote,
|
|
31
32
|
ArcMark,
|
|
32
33
|
AreaMark,
|
|
33
34
|
AxisLayout,
|
|
@@ -44,6 +45,8 @@ export type {
|
|
|
44
45
|
CompileTableOptions,
|
|
45
46
|
EndpointLabelEntry,
|
|
46
47
|
EndpointLabelsLayout,
|
|
48
|
+
FacetLayout,
|
|
49
|
+
FacetPanelLayout,
|
|
47
50
|
FlagTableCell,
|
|
48
51
|
GradientColorStop,
|
|
49
52
|
GradientLegendLayout,
|
|
@@ -124,6 +127,7 @@ export type {
|
|
|
124
127
|
Chrome,
|
|
125
128
|
ChromeText,
|
|
126
129
|
ChromeTextStyle,
|
|
130
|
+
ChromeThemeOverride,
|
|
127
131
|
CircleEncoding,
|
|
128
132
|
Condition,
|
|
129
133
|
ConditionalValueDef,
|
|
@@ -133,6 +137,7 @@ export type {
|
|
|
133
137
|
Encoding,
|
|
134
138
|
EncodingChannel,
|
|
135
139
|
EndpointLabelsConfig,
|
|
140
|
+
FacetChannel,
|
|
136
141
|
FieldPredicate,
|
|
137
142
|
FieldType,
|
|
138
143
|
FilterPredicate,
|
|
@@ -170,6 +175,7 @@ export type {
|
|
|
170
175
|
RelativeTimeRef,
|
|
171
176
|
ResolveConfig,
|
|
172
177
|
ResolveMode,
|
|
178
|
+
ResponsiveConfig,
|
|
173
179
|
SankeyEncoding,
|
|
174
180
|
SankeyLinkColor,
|
|
175
181
|
SankeyNodeAlign,
|
|
@@ -232,6 +238,7 @@ export type {
|
|
|
232
238
|
export type {
|
|
233
239
|
ChromeDefaults,
|
|
234
240
|
ResolvedTheme,
|
|
241
|
+
SeriesStrategy,
|
|
235
242
|
Theme,
|
|
236
243
|
ThemeChromeDefaults,
|
|
237
244
|
ThemeColors,
|
|
@@ -239,4 +246,5 @@ export type {
|
|
|
239
246
|
ThemeFonts,
|
|
240
247
|
ThemeFontWeights,
|
|
241
248
|
ThemeSpacing,
|
|
249
|
+
TokenValue,
|
|
242
250
|
} from './theme';
|
package/src/types/layout.ts
CHANGED
|
@@ -109,6 +109,8 @@ export interface ResolvedChrome {
|
|
|
109
109
|
byline?: ResolvedChromeElement;
|
|
110
110
|
footer?: ResolvedChromeElement;
|
|
111
111
|
brand?: ResolvedChromeElement;
|
|
112
|
+
/** Annotation footnotes generated by auto-thinning. Rendered as a numbered list above the source line. */
|
|
113
|
+
footnotes?: AnnotationFootnote[];
|
|
112
114
|
/** Absolute y at which bottom-chrome relative offsets are anchored: area.y + area.height + x-axis extent. */
|
|
113
115
|
bottomAnchorY?: number;
|
|
114
116
|
}
|
|
@@ -610,6 +612,20 @@ export interface ResolvedAnnotation {
|
|
|
610
612
|
};
|
|
611
613
|
/** Full placed extent: label bounds unioned with subtitle bounds. Collision and placement source of truth. */
|
|
612
614
|
bounds?: Rect;
|
|
615
|
+
/**
|
|
616
|
+
* When set, this annotation was demoted to a footnote marker by auto-thinning.
|
|
617
|
+
* The label is replaced by a numbered dot at the data point; the original text
|
|
618
|
+
* appears in the chrome footnote band. 1-indexed.
|
|
619
|
+
*/
|
|
620
|
+
footnoteIndex?: number;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** A footnote entry generated by annotation auto-thinning. */
|
|
624
|
+
export interface AnnotationFootnote {
|
|
625
|
+
/** 1-indexed footnote number matching the dot marker on the chart. */
|
|
626
|
+
index: number;
|
|
627
|
+
/** The original annotation text that was demoted. */
|
|
628
|
+
text: string;
|
|
613
629
|
}
|
|
614
630
|
|
|
615
631
|
// ---------------------------------------------------------------------------
|
|
@@ -751,6 +767,10 @@ export interface EndpointLabelsLayout {
|
|
|
751
767
|
valueGap: number;
|
|
752
768
|
/** Fill color for the rounded chip background behind the colored bar. */
|
|
753
769
|
swatchChipFill: string;
|
|
770
|
+
/** Leading-edge (left side) entries when `ends: 'both'`. Empty when trailing only. */
|
|
771
|
+
leading?: EndpointLabelEntry[];
|
|
772
|
+
/** Bounding box for the leading column. Only set when `ends: 'both'`. */
|
|
773
|
+
leadingBounds?: Rect;
|
|
754
774
|
}
|
|
755
775
|
|
|
756
776
|
// ---------------------------------------------------------------------------
|
|
@@ -851,6 +871,38 @@ export interface ResolvedMetricBar {
|
|
|
851
871
|
// ChartLayout (the main engine output for charts)
|
|
852
872
|
// ---------------------------------------------------------------------------
|
|
853
873
|
|
|
874
|
+
/**
|
|
875
|
+
* A single panel in a faceted (small-multiples) layout.
|
|
876
|
+
* Each panel has its own marks, axes, and drawing area but typically
|
|
877
|
+
* shares scales with sibling panels.
|
|
878
|
+
*/
|
|
879
|
+
export interface FacetPanelLayout {
|
|
880
|
+
/** Facet value that produced this panel. */
|
|
881
|
+
key: string;
|
|
882
|
+
/** Panel drawing area in SVG coordinates. */
|
|
883
|
+
area: Rect;
|
|
884
|
+
/** Data marks for this panel. */
|
|
885
|
+
marks: Mark[];
|
|
886
|
+
/** Per-panel axis layouts. Inner panels may have ticks suppressed. */
|
|
887
|
+
axes: { x?: AxisLayout; y?: AxisLayout };
|
|
888
|
+
/** Per-panel annotations. */
|
|
889
|
+
annotations: ResolvedAnnotation[];
|
|
890
|
+
/** Panel header label (facet value) positioned above the panel. */
|
|
891
|
+
header: { text: string; x: number; y: number; fontSize: number; fontWeight: number };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** Layout for a faceted (small-multiples) chart. */
|
|
895
|
+
export interface FacetLayout {
|
|
896
|
+
/** Ordered array of panel layouts. */
|
|
897
|
+
panels: FacetPanelLayout[];
|
|
898
|
+
/** The data field used for faceting. */
|
|
899
|
+
facetField: string;
|
|
900
|
+
/** Number of columns in the wrap grid. */
|
|
901
|
+
columns: number;
|
|
902
|
+
/** Whether scales are shared across panels. */
|
|
903
|
+
sharedScales: boolean;
|
|
904
|
+
}
|
|
905
|
+
|
|
854
906
|
/**
|
|
855
907
|
* ChartLayout: the complete engine output for chart visualizations.
|
|
856
908
|
*
|
|
@@ -903,6 +955,8 @@ export interface ChartLayout {
|
|
|
903
955
|
crosshair: boolean;
|
|
904
956
|
/** Real text measurement function from the adapter (for accurate SVG text wrapping). */
|
|
905
957
|
measureText?: MeasureTextFn;
|
|
958
|
+
/** Faceted (small-multiples) layout. When present, renderers iterate panels instead of using top-level marks/axes. */
|
|
959
|
+
facet?: FacetLayout;
|
|
906
960
|
}
|
|
907
961
|
|
|
908
962
|
// ---------------------------------------------------------------------------
|
|
@@ -1320,8 +1374,10 @@ export interface TileMapLayout {
|
|
|
1320
1374
|
chrome: ResolvedChrome;
|
|
1321
1375
|
/** Resolved tile marks with positions, colors, and labels. */
|
|
1322
1376
|
tiles: TileMapTileMark[];
|
|
1323
|
-
/** Gradient color legend (null if
|
|
1377
|
+
/** Gradient color legend for quantitative mode (null if hidden or categorical). */
|
|
1324
1378
|
gradientLegend: GradientLegendLayout | null;
|
|
1379
|
+
/** Categorical swatch legend for nominal mode (null if hidden or quantitative). */
|
|
1380
|
+
categoricalLegend: CategoricalLegendLayout | null;
|
|
1325
1381
|
/** Tooltip content descriptors keyed by state code. */
|
|
1326
1382
|
tooltipDescriptors: Map<string, TooltipContent>;
|
|
1327
1383
|
/** Accessibility metadata for the tile map. */
|
package/src/types/spec.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Re-import for use in LegendConfig and overrides (avoids circular by importing from sibling)
|
|
12
12
|
import type { Breakpoint, LegendPosition } from '../responsive/breakpoints';
|
|
13
13
|
import type { ColumnConfig } from './table';
|
|
14
|
+
import type { SeriesStrategy, TokenValue } from './theme';
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Mark type union (Vega-Lite aligned)
|
|
@@ -417,6 +418,37 @@ export interface EncodingChannel<TData extends DataRow = DataRow> {
|
|
|
417
418
|
* Used in tooltips; `axis.format` takes precedence for axis tick labels.
|
|
418
419
|
*/
|
|
419
420
|
format?: string;
|
|
421
|
+
/**
|
|
422
|
+
* Emphasis highlight for the color channel. Names one or more series values
|
|
423
|
+
* to visually emphasize; all other series are muted to a neutral gray.
|
|
424
|
+
*
|
|
425
|
+
* Only meaningful on the `color` encoding channel. On other channels this
|
|
426
|
+
* property is ignored.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* encoding: {
|
|
430
|
+
* color: { field: 'region', type: 'nominal', highlight: 'West' }
|
|
431
|
+
* }
|
|
432
|
+
*/
|
|
433
|
+
highlight?: string | string[];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Facet channel definition: partitions data into small-multiple panels.
|
|
438
|
+
*
|
|
439
|
+
* Unlike positional channels, facet only needs a field, a categorical type,
|
|
440
|
+
* and optional layout hints (columns, sort). It does not carry scale/axis
|
|
441
|
+
* config because it produces a grid, not a visual encoding.
|
|
442
|
+
*/
|
|
443
|
+
export interface FacetChannel<TData extends DataRow = DataRow> {
|
|
444
|
+
/** Data field to partition by. Each unique value produces one panel. */
|
|
445
|
+
field: keyof TData & string;
|
|
446
|
+
/** Must be categorical. */
|
|
447
|
+
type: 'nominal' | 'ordinal';
|
|
448
|
+
/** Number of columns in the wrap grid. Auto-computed when omitted. */
|
|
449
|
+
columns?: number;
|
|
450
|
+
/** Sort order for panel values. Default ascending. */
|
|
451
|
+
sort?: 'ascending' | 'descending' | null;
|
|
420
452
|
}
|
|
421
453
|
|
|
422
454
|
/**
|
|
@@ -512,6 +544,13 @@ export interface Encoding<TData extends DataRow = DataRow> {
|
|
|
512
544
|
* Not used by any other mark type.
|
|
513
545
|
*/
|
|
514
546
|
radius?: EncodingChannel<TData>;
|
|
547
|
+
/**
|
|
548
|
+
* Facet channel: partitions data into a grid of small-multiple panels.
|
|
549
|
+
* Each unique value of the facet field produces one panel. Panels share
|
|
550
|
+
* scales by default; use `resolve: { scale: { y: 'independent' } }` for
|
|
551
|
+
* per-panel axes.
|
|
552
|
+
*/
|
|
553
|
+
facet?: FacetChannel<TData>;
|
|
515
554
|
}
|
|
516
555
|
|
|
517
556
|
// ---------------------------------------------------------------------------
|
|
@@ -679,6 +718,8 @@ export interface TextAnnotation extends AnnotationBase {
|
|
|
679
718
|
y: string | number;
|
|
680
719
|
/** The annotation text. Required for text annotations. */
|
|
681
720
|
text: string;
|
|
721
|
+
/** Thinning priority (lower = kept longer at narrow widths). When omitted, spec order is used. */
|
|
722
|
+
priority?: number;
|
|
682
723
|
/**
|
|
683
724
|
* Optional muted second-tone text rendered below the primary `text`.
|
|
684
725
|
* Used for supporting context (e.g. methodology, source). Newlines in
|
|
@@ -804,6 +845,21 @@ export type Annotation = TextAnnotation | RangeAnnotation | RefLineAnnotation;
|
|
|
804
845
|
*/
|
|
805
846
|
export type DarkMode = 'auto' | 'force' | 'off';
|
|
806
847
|
|
|
848
|
+
/**
|
|
849
|
+
* Per-element chrome theme override. Pass a plain string (color only,
|
|
850
|
+
* backward-compatible) or an object with full typography control.
|
|
851
|
+
*/
|
|
852
|
+
export interface ChromeThemeOverride {
|
|
853
|
+
/** Text color. Accepts a TokenValue for light/dark pairs. */
|
|
854
|
+
color?: TokenValue;
|
|
855
|
+
/** Font size in pixels. */
|
|
856
|
+
fontSize?: number;
|
|
857
|
+
/** Font weight. */
|
|
858
|
+
fontWeight?: number;
|
|
859
|
+
/** Line height multiplier. */
|
|
860
|
+
lineHeight?: number;
|
|
861
|
+
}
|
|
862
|
+
|
|
807
863
|
/**
|
|
808
864
|
* User-facing theme configuration for overriding defaults.
|
|
809
865
|
* All fields are optional. The engine deep-merges these onto the default theme.
|
|
@@ -823,14 +879,22 @@ export interface ThemeConfig {
|
|
|
823
879
|
sequential?: Record<string, string[]>;
|
|
824
880
|
/** Diverging palettes keyed by name. Each is an array of color stops with a neutral midpoint. */
|
|
825
881
|
diverging?: Record<string, string[]>;
|
|
826
|
-
/** Background color. */
|
|
827
|
-
background?:
|
|
828
|
-
/** Default text color. */
|
|
829
|
-
text?:
|
|
830
|
-
/** Gridline color. */
|
|
831
|
-
gridline?:
|
|
832
|
-
/** Axis line and tick color. */
|
|
833
|
-
axis?:
|
|
882
|
+
/** Background color. Accepts a TokenValue for light/dark pairs. */
|
|
883
|
+
background?: TokenValue;
|
|
884
|
+
/** Default text color. Accepts a TokenValue for light/dark pairs. */
|
|
885
|
+
text?: TokenValue;
|
|
886
|
+
/** Gridline color. Accepts a TokenValue for light/dark pairs. */
|
|
887
|
+
gridline?: TokenValue;
|
|
888
|
+
/** Axis line and tick color. Accepts a TokenValue for light/dark pairs. */
|
|
889
|
+
axis?: TokenValue;
|
|
890
|
+
/** Annotation fill color. Accepts a TokenValue for light/dark pairs. */
|
|
891
|
+
annotationFill?: TokenValue;
|
|
892
|
+
/** Annotation text color. Accepts a TokenValue for light/dark pairs. */
|
|
893
|
+
annotationText?: TokenValue;
|
|
894
|
+
/** Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs. */
|
|
895
|
+
positive?: TokenValue;
|
|
896
|
+
/** Semantic color for negative/down-trend values. Accepts a TokenValue for light/dark pairs. */
|
|
897
|
+
negative?: TokenValue;
|
|
834
898
|
};
|
|
835
899
|
/** Font overrides. */
|
|
836
900
|
fonts?: {
|
|
@@ -855,6 +919,17 @@ export interface ThemeConfig {
|
|
|
855
919
|
/** KPI metric primary value. Default: 22. Delta/secondary derive from this. */
|
|
856
920
|
metricValue?: number;
|
|
857
921
|
};
|
|
922
|
+
/** Font weight overrides. Partial — only specified keys are overridden. */
|
|
923
|
+
weights?: {
|
|
924
|
+
/** Normal text weight. Default: 450. */
|
|
925
|
+
normal?: number;
|
|
926
|
+
/** Medium text weight. Default: 550. */
|
|
927
|
+
medium?: number;
|
|
928
|
+
/** Semibold text weight. Default: 590. */
|
|
929
|
+
semibold?: number;
|
|
930
|
+
/** Bold text weight. Default: 700. */
|
|
931
|
+
bold?: number;
|
|
932
|
+
};
|
|
858
933
|
};
|
|
859
934
|
/** Spacing overrides in pixels. */
|
|
860
935
|
spacing?: {
|
|
@@ -862,6 +937,12 @@ export interface ThemeConfig {
|
|
|
862
937
|
padding?: number;
|
|
863
938
|
/** Gap between chrome elements (title to subtitle, etc.). */
|
|
864
939
|
chromeGap?: number;
|
|
940
|
+
/** Gap between the last chrome element and the chart area. */
|
|
941
|
+
chromeToChart?: number;
|
|
942
|
+
/** Gap between chart area and source/footer below. */
|
|
943
|
+
chartToFooter?: number;
|
|
944
|
+
/** Internal padding within the chart area (axes margins). */
|
|
945
|
+
axisMargin?: number;
|
|
865
946
|
/** Height reserved below chart area for x-axis tick labels. Increase when large axisTick font sizes cause label clipping. */
|
|
866
947
|
xAxisHeight?: number;
|
|
867
948
|
/** Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line. */
|
|
@@ -870,25 +951,29 @@ export interface ThemeConfig {
|
|
|
870
951
|
/** Border radius for chart container and tooltips. */
|
|
871
952
|
borderRadius?: number;
|
|
872
953
|
/**
|
|
873
|
-
* Per-element chrome
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* chrome color the spec set explicitly).
|
|
954
|
+
* Per-element chrome style overrides. Pass a plain string for color-only
|
|
955
|
+
* (backward-compatible), or a ChromeThemeOverride object for full
|
|
956
|
+
* typography control (color, fontSize, fontWeight, lineHeight).
|
|
877
957
|
*/
|
|
878
958
|
chrome?: {
|
|
879
|
-
/** Eyebrow (kicker)
|
|
880
|
-
eyebrow?: string;
|
|
881
|
-
/** Title
|
|
882
|
-
title?: string;
|
|
883
|
-
/** Subtitle
|
|
884
|
-
subtitle?: string;
|
|
885
|
-
/** Source/attribution
|
|
886
|
-
source?: string;
|
|
887
|
-
/** Byline
|
|
888
|
-
byline?: string;
|
|
889
|
-
/** Footer
|
|
890
|
-
footer?: string;
|
|
959
|
+
/** Eyebrow (kicker) style override. */
|
|
960
|
+
eyebrow?: string | ChromeThemeOverride;
|
|
961
|
+
/** Title style override. */
|
|
962
|
+
title?: string | ChromeThemeOverride;
|
|
963
|
+
/** Subtitle style override. */
|
|
964
|
+
subtitle?: string | ChromeThemeOverride;
|
|
965
|
+
/** Source/attribution style override. */
|
|
966
|
+
source?: string | ChromeThemeOverride;
|
|
967
|
+
/** Byline style override. */
|
|
968
|
+
byline?: string | ChromeThemeOverride;
|
|
969
|
+
/** Footer style override. */
|
|
970
|
+
footer?: string | ChromeThemeOverride;
|
|
891
971
|
};
|
|
972
|
+
/**
|
|
973
|
+
* Series color assignment strategy.
|
|
974
|
+
* Default: 'palette' (full categorical palette always, zero visual drift).
|
|
975
|
+
*/
|
|
976
|
+
seriesStrategy?: SeriesStrategy;
|
|
892
977
|
}
|
|
893
978
|
|
|
894
979
|
// ---------------------------------------------------------------------------
|
|
@@ -976,6 +1061,14 @@ export interface LegendConfig {
|
|
|
976
1061
|
export interface EndpointLabelsConfig {
|
|
977
1062
|
/** Explicit on/off. When undefined, the chart auto-decides based on series count. */
|
|
978
1063
|
show?: boolean;
|
|
1064
|
+
/** Maximum number of series before endpoint labels switch to a legend. Default 8. */
|
|
1065
|
+
maxSeries?: number;
|
|
1066
|
+
/**
|
|
1067
|
+
* Which ends of the chart get label columns.
|
|
1068
|
+
* - `'end'` (default): labels on the right (trailing) edge only.
|
|
1069
|
+
* - `'both'`: labels on both left (leading) and right (trailing) edges.
|
|
1070
|
+
*/
|
|
1071
|
+
ends?: 'end' | 'both';
|
|
979
1072
|
/** Field to read the displayed value from. Defaults to `encoding.y.field`. */
|
|
980
1073
|
valueField?: string;
|
|
981
1074
|
/** d3-format string for the value. Defaults to `encoding.y.axis.format`. */
|
|
@@ -1038,6 +1131,17 @@ export interface AnimationStagger {
|
|
|
1038
1131
|
order?: 'index' | 'value' | 'reverse';
|
|
1039
1132
|
}
|
|
1040
1133
|
|
|
1134
|
+
/** Fine-grained responsive behavior configuration. */
|
|
1135
|
+
export interface ResponsiveConfig {
|
|
1136
|
+
/**
|
|
1137
|
+
* Automatic annotation thinning at narrow widths. When true (default),
|
|
1138
|
+
* annotations that can't be placed without overlap are demoted to numbered
|
|
1139
|
+
* dot markers with footnote text below the chart. Set false to restore
|
|
1140
|
+
* the pre-auto-thinning behavior (annotations hidden at compact breakpoints).
|
|
1141
|
+
*/
|
|
1142
|
+
autoThin?: boolean;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1041
1145
|
/**
|
|
1042
1146
|
* Animation phase config. Follows Vega's enter/update/exit model.
|
|
1043
1147
|
* Each phase can be true (use defaults) or a config object.
|
|
@@ -1282,8 +1386,12 @@ interface BaseChartSpec<TData extends DataRow = DataRow> {
|
|
|
1282
1386
|
* relates this flag to `legend.show` and the legacy end-of-line labels.
|
|
1283
1387
|
*/
|
|
1284
1388
|
endpointLabels?: boolean | EndpointLabelsConfig;
|
|
1285
|
-
/**
|
|
1286
|
-
|
|
1389
|
+
/**
|
|
1390
|
+
* Responsive behavior. `true`/`false` enables/disables container-width
|
|
1391
|
+
* adaptation. Pass a `ResponsiveConfig` object for fine-grained control
|
|
1392
|
+
* (e.g. `{ autoThin: false }` to disable automatic annotation thinning).
|
|
1393
|
+
*/
|
|
1394
|
+
responsive?: boolean | ResponsiveConfig;
|
|
1287
1395
|
/** Theme configuration overrides. */
|
|
1288
1396
|
theme?: ThemeConfig;
|
|
1289
1397
|
/** Dark mode behavior. Defaults to "off". */
|
|
@@ -1329,6 +1437,13 @@ interface BaseChartSpec<TData extends DataRow = DataRow> {
|
|
|
1329
1437
|
* top-level and per-breakpoint overrides.
|
|
1330
1438
|
*/
|
|
1331
1439
|
display?: Display;
|
|
1440
|
+
/**
|
|
1441
|
+
* Resolution strategy for shared/independent scales and axes across facet
|
|
1442
|
+
* panels. Only meaningful when `encoding.facet` is present. Shared scales
|
|
1443
|
+
* (the default) let readers compare across panels; independent scales give
|
|
1444
|
+
* each panel its own domain.
|
|
1445
|
+
*/
|
|
1446
|
+
resolve?: ResolveConfig;
|
|
1332
1447
|
/**
|
|
1333
1448
|
* Render order within a LayerSpec. Higher values render on top.
|
|
1334
1449
|
* When omitted, layers render in array order (later layers paint on top).
|
|
@@ -1667,8 +1782,14 @@ export interface SankeySpec {
|
|
|
1667
1782
|
export interface TileMapEncoding {
|
|
1668
1783
|
/** State code field (required, nominal). Maps to US state abbreviations. */
|
|
1669
1784
|
state: EncodingChannel;
|
|
1670
|
-
/**
|
|
1785
|
+
/**
|
|
1786
|
+
* Value field. For quantitative mode, maps to a sequential color scale.
|
|
1787
|
+
* For categorical mode (when color channel is present or values are strings),
|
|
1788
|
+
* maps to category labels shown in tooltips.
|
|
1789
|
+
*/
|
|
1671
1790
|
value: EncodingChannel;
|
|
1791
|
+
/** Color encoding channel (optional, nominal). When present, enables categorical coloring. */
|
|
1792
|
+
color?: EncodingChannel;
|
|
1672
1793
|
/** Tooltip encoding (optional). */
|
|
1673
1794
|
tooltip?: EncodingChannel | EncodingChannel[];
|
|
1674
1795
|
}
|
|
@@ -1682,16 +1803,24 @@ export interface TileMapSpec {
|
|
|
1682
1803
|
/**
|
|
1683
1804
|
* Data for the tile map. Accepts either:
|
|
1684
1805
|
* - A record mapping state codes to numeric values: `{ "CA": 12000, "TX": 8500 }`
|
|
1806
|
+
* - A record mapping state codes to string categories: `{ "CA": "high", "TX": "low" }`
|
|
1685
1807
|
* - Tabular data rows with state and value fields (requires encoding)
|
|
1686
1808
|
*/
|
|
1687
|
-
data: Record<string, number | null> | DataRow[];
|
|
1809
|
+
data: Record<string, number | string | null> | DataRow[];
|
|
1688
1810
|
/**
|
|
1689
1811
|
* Encoding channels mapping data fields to visual properties.
|
|
1690
1812
|
* Required when data is DataRow[]. Auto-generated when data is a record map.
|
|
1691
1813
|
*/
|
|
1692
1814
|
encoding?: TileMapEncoding;
|
|
1693
|
-
/** Sequential color palette. Defaults to 'blue'. */
|
|
1815
|
+
/** Sequential color palette. Defaults to 'blue'. Only used in quantitative mode. */
|
|
1694
1816
|
palette?: TileMapPalette;
|
|
1817
|
+
/**
|
|
1818
|
+
* Custom category-to-color mapping for categorical tilemaps.
|
|
1819
|
+
* Keys are category values, values are CSS color strings.
|
|
1820
|
+
* When provided, forces categorical mode regardless of encoding.
|
|
1821
|
+
* Example: `{ "medical_only": "#ee4a73", "religious": "#e07d00", "philosophical": "#06b6d4" }`
|
|
1822
|
+
*/
|
|
1823
|
+
colors?: Record<string, string>;
|
|
1695
1824
|
/** Editorial chrome (title, subtitle, source, byline, footer). */
|
|
1696
1825
|
chrome?: Chrome;
|
|
1697
1826
|
/** Legend display configuration. */
|
package/src/types/theme.ts
CHANGED
|
@@ -5,6 +5,33 @@
|
|
|
5
5
|
* ResolvedTheme is the engine-internal fully resolved version (no optionals).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Token value (light/dark pair)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A color token value: either a plain CSS color string, or a light/dark pair.
|
|
14
|
+
* When a pair is given, the resolver picks the appropriate value based on
|
|
15
|
+
* dark mode state. When a plain string is given, dark-mode adaptation
|
|
16
|
+
* derives the dark value algorithmically (existing behavior).
|
|
17
|
+
*/
|
|
18
|
+
export type TokenValue = string | { light: string; dark: string };
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Series strategy
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Series color assignment strategy.
|
|
26
|
+
*
|
|
27
|
+
* - `'palette'` — always assign from the full categorical palette (current
|
|
28
|
+
* default, zero visual drift).
|
|
29
|
+
* - `'accent-neutral'` — assign by series count: a single series gets the
|
|
30
|
+
* accent (first categorical color) only; 2-4 series get accent for the
|
|
31
|
+
* first and neutral grays for the rest; 5+ fall back to the full palette.
|
|
32
|
+
*/
|
|
33
|
+
export type SeriesStrategy = 'palette' | 'accent-neutral';
|
|
34
|
+
|
|
8
35
|
// ---------------------------------------------------------------------------
|
|
9
36
|
// Color palette types
|
|
10
37
|
// ---------------------------------------------------------------------------
|
|
@@ -154,6 +181,8 @@ export interface Theme {
|
|
|
154
181
|
borderRadius: number;
|
|
155
182
|
/** Default chrome text styles. */
|
|
156
183
|
chrome: ThemeChromeDefaults;
|
|
184
|
+
/** Series color assignment strategy. Default: 'palette' (full categorical). */
|
|
185
|
+
seriesStrategy: SeriesStrategy;
|
|
157
186
|
}
|
|
158
187
|
|
|
159
188
|
/**
|
|
@@ -170,4 +199,11 @@ export interface Theme {
|
|
|
170
199
|
export interface ResolvedTheme extends Theme {
|
|
171
200
|
/** Whether dark mode adaptations have been applied to this theme. */
|
|
172
201
|
isDark: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Original light/dark token pairs from the user's ThemeConfig. Used
|
|
204
|
+
* by adaptTheme() to skip algorithmic adaptation for tokens with
|
|
205
|
+
* explicit dark values. Engine-internal, not part of the public API.
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
_tokenPairs?: Record<string, { light: string; dark: string }>;
|
|
173
209
|
}
|