@opendata-ai/openchart-engine 7.10.0 → 8.0.0-rc.1
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 +142 -7
- package/dist/index.js +16182 -10032
- 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__/dimensions.test.ts +49 -4
- package/src/__tests__/helpers/invariants.ts +54 -20
- package/src/__tests__/known-bugs.test.ts +455 -2
- package/src/__tests__/legend.test.ts +120 -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 +315 -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__/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/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 +28 -2
- package/src/layout/axes/ticks.ts +59 -10
- package/src/layout/axes.ts +30 -2
- package/src/layout/dimensions.ts +178 -95
- package/src/layout/facet.ts +14 -3
- package/src/layout/plan.ts +86 -35
- 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 +126 -14
- package/src/legend/continuous.ts +327 -0
- package/src/legend/size.ts +407 -0
- package/src/legend/suppression.ts +8 -7
- package/src/legend/wrap.ts +8 -0
- 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
|
|
|
@@ -640,14 +619,26 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
640
619
|
const chartArea = options.frozenChartArea ?? dims.chartArea;
|
|
641
620
|
|
|
642
621
|
// Place legend in its final position relative to the computed chart area.
|
|
622
|
+
// effectiveAxisGap lifts a top legend above the inline-tick overhang zone
|
|
623
|
+
// the margin stack reserved below it.
|
|
643
624
|
const finalLegend = placeLegend(
|
|
644
625
|
plan.legendContent,
|
|
645
626
|
chartArea,
|
|
646
627
|
options.width,
|
|
647
628
|
theme,
|
|
648
629
|
plan.xAxisExtent,
|
|
630
|
+
dims.effectiveAxisGap,
|
|
649
631
|
);
|
|
650
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
|
+
|
|
651
642
|
// Inline y-axis labels: inset the x-scale range start by the widest tick
|
|
652
643
|
// label width so data marks start to the right of the labels. The plan
|
|
653
644
|
// already measured the real tick labels.
|
|
@@ -674,8 +665,16 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
674
665
|
scales.defaultColor =
|
|
675
666
|
chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
676
667
|
|
|
677
|
-
//
|
|
678
|
-
|
|
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);
|
|
679
678
|
|
|
680
679
|
// Compute axes (skip for radial charts).
|
|
681
680
|
// Sparkline mode skips axes by default unless the user explicitly opted into
|
|
@@ -715,6 +714,18 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
715
714
|
? renderer(renderSpec, scales, chartArea, strategy, theme, options.width)
|
|
716
715
|
: [];
|
|
717
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
|
+
|
|
718
729
|
// Compute the right-side endpoint labels column for multi-series line/area
|
|
719
730
|
// charts. Reads `mark.dataPoints` so it must run AFTER marks are computed.
|
|
720
731
|
// dimensions.ts already reserved the right margin via predictEndpointLabelsWidth.
|
|
@@ -762,6 +773,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
762
773
|
obstacles,
|
|
763
774
|
svg: { width: dims.total.width, height: dims.total.height },
|
|
764
775
|
measure: annotationMeasure,
|
|
776
|
+
fontFamily: theme.fonts.family,
|
|
765
777
|
autoThin: chartSpec.autoThin,
|
|
766
778
|
});
|
|
767
779
|
|
|
@@ -770,26 +782,67 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
770
782
|
// alignment with the resolved array is preserved.
|
|
771
783
|
let footnotes: import('@opendata-ai/openchart-core').AnnotationFootnote[] | undefined;
|
|
772
784
|
if (chartSpec.autoThin && annotations.length > 1) {
|
|
773
|
-
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
|
+
);
|
|
774
792
|
annotations = result.annotations;
|
|
775
793
|
if (result.footnotes.length > 0) {
|
|
776
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
|
+
}
|
|
777
826
|
}
|
|
778
827
|
}
|
|
779
828
|
|
|
780
829
|
// Compute tooltip descriptors from marks and encoding
|
|
781
830
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
782
831
|
|
|
783
|
-
// Compute accessibility
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
);
|
|
793
846
|
const dataTableFallback = generateDataTable(
|
|
794
847
|
{
|
|
795
848
|
mark: chartSpec.markType,
|
|
@@ -808,6 +861,8 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
808
861
|
area: chartArea,
|
|
809
862
|
chrome,
|
|
810
863
|
metrics: dims.metrics,
|
|
864
|
+
...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
|
|
865
|
+
...(youDrawIt ? { youDrawIt } : {}),
|
|
811
866
|
axes: {
|
|
812
867
|
x: axes.x,
|
|
813
868
|
y: axes.y,
|
|
@@ -815,6 +870,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
815
870
|
marks,
|
|
816
871
|
annotations,
|
|
817
872
|
legend: finalLegend,
|
|
873
|
+
legends: allLegends,
|
|
818
874
|
...(endpointLabels.entries.length > 0 ? { endpointLabels } : {}),
|
|
819
875
|
tooltipDescriptors,
|
|
820
876
|
a11y: {
|
|
@@ -822,6 +878,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
822
878
|
dataTableFallback,
|
|
823
879
|
role: 'img',
|
|
824
880
|
keyboardNavigable: marks.length > 0,
|
|
881
|
+
...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
|
|
825
882
|
},
|
|
826
883
|
theme,
|
|
827
884
|
dimensions: {
|
|
@@ -850,6 +907,8 @@ function compileFaceted(
|
|
|
850
907
|
watermark: boolean,
|
|
851
908
|
resolvedAnimation: ResolvedAnimation | undefined,
|
|
852
909
|
crosshair: boolean,
|
|
910
|
+
rawSpec: unknown,
|
|
911
|
+
optionsInput: CompileOptions,
|
|
853
912
|
): ChartLayout {
|
|
854
913
|
const measure = createMeasureFn(options.measureText);
|
|
855
914
|
|
|
@@ -898,12 +957,14 @@ function compileFaceted(
|
|
|
898
957
|
);
|
|
899
958
|
const chartArea = dims.chartArea;
|
|
900
959
|
|
|
960
|
+
// Same effectiveAxisGap threading as the regular (non-faceted) path.
|
|
901
961
|
const finalLegend = placeLegend(
|
|
902
962
|
plan.legendContent,
|
|
903
963
|
chartArea,
|
|
904
964
|
options.width,
|
|
905
965
|
theme,
|
|
906
966
|
plan.xAxisExtent,
|
|
967
|
+
dims.effectiveAxisGap,
|
|
907
968
|
);
|
|
908
969
|
|
|
909
970
|
// Estimate y-axis gutter width (used as left reservation for outer axes)
|
|
@@ -917,6 +978,23 @@ function compileFaceted(
|
|
|
917
978
|
bottom: bottomReservation,
|
|
918
979
|
});
|
|
919
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
|
+
|
|
920
998
|
// Resolve scale for determining if we use 'independent' or 'shared'
|
|
921
999
|
const resolveConfig = chartSpec.resolve;
|
|
922
1000
|
const yResolve = resolveConfig?.scale?.y ?? 'shared';
|
|
@@ -929,7 +1007,7 @@ function compileFaceted(
|
|
|
929
1007
|
const sharedYDomain =
|
|
930
1008
|
yResolve === 'shared' && fullScales.y ? fullScales.y.scale.domain() : undefined;
|
|
931
1009
|
|
|
932
|
-
const isRadial = chartSpec.markType
|
|
1010
|
+
const isRadial = isAxislessMark(chartSpec.markType);
|
|
933
1011
|
const rendererKey = resolveRendererKey(
|
|
934
1012
|
renderSpec.markType,
|
|
935
1013
|
panelEncoding as Encoding,
|
|
@@ -939,6 +1017,11 @@ function compileFaceted(
|
|
|
939
1017
|
|
|
940
1018
|
const allMarks: Mark[] = [];
|
|
941
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>();
|
|
942
1025
|
|
|
943
1026
|
for (const gridPanel of grid.panels) {
|
|
944
1027
|
const panelData = groups.get(gridPanel.key) ?? [];
|
|
@@ -971,18 +1054,22 @@ function compileFaceted(
|
|
|
971
1054
|
panelScales.defaultColor =
|
|
972
1055
|
chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
973
1056
|
|
|
974
|
-
//
|
|
975
|
-
//
|
|
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
|
+
|
|
976
1066
|
const isLeftCol = gridPanel.col === 0;
|
|
977
|
-
const isBottomRow = gridPanel.row === grid.rows - 1;
|
|
978
1067
|
|
|
979
1068
|
const panelAxes = isRadial
|
|
980
1069
|
? { x: undefined, y: undefined }
|
|
981
1070
|
: computeAxes(panelScales, gridPanel.area, strategy, theme, options.measureText, {
|
|
982
1071
|
data: panelData,
|
|
983
1072
|
encoding: panelSpecWithDomains.encoding as Encoding,
|
|
984
|
-
skipX: xResolve === 'shared' ? !isBottomRow : false,
|
|
985
|
-
skipY: yResolve === 'shared' ? !isLeftCol : false,
|
|
986
1073
|
markType: chartSpec.markType,
|
|
987
1074
|
totalWidth: gridPanel.area.width,
|
|
988
1075
|
});
|
|
@@ -991,6 +1078,18 @@ function compileFaceted(
|
|
|
991
1078
|
computeGridlines(panelAxes, gridPanel.area);
|
|
992
1079
|
}
|
|
993
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
|
+
|
|
994
1093
|
// Compute marks for this panel
|
|
995
1094
|
const panelMarks: Mark[] = renderer
|
|
996
1095
|
? renderer(
|
|
@@ -1007,7 +1106,7 @@ function compileFaceted(
|
|
|
1007
1106
|
const annotationMeasure: AnnotationMeasureTextFn = options.measureText
|
|
1008
1107
|
? (text, font) => options.measureText!(text, font.fontSize, font.fontWeight).width
|
|
1009
1108
|
: heuristicMeasure;
|
|
1010
|
-
|
|
1109
|
+
let panelAnnotations = computeAnnotations(panelSpecWithDomains, {
|
|
1011
1110
|
scales: panelScales,
|
|
1012
1111
|
chartArea: gridPanel.area,
|
|
1013
1112
|
strategy,
|
|
@@ -1015,9 +1114,55 @@ function compileFaceted(
|
|
|
1015
1114
|
obstacles: [],
|
|
1016
1115
|
svg: { width: options.width, height: options.height },
|
|
1017
1116
|
measure: annotationMeasure,
|
|
1117
|
+
fontFamily: theme.fonts.family,
|
|
1018
1118
|
autoThin: panelSpecWithDomains.autoThin,
|
|
1019
1119
|
});
|
|
1020
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
|
+
|
|
1021
1166
|
allMarks.push(...panelMarks);
|
|
1022
1167
|
|
|
1023
1168
|
panelLayouts.push({
|
|
@@ -1036,10 +1181,37 @@ function compileFaceted(
|
|
|
1036
1181
|
});
|
|
1037
1182
|
}
|
|
1038
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
|
+
|
|
1039
1206
|
// Compute tooltip descriptors from all marks across panels
|
|
1040
1207
|
const tooltipDescriptors = computeTooltipDescriptors(panelChartSpec, allMarks);
|
|
1041
1208
|
|
|
1042
|
-
//
|
|
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).
|
|
1043
1215
|
const panelCount = facetValues.length;
|
|
1044
1216
|
const baseAltText = generateAltText(
|
|
1045
1217
|
{
|
|
@@ -1050,7 +1222,9 @@ function compileFaceted(
|
|
|
1050
1222
|
} as ChartSpec,
|
|
1051
1223
|
chartSpec.data,
|
|
1052
1224
|
);
|
|
1053
|
-
const altText =
|
|
1225
|
+
const altText =
|
|
1226
|
+
chartSpec.a11y?.description ??
|
|
1227
|
+
`${baseAltText} Faceted into ${panelCount} panels by ${facetChannel.field}.`;
|
|
1054
1228
|
|
|
1055
1229
|
const dataTableFallback = generateDataTable(
|
|
1056
1230
|
{
|
|
@@ -1068,18 +1242,23 @@ function compileFaceted(
|
|
|
1068
1242
|
// Figure-level marks are the union of all panel marks (for tooltip/keyboard nav)
|
|
1069
1243
|
return {
|
|
1070
1244
|
area: chartArea,
|
|
1071
|
-
chrome: dims.chrome,
|
|
1245
|
+
chrome: allFootnotes.length > 0 ? { ...dims.chrome, footnotes: allFootnotes } : dims.chrome,
|
|
1072
1246
|
metrics: dims.metrics,
|
|
1247
|
+
...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
|
|
1073
1248
|
axes: { x: undefined, y: undefined },
|
|
1074
1249
|
marks: allMarks,
|
|
1075
1250
|
annotations: [],
|
|
1076
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],
|
|
1077
1255
|
tooltipDescriptors,
|
|
1078
1256
|
a11y: {
|
|
1079
1257
|
altText,
|
|
1080
1258
|
dataTableFallback,
|
|
1081
1259
|
role: 'img',
|
|
1082
1260
|
keyboardNavigable: allMarks.length > 0,
|
|
1261
|
+
...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
|
|
1083
1262
|
},
|
|
1084
1263
|
theme,
|
|
1085
1264
|
dimensions: { width: options.width, height: options.height },
|
|
@@ -1102,7 +1281,17 @@ function compileFaceted(
|
|
|
1102
1281
|
// ---------------------------------------------------------------------------
|
|
1103
1282
|
|
|
1104
1283
|
export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLayout {
|
|
1105
|
-
|
|
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);
|
|
1106
1295
|
}
|
|
1107
1296
|
|
|
1108
1297
|
// ---------------------------------------------------------------------------
|
|
@@ -1214,6 +1403,29 @@ export function compileTileMap(
|
|
|
1214
1403
|
return compileTileMapImpl(spec, options);
|
|
1215
1404
|
}
|
|
1216
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
|
+
|
|
1217
1429
|
// ---------------------------------------------------------------------------
|
|
1218
1430
|
// BarList compilation
|
|
1219
1431
|
// ---------------------------------------------------------------------------
|