@opendata-ai/openchart-engine 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 +123 -14
- package/dist/index.js +16166 -10027
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +170 -56
- package/src/__tests__/annotation-behavior.test.ts +721 -0
- package/src/__tests__/axes.test.ts +50 -3
- package/src/__tests__/compile-chart.test.ts +30 -0
- package/src/__tests__/compile-footnotes.test.ts +277 -0
- package/src/__tests__/compile-layer.test.ts +120 -1
- package/src/__tests__/helpers/invariants.ts +54 -20
- package/src/__tests__/known-bugs.test.ts +455 -2
- package/src/__tests__/legend.test.ts +69 -4
- package/src/__tests__/ranking-recipes.test.ts +260 -0
- package/src/__tests__/scales.test.ts +9 -3
- package/src/__tests__/series-search.test.ts +92 -0
- package/src/__tests__/spec-sugar.test.ts +801 -0
- package/src/__tests__/you-draw-it.test.ts +160 -0
- package/src/annotations/__tests__/compute.test.ts +207 -92
- package/src/annotations/__tests__/geometry.test.ts +66 -0
- package/src/annotations/__tests__/resolve-text.test.ts +527 -27
- package/src/annotations/__tests__/rich-text.test.ts +91 -0
- package/src/annotations/__tests__/thinning.test.ts +148 -0
- package/src/annotations/collisions.ts +130 -84
- package/src/annotations/compute.ts +83 -85
- package/src/annotations/constants.ts +116 -6
- package/src/annotations/geometry.ts +222 -77
- package/src/annotations/move.ts +135 -0
- package/src/annotations/placement.ts +62 -14
- package/src/annotations/resolve-range.ts +2 -0
- package/src/annotations/resolve-refline.ts +2 -0
- package/src/annotations/resolve-text.ts +312 -111
- package/src/annotations/rich-text.ts +65 -0
- package/src/annotations/thinning.ts +84 -10
- package/src/barlist/compile-barlist.ts +16 -6
- package/src/charts/_shared/format-label-value.ts +9 -8
- package/src/charts/bar/__tests__/compute.test.ts +57 -10
- package/src/charts/bar/__tests__/labels.test.ts +5 -4
- package/src/charts/bar/compute.ts +2 -6
- package/src/charts/bar/index.ts +8 -1
- package/src/charts/bar/labels.ts +3 -3
- package/src/charts/beeswarm/__tests__/compute.test.ts +280 -0
- package/src/charts/beeswarm/__tests__/dodge.test.ts +148 -0
- package/src/charts/beeswarm/compute.ts +203 -0
- package/src/charts/beeswarm/dodge.ts +155 -0
- package/src/charts/beeswarm/index.ts +32 -0
- package/src/charts/builtin.ts +12 -1
- package/src/charts/calendar/__tests__/compute.test.ts +417 -0
- package/src/charts/calendar/compute.ts +319 -0
- package/src/charts/calendar/index.ts +32 -0
- package/src/charts/column/__tests__/compute.test.ts +21 -7
- package/src/charts/column/__tests__/labels.test.ts +76 -1
- package/src/charts/column/compute.ts +2 -6
- package/src/charts/column/index.ts +10 -2
- package/src/charts/column/labels.ts +95 -21
- package/src/charts/dot/index.ts +8 -1
- package/src/charts/dot/labels.ts +3 -3
- package/src/charts/line/__tests__/compute.test.ts +15 -19
- package/src/charts/line/area.ts +8 -23
- package/src/charts/line/compute.ts +32 -2
- package/src/charts/parliament/__tests__/compute.test.ts +411 -0
- package/src/charts/parliament/compute.ts +313 -0
- package/src/charts/parliament/index.ts +32 -0
- package/src/charts/pie/__tests__/compute.test.ts +103 -0
- package/src/charts/pie/compute.ts +63 -8
- package/src/charts/pie/labels.ts +1 -0
- package/src/charts/range/__tests__/compute.test.ts +486 -0
- package/src/charts/range/compute.ts +273 -0
- package/src/charts/range/index.ts +82 -0
- package/src/charts/range/labels.ts +169 -0
- package/src/charts/rect/__tests__/compute.test.ts +120 -0
- package/src/charts/rect/index.ts +127 -0
- package/src/charts/scatter/compute.ts +7 -21
- package/src/charts/text/__tests__/compute.test.ts +307 -0
- package/src/charts/text/index.ts +44 -7
- package/src/charts/tick/__tests__/compute.test.ts +75 -0
- package/src/charts/tick/index.ts +7 -4
- package/src/charts/utils.ts +5 -3
- package/src/charts/waffle/__tests__/compute.test.ts +305 -0
- package/src/charts/waffle/compute.ts +197 -0
- package/src/charts/waffle/index.ts +31 -0
- package/src/compile/__tests__/color-scale-reverse.test.ts +89 -0
- package/src/compile/__tests__/data-clip.test.ts +68 -0
- package/src/compile/__tests__/fill-patterns.test.ts +154 -0
- package/src/compile/__tests__/size-scale.test.ts +108 -0
- package/src/compile/color-scale-range.ts +54 -33
- package/src/compile/data-clip.ts +21 -6
- package/src/compile/fill-patterns.ts +74 -0
- package/src/compile/layer.ts +117 -21
- package/src/compile/size-scale.ts +139 -0
- package/src/compile/spec-sugar.ts +758 -0
- package/src/compile/you-draw-it.ts +123 -0
- package/src/compile.ts +310 -103
- package/src/compiler/__tests__/a11y-warnings.test.ts +148 -0
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +20 -0
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +22 -0
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +14 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +34 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +22 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +41 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +38 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +28 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +28 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +30 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +29 -0
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +32 -0
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +25 -0
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +21 -0
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +23 -0
- package/src/compiler/__tests__/keys.test.ts +3 -9
- package/src/compiler/__tests__/normalize.test.ts +65 -1
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +86 -0
- package/src/compiler/__tests__/validate-near-miss.test.ts +89 -0
- package/src/compiler/__tests__/validate.test.ts +156 -0
- package/src/compiler/__tests__/vl-parity.test.ts +68 -0
- package/src/compiler/a11y-warnings.ts +110 -0
- package/src/compiler/keys.ts +5 -4
- package/src/compiler/normalize.ts +150 -7
- package/src/compiler/types.ts +11 -0
- package/src/compiler/validate.ts +664 -18
- package/src/endpoint-labels/__tests__/compute.test.ts +88 -0
- package/src/endpoint-labels/compute.ts +40 -12
- package/src/endpoint-labels/format.ts +45 -15
- package/src/endpoint-labels/predict.ts +50 -36
- package/src/format/__tests__/field-format.test.ts +164 -0
- package/src/format/field-format.ts +27 -0
- package/src/geo/__tests__/compile-map.test.ts +621 -0
- package/src/geo/compile-map.ts +856 -0
- package/src/geo/join.ts +115 -0
- package/src/geo/projections.ts +45 -0
- package/src/geo/types.ts +24 -0
- package/src/graphs/__tests__/compile-graph.test.ts +25 -0
- package/src/graphs/compile-graph.ts +27 -18
- package/src/index.ts +20 -2
- package/src/layout/axes/ticks.ts +59 -10
- package/src/layout/axes.ts +35 -6
- package/src/layout/dimensions.ts +139 -83
- package/src/layout/facet.ts +14 -3
- package/src/layout/plan.ts +83 -33
- package/src/layout/scales.ts +132 -15
- package/src/legend/__tests__/continuous.test.ts +318 -0
- package/src/legend/__tests__/size.test.ts +348 -0
- package/src/legend/__tests__/suppression.test.ts +17 -1
- package/src/legend/compute.ts +119 -13
- package/src/legend/continuous.ts +327 -0
- package/src/legend/size.ts +407 -0
- package/src/legend/suppression.ts +8 -7
- package/src/sankey/compile-sankey.ts +31 -19
- package/src/tables/format-cells.ts +16 -7
- package/src/tilemap/__tests__/compile-tilemap.test.ts +39 -0
- package/src/tilemap/compile-tilemap.ts +24 -7
- package/src/tooltips/__tests__/compute.test.ts +2 -2
- package/src/tooltips/compute.ts +422 -41
- package/src/transforms/__tests__/conditional.test.ts +18 -0
- package/src/transforms/aggregate.ts +2 -1
package/src/compile.ts
CHANGED
|
@@ -14,35 +14,32 @@
|
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
16
|
AnimationSpec,
|
|
17
|
-
BinParams,
|
|
18
|
-
BinTransform,
|
|
19
17
|
ChartLayout,
|
|
20
18
|
ChartSpec,
|
|
21
19
|
CompileOptions,
|
|
22
20
|
CompileTableOptions,
|
|
23
21
|
Encoding,
|
|
24
|
-
EncodingChannel,
|
|
25
22
|
FacetChannel,
|
|
26
23
|
FacetPanelLayout,
|
|
27
24
|
LayerSpec,
|
|
28
25
|
LayoutStrategy,
|
|
26
|
+
LegendLayout,
|
|
29
27
|
Mark,
|
|
30
28
|
ResolvedAnimation,
|
|
31
29
|
ResolvedAnnotation,
|
|
32
30
|
ResolvedTheme,
|
|
33
31
|
TableLayout,
|
|
34
|
-
TimeUnit,
|
|
35
|
-
TimeUnitTransform,
|
|
36
|
-
Transform,
|
|
37
32
|
} from '@opendata-ai/openchart-core';
|
|
38
33
|
import {
|
|
39
34
|
adaptTheme,
|
|
40
35
|
computeLabelBounds,
|
|
36
|
+
footnoteBandHeight,
|
|
41
37
|
generateAltText,
|
|
42
38
|
generateDataTable,
|
|
43
39
|
getBreakpoint,
|
|
44
40
|
getHeightClass,
|
|
45
41
|
getLayoutStrategy,
|
|
42
|
+
isAxislessMark,
|
|
46
43
|
resolveTheme,
|
|
47
44
|
} from '@opendata-ai/openchart-core';
|
|
48
45
|
import { computeAnnotations } from './annotations/compute';
|
|
@@ -64,8 +61,12 @@ import { getChartRenderer } from './charts/registry';
|
|
|
64
61
|
import { groupByField } from './charts/utils';
|
|
65
62
|
import { applyColorScaleRange } from './compile/color-scale-range';
|
|
66
63
|
import { filterClippedDomains } from './compile/data-clip';
|
|
64
|
+
import { applyFillPatterns } from './compile/fill-patterns';
|
|
67
65
|
import { compileLayer as compileLayerImpl } from './compile/layer';
|
|
66
|
+
import { emitSpecWarnings, expandSpecSugar } from './compile/spec-sugar';
|
|
68
67
|
import { computeWatermarkObstacle } from './compile/watermark-obstacle';
|
|
68
|
+
import { resolveYouDrawIt } from './compile/you-draw-it';
|
|
69
|
+
import { collectContrastWarnings } from './compiler/a11y-warnings';
|
|
69
70
|
import { resolveAnimation } from './compiler/animation';
|
|
70
71
|
import { compile as compileSpec } from './compiler/index';
|
|
71
72
|
import {
|
|
@@ -75,15 +76,17 @@ import {
|
|
|
75
76
|
trendColor,
|
|
76
77
|
} from './compiler/sparkline-defaults';
|
|
77
78
|
import type { NormalizedChartSpec, NormalizedTableSpec } from './compiler/types';
|
|
79
|
+
import { compileMap as compileMapImpl } from './geo/compile-map';
|
|
78
80
|
import { compileGraph as compileGraphImpl } from './graphs/compile-graph';
|
|
79
81
|
import type { GraphCompilation } from './graphs/types';
|
|
80
82
|
import { computeAxes } from './layout/axes';
|
|
81
83
|
import { computeDimensions } from './layout/dimensions';
|
|
82
|
-
import { computeFacetGrid } from './layout/facet';
|
|
84
|
+
import { computeFacetGrid, facetMinHeight, MIN_PANEL_HEIGHT } from './layout/facet';
|
|
83
85
|
import { computeGridlines } from './layout/gridlines';
|
|
84
86
|
import { createMeasureFn, resolveLayoutPlan } from './layout/plan';
|
|
85
87
|
import { computeScales } from './layout/scales';
|
|
86
88
|
import { placeLegend } from './legend/compute';
|
|
89
|
+
import { placeSizeLegend } from './legend/size';
|
|
87
90
|
import { compileSankey as compileSankeyImpl } from './sankey/compile-sankey';
|
|
88
91
|
import { compileTableLayout } from './tables/compile-table';
|
|
89
92
|
import { compileTileMap as compileTileMapImpl } from './tilemap/compile-tilemap';
|
|
@@ -91,76 +94,26 @@ import { computeTooltipDescriptors } from './tooltips/compute';
|
|
|
91
94
|
import { runTransforms } from './transforms';
|
|
92
95
|
|
|
93
96
|
// ---------------------------------------------------------------------------
|
|
94
|
-
//
|
|
97
|
+
// Spec sugar expansion (VL idioms, bin/timeUnit, deprecation warnings)
|
|
95
98
|
// ---------------------------------------------------------------------------
|
|
96
99
|
|
|
100
|
+
// Implementation lives in ./compile/spec-sugar; re-exported here so existing
|
|
101
|
+
// deep imports (`import { expandEncodingSugar } from '.../compile'`) keep working.
|
|
102
|
+
export { expandEncodingSugar, expandSpecSugar } from './compile/spec-sugar';
|
|
103
|
+
|
|
97
104
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* This function detects those shorthands, generates the corresponding transforms,
|
|
102
|
-
* updates encoding field references to the output field names, and prepends the
|
|
103
|
-
* transforms to the spec's transform array.
|
|
104
|
-
*
|
|
105
|
-
* Mutates nothing; returns a new spec object (shallow copy).
|
|
105
|
+
* Apply top-level spec `width`/`height` (VL fixed-size sugar) as overrides on
|
|
106
|
+
* the compile options. Returns the options untouched when the spec carries no
|
|
107
|
+
* size.
|
|
106
108
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const generatedTransforms: Transform[] = [];
|
|
112
|
-
const updatedEncoding = { ...encoding };
|
|
113
|
-
let changed = false;
|
|
114
|
-
|
|
115
|
-
for (const channel of Object.keys(encoding)) {
|
|
116
|
-
const ch = encoding[channel];
|
|
117
|
-
if (!ch || !ch.field) continue;
|
|
118
|
-
|
|
119
|
-
// Expand bin shorthand
|
|
120
|
-
if (ch.bin != null && ch.bin !== false) {
|
|
121
|
-
const field = ch.field;
|
|
122
|
-
const outputField = `bin_${field}`;
|
|
123
|
-
const binTransform: BinTransform = {
|
|
124
|
-
bin: ch.bin === true ? true : (ch.bin as BinParams),
|
|
125
|
-
field,
|
|
126
|
-
as: outputField,
|
|
127
|
-
};
|
|
128
|
-
generatedTransforms.push(binTransform);
|
|
129
|
-
|
|
130
|
-
// Update encoding to reference binned output field, remove bin property
|
|
131
|
-
const { bin: _bin, ...rest } = ch;
|
|
132
|
-
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
133
|
-
changed = true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Expand timeUnit shorthand (read from updated encoding in case bin already ran)
|
|
137
|
-
const current = updatedEncoding[channel] ?? ch;
|
|
138
|
-
if (current.timeUnit) {
|
|
139
|
-
const field = current.field;
|
|
140
|
-
const unit = current.timeUnit as TimeUnit;
|
|
141
|
-
const outputField = `${unit}_${field}`;
|
|
142
|
-
const timeUnitTransform: TimeUnitTransform = {
|
|
143
|
-
timeUnit: unit,
|
|
144
|
-
field,
|
|
145
|
-
as: outputField,
|
|
146
|
-
};
|
|
147
|
-
generatedTransforms.push(timeUnitTransform);
|
|
148
|
-
|
|
149
|
-
// Update encoding to reference timeUnit output field, remove timeUnit property
|
|
150
|
-
const { timeUnit: _tu, ...rest } = current;
|
|
151
|
-
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
152
|
-
changed = true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!changed) return spec;
|
|
157
|
-
|
|
158
|
-
// Prepend generated transforms before any user-defined transforms
|
|
159
|
-
const existingTransforms = (spec.transform as Transform[] | undefined) ?? [];
|
|
109
|
+
function applySpecSize(spec: unknown, options: CompileOptions): CompileOptions {
|
|
110
|
+
if (!spec || typeof spec !== 'object') return options;
|
|
111
|
+
const sized = spec as Record<string, unknown>;
|
|
112
|
+
if (typeof sized.width !== 'number' && typeof sized.height !== 'number') return options;
|
|
160
113
|
return {
|
|
161
|
-
...
|
|
162
|
-
|
|
163
|
-
|
|
114
|
+
...options,
|
|
115
|
+
width: typeof sized.width === 'number' ? sized.width : options.width,
|
|
116
|
+
height: typeof sized.height === 'number' ? sized.height : options.height,
|
|
164
117
|
};
|
|
165
118
|
}
|
|
166
119
|
|
|
@@ -168,6 +121,15 @@ export function expandEncodingSugar(spec: Record<string, unknown>): Record<strin
|
|
|
168
121
|
// Chart compilation
|
|
169
122
|
// ---------------------------------------------------------------------------
|
|
170
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Cap on footnote-reserve convergence passes in compileChart. Reserving space
|
|
126
|
+
* for the footnote list shrinks the plot, which can demote one more label and
|
|
127
|
+
* grow the list; the count only ever grows and is bounded by the annotation
|
|
128
|
+
* count, so this terminates on its own. The cap is a guard against a
|
|
129
|
+
* measurement quirk oscillating, not the normal exit condition.
|
|
130
|
+
*/
|
|
131
|
+
const MAX_FOOTNOTE_PASSES = 3;
|
|
132
|
+
|
|
171
133
|
/**
|
|
172
134
|
* Inject sparkline-mode visual defaults that depend on the resolved theme.
|
|
173
135
|
*
|
|
@@ -233,20 +195,32 @@ function applySparklineDefaults(
|
|
|
233
195
|
* ChartLayout with positions, colors, and marks ready for rendering.
|
|
234
196
|
*
|
|
235
197
|
* @param spec - Raw chart spec (validated and normalized internally).
|
|
236
|
-
* @param
|
|
198
|
+
* @param optionsInput - Compile options (width, height, theme, darkMode).
|
|
199
|
+
* Top-level spec width/height, when present, override the option values.
|
|
237
200
|
* @returns ChartLayout with all computed positions.
|
|
238
201
|
* @throws Error if spec is invalid or not a chart type.
|
|
239
202
|
*/
|
|
240
|
-
export function compileChart(spec: unknown,
|
|
241
|
-
// Expand encoding-level
|
|
242
|
-
//
|
|
203
|
+
export function compileChart(spec: unknown, optionsInput: CompileOptions): ChartLayout {
|
|
204
|
+
// Expand VL-idiom and encoding-level sugar (data {values}, top-level title,
|
|
205
|
+
// sort-by-value, value defs, scheme names, theta, bin/timeUnit, ...) before
|
|
206
|
+
// validation + normalization, surfacing any deprecation warnings.
|
|
207
|
+
const sugarWarnings: string[] = [];
|
|
243
208
|
const expandedSpec =
|
|
244
209
|
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
245
|
-
?
|
|
210
|
+
? expandSpecSugar(spec as Record<string, unknown>, sugarWarnings)
|
|
246
211
|
: spec;
|
|
247
212
|
|
|
248
|
-
//
|
|
249
|
-
|
|
213
|
+
// Top-level width/height are fixed-size overrides (VL alignment): the
|
|
214
|
+
// authored size wins over the container-derived compile options.
|
|
215
|
+
const options = applySpecSize(expandedSpec, optionsInput);
|
|
216
|
+
|
|
217
|
+
// Validate + normalize. Normalize-stage warnings (type mismatches,
|
|
218
|
+
// beeswarm point budget, sparkline mark advice) surface through the same
|
|
219
|
+
// console channel as the sugar warnings. Emit both together in one call so
|
|
220
|
+
// dedup spans the whole compile: a message produced by both a sugar pass and
|
|
221
|
+
// a normalize pass prints once, not once per stage.
|
|
222
|
+
const { spec: normalized, warnings: normalizeWarnings } = compileSpec(expandedSpec);
|
|
223
|
+
emitSpecWarnings([...sugarWarnings, ...normalizeWarnings], options.onWarn);
|
|
250
224
|
|
|
251
225
|
if ('type' in normalized && (normalized as unknown as Record<string, unknown>).type === 'table') {
|
|
252
226
|
throw new Error('compileChart received a table spec. Use compileTable instead.');
|
|
@@ -260,6 +234,9 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
260
234
|
) {
|
|
261
235
|
throw new Error('compileChart received a sankey spec. Use compileSankey instead.');
|
|
262
236
|
}
|
|
237
|
+
if ('type' in normalized && (normalized as unknown as Record<string, unknown>).type === 'map') {
|
|
238
|
+
throw new Error('compileChart received a map spec. Use compileMap instead.');
|
|
239
|
+
}
|
|
263
240
|
|
|
264
241
|
let chartSpec = normalized as NormalizedChartSpec;
|
|
265
242
|
|
|
@@ -607,6 +584,8 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
607
584
|
watermark,
|
|
608
585
|
resolvedAnimation,
|
|
609
586
|
crosshair,
|
|
587
|
+
spec,
|
|
588
|
+
optionsInput,
|
|
610
589
|
);
|
|
611
590
|
}
|
|
612
591
|
|
|
@@ -651,6 +630,15 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
651
630
|
dims.effectiveAxisGap,
|
|
652
631
|
);
|
|
653
632
|
|
|
633
|
+
// Every legend on the chart. The color legend is primary (and stays in the
|
|
634
|
+
// singular `legend` slot); a size legend joins it when a quantitative `size`
|
|
635
|
+
// channel is encoded. Consumers that reserve space or hit-test must iterate
|
|
636
|
+
// this rather than read `legend` -- see ChartLayout.legends.
|
|
637
|
+
const sizeLegend = plan.sizeLegendContent
|
|
638
|
+
? placeSizeLegend(plan.sizeLegendContent, chartArea, theme, finalLegend)
|
|
639
|
+
: null;
|
|
640
|
+
const allLegends: LegendLayout[] = [finalLegend, ...(sizeLegend ? [sizeLegend] : [])];
|
|
641
|
+
|
|
654
642
|
// Inline y-axis labels: inset the x-scale range start by the widest tick
|
|
655
643
|
// label width so data marks start to the right of the labels. The plan
|
|
656
644
|
// already measured the real tick labels.
|
|
@@ -677,8 +665,16 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
677
665
|
scales.defaultColor =
|
|
678
666
|
chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
679
667
|
|
|
680
|
-
//
|
|
681
|
-
|
|
668
|
+
// Dev-mode WCAG contrast diagnostics. Host-gated via options.dev (never an
|
|
669
|
+
// env sniff; the engine is isomorphic). Advisory console.warn only.
|
|
670
|
+
if (options.dev) {
|
|
671
|
+
emitSpecWarnings(collectContrastWarnings(scales, chartSpec.markType, theme), options.onWarn);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Axisless marks (arc, waffle, calendar, parliament) don't use axes or
|
|
675
|
+
// gridlines: arcs and parliament hemicycles are radial, waffles and calendars
|
|
676
|
+
// compute their own grid geometry.
|
|
677
|
+
const isRadial = isAxislessMark(chartSpec.markType);
|
|
682
678
|
|
|
683
679
|
// Compute axes (skip for radial charts).
|
|
684
680
|
// Sparkline mode skips axes by default unless the user explicitly opted into
|
|
@@ -718,6 +714,18 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
718
714
|
? renderer(renderSpec, scales, chartArea, strategy, theme, options.width)
|
|
719
715
|
: [];
|
|
720
716
|
|
|
717
|
+
// Opt-in per-series fill patterns (mark.fillPattern: 'auto'). Mutates
|
|
718
|
+
// filled marks in place; assignment is deterministic and theme-aware.
|
|
719
|
+
applyFillPatterns(marks, chartSpec.markDef, theme);
|
|
720
|
+
|
|
721
|
+
// Resolve "you draw it" geometry (pixel fromX, x-sample positions, target
|
|
722
|
+
// line color/key). Reads marks so it must run after they're computed.
|
|
723
|
+
const xFieldForYdi =
|
|
724
|
+
chartSpec.encoding.x && 'field' in chartSpec.encoding.x
|
|
725
|
+
? (chartSpec.encoding.x.field as string | undefined)
|
|
726
|
+
: undefined;
|
|
727
|
+
const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
|
|
728
|
+
|
|
721
729
|
// Compute the right-side endpoint labels column for multi-series line/area
|
|
722
730
|
// charts. Reads `mark.dataPoints` so it must run AFTER marks are computed.
|
|
723
731
|
// dimensions.ts already reserved the right margin via predictEndpointLabelsWidth.
|
|
@@ -765,6 +773,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
765
773
|
obstacles,
|
|
766
774
|
svg: { width: dims.total.width, height: dims.total.height },
|
|
767
775
|
measure: annotationMeasure,
|
|
776
|
+
fontFamily: theme.fonts.family,
|
|
768
777
|
autoThin: chartSpec.autoThin,
|
|
769
778
|
});
|
|
770
779
|
|
|
@@ -773,26 +782,67 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
773
782
|
// alignment with the resolved array is preserved.
|
|
774
783
|
let footnotes: import('@opendata-ai/openchart-core').AnnotationFootnote[] | undefined;
|
|
775
784
|
if (chartSpec.autoThin && annotations.length > 1) {
|
|
776
|
-
const result = thinAnnotations(
|
|
785
|
+
const result = thinAnnotations(
|
|
786
|
+
annotations,
|
|
787
|
+
chartSpec.annotations,
|
|
788
|
+
annotationMeasure,
|
|
789
|
+
chartArea,
|
|
790
|
+
{ x: 0, y: 0, width: dims.total.width, height: dims.total.height },
|
|
791
|
+
);
|
|
777
792
|
annotations = result.annotations;
|
|
778
793
|
if (result.footnotes.length > 0) {
|
|
779
794
|
footnotes = result.footnotes;
|
|
795
|
+
|
|
796
|
+
// The footnote list renders below the plot, but nothing reserved space for
|
|
797
|
+
// it — thinning only just now discovered how many lines there are. Recompile
|
|
798
|
+
// with that band carved out of the bottom margin so the list can't overrun
|
|
799
|
+
// the source row and the brand watermark.
|
|
800
|
+
//
|
|
801
|
+
// Reserving shrinks the plot, which can push a further label out of bounds
|
|
802
|
+
// and grow the list by one, so re-reserve until the count settles. The
|
|
803
|
+
// count only ever grows (a smaller plot never keeps more labels inline) and
|
|
804
|
+
// is bounded by the annotation count, so this terminates; the explicit cap
|
|
805
|
+
// is a belt-and-braces guard against a measurement quirk oscillating.
|
|
806
|
+
//
|
|
807
|
+
// A frozen area short-circuits all of this. `compileLayer` pins every
|
|
808
|
+
// leaf to the primary layout's coordinate space, and `chartArea` above
|
|
809
|
+
// takes `frozenChartArea` over anything `computeDimensions` produces --
|
|
810
|
+
// so a grown bottom margin is computed and then thrown away. The
|
|
811
|
+
// recursion would re-run the whole compile up to MAX_FOOTNOTE_PASSES
|
|
812
|
+
// times and return the identical layout. Skip it rather than pay for it.
|
|
813
|
+
const reserve = footnoteBandHeight(footnotes.length, theme);
|
|
814
|
+
const currentReserve = options.footnoteReserve ?? 0;
|
|
815
|
+
if (
|
|
816
|
+
!options.frozenChartArea &&
|
|
817
|
+
reserve > currentReserve &&
|
|
818
|
+
(options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES
|
|
819
|
+
) {
|
|
820
|
+
return compileChart(spec, {
|
|
821
|
+
...optionsInput,
|
|
822
|
+
footnoteReserve: reserve,
|
|
823
|
+
footnotePass: (options.footnotePass ?? 0) + 1,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
780
826
|
}
|
|
781
827
|
}
|
|
782
828
|
|
|
783
829
|
// Compute tooltip descriptors from marks and encoding
|
|
784
830
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
785
831
|
|
|
786
|
-
// Compute accessibility
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
832
|
+
// Compute accessibility. An author-provided description (a11y.description,
|
|
833
|
+
// or top-level `description` folded in by the sugar expansion) replaces the
|
|
834
|
+
// auto-generated alt text; auto-generation stays the fallback.
|
|
835
|
+
const altText =
|
|
836
|
+
chartSpec.a11y?.description ??
|
|
837
|
+
generateAltText(
|
|
838
|
+
{
|
|
839
|
+
mark: chartSpec.markType,
|
|
840
|
+
data: chartSpec.data,
|
|
841
|
+
encoding: chartSpec.encoding,
|
|
842
|
+
chrome: chartSpec.chrome,
|
|
843
|
+
} as ChartSpec,
|
|
844
|
+
chartSpec.data,
|
|
845
|
+
);
|
|
796
846
|
const dataTableFallback = generateDataTable(
|
|
797
847
|
{
|
|
798
848
|
mark: chartSpec.markType,
|
|
@@ -811,6 +861,8 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
811
861
|
area: chartArea,
|
|
812
862
|
chrome,
|
|
813
863
|
metrics: dims.metrics,
|
|
864
|
+
...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
|
|
865
|
+
...(youDrawIt ? { youDrawIt } : {}),
|
|
814
866
|
axes: {
|
|
815
867
|
x: axes.x,
|
|
816
868
|
y: axes.y,
|
|
@@ -818,6 +870,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
818
870
|
marks,
|
|
819
871
|
annotations,
|
|
820
872
|
legend: finalLegend,
|
|
873
|
+
legends: allLegends,
|
|
821
874
|
...(endpointLabels.entries.length > 0 ? { endpointLabels } : {}),
|
|
822
875
|
tooltipDescriptors,
|
|
823
876
|
a11y: {
|
|
@@ -825,6 +878,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
825
878
|
dataTableFallback,
|
|
826
879
|
role: 'img',
|
|
827
880
|
keyboardNavigable: marks.length > 0,
|
|
881
|
+
...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
|
|
828
882
|
},
|
|
829
883
|
theme,
|
|
830
884
|
dimensions: {
|
|
@@ -853,6 +907,8 @@ function compileFaceted(
|
|
|
853
907
|
watermark: boolean,
|
|
854
908
|
resolvedAnimation: ResolvedAnimation | undefined,
|
|
855
909
|
crosshair: boolean,
|
|
910
|
+
rawSpec: unknown,
|
|
911
|
+
optionsInput: CompileOptions,
|
|
856
912
|
): ChartLayout {
|
|
857
913
|
const measure = createMeasureFn(options.measureText);
|
|
858
914
|
|
|
@@ -922,6 +978,23 @@ function compileFaceted(
|
|
|
922
978
|
bottom: bottomReservation,
|
|
923
979
|
});
|
|
924
980
|
|
|
981
|
+
// If panels are too short, grow the compile height and restart (once).
|
|
982
|
+
if (
|
|
983
|
+
!optionsInput.facetHeightGrown &&
|
|
984
|
+
grid.panels.length > 0 &&
|
|
985
|
+
grid.panels[0].area.height < MIN_PANEL_HEIGHT
|
|
986
|
+
) {
|
|
987
|
+
const neededChartHeight = facetMinHeight(facetValues.length, grid.columns);
|
|
988
|
+
const heightGrowth = neededChartHeight - chartArea.height;
|
|
989
|
+
if (heightGrowth > 0) {
|
|
990
|
+
return compileChart(rawSpec, {
|
|
991
|
+
...optionsInput,
|
|
992
|
+
height: options.height + heightGrowth,
|
|
993
|
+
facetHeightGrown: true,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
925
998
|
// Resolve scale for determining if we use 'independent' or 'shared'
|
|
926
999
|
const resolveConfig = chartSpec.resolve;
|
|
927
1000
|
const yResolve = resolveConfig?.scale?.y ?? 'shared';
|
|
@@ -934,7 +1007,7 @@ function compileFaceted(
|
|
|
934
1007
|
const sharedYDomain =
|
|
935
1008
|
yResolve === 'shared' && fullScales.y ? fullScales.y.scale.domain() : undefined;
|
|
936
1009
|
|
|
937
|
-
const isRadial = chartSpec.markType
|
|
1010
|
+
const isRadial = isAxislessMark(chartSpec.markType);
|
|
938
1011
|
const rendererKey = resolveRendererKey(
|
|
939
1012
|
renderSpec.markType,
|
|
940
1013
|
panelEncoding as Encoding,
|
|
@@ -944,6 +1017,11 @@ function compileFaceted(
|
|
|
944
1017
|
|
|
945
1018
|
const allMarks: Mark[] = [];
|
|
946
1019
|
const panelLayouts: FacetPanelLayout[] = [];
|
|
1020
|
+
// Footnotes pool across panels into one figure-level list (the renderer draws
|
|
1021
|
+
// a single list below the grid from `chrome.footnotes`), so the numbering is
|
|
1022
|
+
// continuous and each panel's markers index into it.
|
|
1023
|
+
const allFootnotes: import('@opendata-ai/openchart-core').AnnotationFootnote[] = [];
|
|
1024
|
+
const footnoteNumberBySpecIndex = new Map<number, number>();
|
|
947
1025
|
|
|
948
1026
|
for (const gridPanel of grid.panels) {
|
|
949
1027
|
const panelData = groups.get(gridPanel.key) ?? [];
|
|
@@ -976,18 +1054,22 @@ function compileFaceted(
|
|
|
976
1054
|
panelScales.defaultColor =
|
|
977
1055
|
chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
978
1056
|
|
|
979
|
-
//
|
|
980
|
-
//
|
|
1057
|
+
// Dev-mode contrast diagnostics: panels share one palette resolution, so
|
|
1058
|
+
// checking the first panel covers the figure without repeating warnings.
|
|
1059
|
+
if (options.dev && panelLayouts.length === 0) {
|
|
1060
|
+
emitSpecWarnings(
|
|
1061
|
+
collectContrastWarnings(panelScales, chartSpec.markType, theme),
|
|
1062
|
+
options.onWarn,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
981
1066
|
const isLeftCol = gridPanel.col === 0;
|
|
982
|
-
const isBottomRow = gridPanel.row === grid.rows - 1;
|
|
983
1067
|
|
|
984
1068
|
const panelAxes = isRadial
|
|
985
1069
|
? { x: undefined, y: undefined }
|
|
986
1070
|
: computeAxes(panelScales, gridPanel.area, strategy, theme, options.measureText, {
|
|
987
1071
|
data: panelData,
|
|
988
1072
|
encoding: panelSpecWithDomains.encoding as Encoding,
|
|
989
|
-
skipX: xResolve === 'shared' ? !isBottomRow : false,
|
|
990
|
-
skipY: yResolve === 'shared' ? !isLeftCol : false,
|
|
991
1073
|
markType: chartSpec.markType,
|
|
992
1074
|
totalWidth: gridPanel.area.width,
|
|
993
1075
|
});
|
|
@@ -996,6 +1078,18 @@ function compileFaceted(
|
|
|
996
1078
|
computeGridlines(panelAxes, gridPanel.area);
|
|
997
1079
|
}
|
|
998
1080
|
|
|
1081
|
+
// Strip y-axis tick labels from non-leftmost panels (gridlines stay).
|
|
1082
|
+
// X-axis labels show on every panel for readability.
|
|
1083
|
+
if (yResolve === 'shared' && !isLeftCol && panelAxes.y) {
|
|
1084
|
+
panelAxes.y = {
|
|
1085
|
+
...panelAxes.y,
|
|
1086
|
+
ticks: [],
|
|
1087
|
+
label: undefined,
|
|
1088
|
+
domainLine: false,
|
|
1089
|
+
tickMarks: false,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
999
1093
|
// Compute marks for this panel
|
|
1000
1094
|
const panelMarks: Mark[] = renderer
|
|
1001
1095
|
? renderer(
|
|
@@ -1012,7 +1106,7 @@ function compileFaceted(
|
|
|
1012
1106
|
const annotationMeasure: AnnotationMeasureTextFn = options.measureText
|
|
1013
1107
|
? (text, font) => options.measureText!(text, font.fontSize, font.fontWeight).width
|
|
1014
1108
|
: heuristicMeasure;
|
|
1015
|
-
|
|
1109
|
+
let panelAnnotations = computeAnnotations(panelSpecWithDomains, {
|
|
1016
1110
|
scales: panelScales,
|
|
1017
1111
|
chartArea: gridPanel.area,
|
|
1018
1112
|
strategy,
|
|
@@ -1020,9 +1114,55 @@ function compileFaceted(
|
|
|
1020
1114
|
obstacles: [],
|
|
1021
1115
|
svg: { width: options.width, height: options.height },
|
|
1022
1116
|
measure: annotationMeasure,
|
|
1117
|
+
fontFamily: theme.fonts.family,
|
|
1023
1118
|
autoThin: panelSpecWithDomains.autoThin,
|
|
1024
1119
|
});
|
|
1025
1120
|
|
|
1121
|
+
// Auto-thinning, per panel. A panel is its own little chart: the box a label
|
|
1122
|
+
// must stay inside is the panel, not the figure. Thinning against the whole
|
|
1123
|
+
// SVG (what the single-chart path passes) would let a label from the left
|
|
1124
|
+
// column sprawl across its neighbour and still count as "fits".
|
|
1125
|
+
//
|
|
1126
|
+
// The spec's annotations resolve into *every* panel, so the same annotation
|
|
1127
|
+
// can demote in more than one. The footnote list is figure-level and its text
|
|
1128
|
+
// is per-spec-annotation, so number by spec index and reuse that number in
|
|
1129
|
+
// every panel that demoted it -- otherwise the list repeats itself once per
|
|
1130
|
+
// panel.
|
|
1131
|
+
if (chartSpec.autoThin && panelAnnotations.length > 1) {
|
|
1132
|
+
const thinned = thinAnnotations(
|
|
1133
|
+
panelAnnotations,
|
|
1134
|
+
chartSpec.annotations,
|
|
1135
|
+
annotationMeasure,
|
|
1136
|
+
gridPanel.area,
|
|
1137
|
+
gridPanel.area,
|
|
1138
|
+
);
|
|
1139
|
+
panelAnnotations = thinned.annotations;
|
|
1140
|
+
// `thinAnnotations` numbers footnotes 1..n *within the panel it was given*,
|
|
1141
|
+
// so restamp each demoted annotation with its figure-level number. Take the
|
|
1142
|
+
// text from `thinned.footnotes` (the 1-based panel-local index it just
|
|
1143
|
+
// assigned) rather than re-reading `label.text`: thinning owns that string,
|
|
1144
|
+
// and re-deriving it here would be a second producer to drift out of sync.
|
|
1145
|
+
//
|
|
1146
|
+
// Key on `specIndex`, NOT on position: with `resolve.scale.x: 'independent'`
|
|
1147
|
+
// each panel derives its own domain, so an annotation can resolve in one
|
|
1148
|
+
// panel and be dropped in another. Position i then means a different spec
|
|
1149
|
+
// annotation in each panel, and the markers point at other panels' text.
|
|
1150
|
+
for (const a of panelAnnotations) {
|
|
1151
|
+
const panelIndex = a.footnoteIndex;
|
|
1152
|
+
if (panelIndex == null || a.specIndex === undefined) continue;
|
|
1153
|
+
let num = footnoteNumberBySpecIndex.get(a.specIndex);
|
|
1154
|
+
if (num === undefined) {
|
|
1155
|
+
num = allFootnotes.length + 1;
|
|
1156
|
+
footnoteNumberBySpecIndex.set(a.specIndex, num);
|
|
1157
|
+
allFootnotes.push({
|
|
1158
|
+
index: num,
|
|
1159
|
+
text: thinned.footnotes[panelIndex - 1]?.text ?? '',
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
a.footnoteIndex = num;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1026
1166
|
allMarks.push(...panelMarks);
|
|
1027
1167
|
|
|
1028
1168
|
panelLayouts.push({
|
|
@@ -1041,10 +1181,37 @@ function compileFaceted(
|
|
|
1041
1181
|
});
|
|
1042
1182
|
}
|
|
1043
1183
|
|
|
1184
|
+
// The footnote list renders below the grid, but nothing reserved space for it
|
|
1185
|
+
// -- thinning only just now discovered how many lines there are. Recompile with
|
|
1186
|
+
// that band carved out of the bottom margin, same contract as the single-chart
|
|
1187
|
+
// path (see the matching block in compileChart). computeDimensions applies
|
|
1188
|
+
// options.footnoteReserve generically, so the facet grid shrinks with it.
|
|
1189
|
+
//
|
|
1190
|
+
// A frozen area short-circuits this, same as the single-chart path: compileLayer
|
|
1191
|
+
// pins every leaf to the primary layout's coordinate space, so the grown bottom
|
|
1192
|
+
// margin is computed and then thrown away. Recursing would re-run the whole
|
|
1193
|
+
// compile up to MAX_FOOTNOTE_PASSES times for an identical layout.
|
|
1194
|
+
if (allFootnotes.length > 0 && !options.frozenChartArea) {
|
|
1195
|
+
const reserve = footnoteBandHeight(allFootnotes.length, theme);
|
|
1196
|
+
const currentReserve = options.footnoteReserve ?? 0;
|
|
1197
|
+
if (reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
|
|
1198
|
+
return compileChart(rawSpec, {
|
|
1199
|
+
...optionsInput,
|
|
1200
|
+
footnoteReserve: reserve,
|
|
1201
|
+
footnotePass: (options.footnotePass ?? 0) + 1,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1044
1206
|
// Compute tooltip descriptors from all marks across panels
|
|
1045
1207
|
const tooltipDescriptors = computeTooltipDescriptors(panelChartSpec, allMarks);
|
|
1046
1208
|
|
|
1047
|
-
//
|
|
1209
|
+
// Opt-in per-series fill patterns across all panels at once so a series
|
|
1210
|
+
// gets the same pattern in every panel.
|
|
1211
|
+
applyFillPatterns(allMarks, chartSpec.markDef, theme);
|
|
1212
|
+
|
|
1213
|
+
// A11y: mention faceting in alt text. An author description is used
|
|
1214
|
+
// verbatim (the author already describes the whole figure).
|
|
1048
1215
|
const panelCount = facetValues.length;
|
|
1049
1216
|
const baseAltText = generateAltText(
|
|
1050
1217
|
{
|
|
@@ -1055,7 +1222,9 @@ function compileFaceted(
|
|
|
1055
1222
|
} as ChartSpec,
|
|
1056
1223
|
chartSpec.data,
|
|
1057
1224
|
);
|
|
1058
|
-
const altText =
|
|
1225
|
+
const altText =
|
|
1226
|
+
chartSpec.a11y?.description ??
|
|
1227
|
+
`${baseAltText} Faceted into ${panelCount} panels by ${facetChannel.field}.`;
|
|
1059
1228
|
|
|
1060
1229
|
const dataTableFallback = generateDataTable(
|
|
1061
1230
|
{
|
|
@@ -1073,18 +1242,23 @@ function compileFaceted(
|
|
|
1073
1242
|
// Figure-level marks are the union of all panel marks (for tooltip/keyboard nav)
|
|
1074
1243
|
return {
|
|
1075
1244
|
area: chartArea,
|
|
1076
|
-
chrome: dims.chrome,
|
|
1245
|
+
chrome: allFootnotes.length > 0 ? { ...dims.chrome, footnotes: allFootnotes } : dims.chrome,
|
|
1077
1246
|
metrics: dims.metrics,
|
|
1247
|
+
...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
|
|
1078
1248
|
axes: { x: undefined, y: undefined },
|
|
1079
1249
|
marks: allMarks,
|
|
1080
1250
|
annotations: [],
|
|
1081
1251
|
legend: finalLegend,
|
|
1252
|
+
// Facets share one color legend across the grid; the size channel isn't
|
|
1253
|
+
// supported in facet specs, so there is no second legend to carry.
|
|
1254
|
+
legends: [finalLegend],
|
|
1082
1255
|
tooltipDescriptors,
|
|
1083
1256
|
a11y: {
|
|
1084
1257
|
altText,
|
|
1085
1258
|
dataTableFallback,
|
|
1086
1259
|
role: 'img',
|
|
1087
1260
|
keyboardNavigable: allMarks.length > 0,
|
|
1261
|
+
...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
|
|
1088
1262
|
},
|
|
1089
1263
|
theme,
|
|
1090
1264
|
dimensions: { width: options.width, height: options.height },
|
|
@@ -1107,7 +1281,17 @@ function compileFaceted(
|
|
|
1107
1281
|
// ---------------------------------------------------------------------------
|
|
1108
1282
|
|
|
1109
1283
|
export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLayout {
|
|
1110
|
-
|
|
1284
|
+
// Expand VL-idiom sugar on the layer spec and every child up front. Leaf
|
|
1285
|
+
// compiles re-run the (idempotent) expansion after encoding inheritance;
|
|
1286
|
+
// warning triggers are stripped or stamped here so each warns exactly once.
|
|
1287
|
+
const sugarWarnings: string[] = [];
|
|
1288
|
+
const expanded = expandSpecSugar(
|
|
1289
|
+
spec as unknown as Record<string, unknown>,
|
|
1290
|
+
sugarWarnings,
|
|
1291
|
+
) as unknown as LayerSpec;
|
|
1292
|
+
emitSpecWarnings(sugarWarnings, options.onWarn);
|
|
1293
|
+
const resolvedOptions = applySpecSize(expanded, options);
|
|
1294
|
+
return compileLayerImpl(expanded, resolvedOptions, compileChart);
|
|
1111
1295
|
}
|
|
1112
1296
|
|
|
1113
1297
|
// ---------------------------------------------------------------------------
|
|
@@ -1219,6 +1403,29 @@ export function compileTileMap(
|
|
|
1219
1403
|
return compileTileMapImpl(spec, options);
|
|
1220
1404
|
}
|
|
1221
1405
|
|
|
1406
|
+
// ---------------------------------------------------------------------------
|
|
1407
|
+
// Map compilation
|
|
1408
|
+
// ---------------------------------------------------------------------------
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Compile a map spec into a MapLayout.
|
|
1412
|
+
*
|
|
1413
|
+
* Takes a raw map spec, validates, normalizes, resolves theme and chrome,
|
|
1414
|
+
* projects geo features, joins data, builds feature marks with color fills,
|
|
1415
|
+
* and returns a MapLayout ready for rendering.
|
|
1416
|
+
*
|
|
1417
|
+
* @param spec - Raw map spec (validated and normalized internally).
|
|
1418
|
+
* @param options - Compile options (width, height, theme, darkMode).
|
|
1419
|
+
* @returns MapLayout with computed positions and visual properties.
|
|
1420
|
+
* @throws Error if spec is invalid or not a map type.
|
|
1421
|
+
*/
|
|
1422
|
+
export function compileMap(
|
|
1423
|
+
spec: unknown,
|
|
1424
|
+
options: CompileOptions,
|
|
1425
|
+
): import('@opendata-ai/openchart-core').MapLayout {
|
|
1426
|
+
return compileMapImpl(spec, options);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1222
1429
|
// ---------------------------------------------------------------------------
|
|
1223
1430
|
// BarList compilation
|
|
1224
1431
|
// ---------------------------------------------------------------------------
|