@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.0.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 +149 -12
- package/dist/index.js +804 -155
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,758 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-validation spec sugar expansion.
|
|
3
|
-
*
|
|
4
|
-
* Accepts common Vega-Lite idioms alongside the canonical openchart forms and
|
|
5
|
-
* rewrites them into canonical shape BEFORE validateSpec runs (validation
|
|
6
|
-
* hard-rejects the VL forms, so sugar placed later would be dead code):
|
|
7
|
-
*
|
|
8
|
-
* - `data: { values: [...] }` unwraps to the bare array
|
|
9
|
-
* - top-level `title` / `subtitle` expand into `chrome` (chrome wins on conflict)
|
|
10
|
-
* - top-level `description` folds into `a11y.description` (a11y wins on conflict)
|
|
11
|
-
* - bare value defs (`color/size/opacity: { value }`) move to mark-level props
|
|
12
|
-
* - channel-level `legend: null | config` on color merges into the top-level legend
|
|
13
|
-
* - `axis: null` becomes `axis: false`
|
|
14
|
-
* - `scale: { scheme }` resolves to `scale.range` via the core palette registry
|
|
15
|
-
* - `theta` is the canonical arc/waffle/parliament value channel in v8; `y` on
|
|
16
|
-
* those marks is a deprecated alias, expanded into `theta` (and vice versa,
|
|
17
|
-
* since the engine still reads `y` internally) with a warning
|
|
18
|
-
* - annotation `type: 'rule'` is a deprecated alias for `'refline'`
|
|
19
|
-
* - `aggregate: 'count'` without a field desugars to an aggregate transform
|
|
20
|
-
* - VL sort forms (`'-y'`, value arrays, `{ field, op, order }`) resolve to an
|
|
21
|
-
* explicit categorical `scale.domain`
|
|
22
|
-
* - encoding-level `bin` / `timeUnit` desugar to transforms (expandEncodingSugar)
|
|
23
|
-
*
|
|
24
|
-
* It also emits deprecation warnings for spec surface removed in v8
|
|
25
|
-
* (`radius`, `shape`, `href`, `order`) and strips the triggering forms so
|
|
26
|
-
* each one warns exactly once per compile, even when layer leaves are
|
|
27
|
-
* re-expanded.
|
|
28
|
-
*
|
|
29
|
-
* Applied to top-level chart specs (compileChart) and to LayerSpec children
|
|
30
|
-
* (compileLayer) so every sugar works inside layers too.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import type {
|
|
34
|
-
AggregateOp,
|
|
35
|
-
AggregateTransform,
|
|
36
|
-
BinParams,
|
|
37
|
-
BinTransform,
|
|
38
|
-
DataRow,
|
|
39
|
-
EncodingChannel,
|
|
40
|
-
TimeUnit,
|
|
41
|
-
TimeUnitTransform,
|
|
42
|
-
Transform,
|
|
43
|
-
} from '@opendata-ai/openchart-core';
|
|
44
|
-
import { resolveSchemeName } from '@opendata-ai/openchart-core';
|
|
45
|
-
import { computeAggregate } from '../transforms/aggregate';
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Helpers
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
/** Resolve the mark type string from a raw (unvalidated) spec. */
|
|
52
|
-
function markTypeOf(spec: Record<string, unknown>): string | undefined {
|
|
53
|
-
const mark = spec.mark;
|
|
54
|
-
if (typeof mark === 'string') return mark;
|
|
55
|
-
if (mark && typeof mark === 'object' && !Array.isArray(mark)) {
|
|
56
|
-
return (mark as Record<string, unknown>).type as string | undefined;
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Unique string values of a data column, in data order, skipping nulls. */
|
|
62
|
-
function uniqueFieldStrings(data: DataRow[], field: string): string[] {
|
|
63
|
-
const seen = new Set<string>();
|
|
64
|
-
const out: string[] = [];
|
|
65
|
-
for (const row of data) {
|
|
66
|
-
const raw = row[field];
|
|
67
|
-
if (raw == null) continue;
|
|
68
|
-
const key = String(raw);
|
|
69
|
-
if (!seen.has(key)) {
|
|
70
|
-
seen.add(key);
|
|
71
|
-
out.push(key);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// Encoding sugar expansion (bin, timeUnit on encoding channels)
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Expand encoding-level `bin` and `timeUnit` shorthand into explicit transforms.
|
|
83
|
-
*
|
|
84
|
-
* Vega-Lite allows `encoding.x.bin: true` as sugar for a BinTransform.
|
|
85
|
-
* This function detects those shorthands, generates the corresponding transforms,
|
|
86
|
-
* updates encoding field references to the output field names, and prepends the
|
|
87
|
-
* transforms to the spec's transform array.
|
|
88
|
-
*
|
|
89
|
-
* Mutates nothing; returns a new spec object (shallow copy).
|
|
90
|
-
*/
|
|
91
|
-
export function expandEncodingSugar(spec: Record<string, unknown>): Record<string, unknown> {
|
|
92
|
-
const encoding = spec.encoding as Record<string, EncodingChannel | undefined> | undefined;
|
|
93
|
-
if (!encoding) return spec;
|
|
94
|
-
|
|
95
|
-
const generatedTransforms: Transform[] = [];
|
|
96
|
-
const updatedEncoding = { ...encoding };
|
|
97
|
-
let changed = false;
|
|
98
|
-
|
|
99
|
-
for (const channel of Object.keys(encoding)) {
|
|
100
|
-
const ch = encoding[channel];
|
|
101
|
-
if (!ch || !ch.field) continue;
|
|
102
|
-
|
|
103
|
-
// Expand bin shorthand
|
|
104
|
-
if (ch.bin != null && ch.bin !== false) {
|
|
105
|
-
const field = ch.field;
|
|
106
|
-
const outputField = `bin_${field}`;
|
|
107
|
-
const binTransform: BinTransform = {
|
|
108
|
-
bin: ch.bin === true ? true : (ch.bin as BinParams),
|
|
109
|
-
field,
|
|
110
|
-
as: outputField,
|
|
111
|
-
};
|
|
112
|
-
generatedTransforms.push(binTransform);
|
|
113
|
-
|
|
114
|
-
// Update encoding to reference binned output field, remove bin property
|
|
115
|
-
const { bin: _bin, ...rest } = ch;
|
|
116
|
-
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
117
|
-
changed = true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Expand timeUnit shorthand (read from updated encoding in case bin already ran)
|
|
121
|
-
const current = updatedEncoding[channel] ?? ch;
|
|
122
|
-
if (current.timeUnit) {
|
|
123
|
-
const field = current.field;
|
|
124
|
-
const unit = current.timeUnit as TimeUnit;
|
|
125
|
-
const outputField = `${unit}_${field}`;
|
|
126
|
-
const timeUnitTransform: TimeUnitTransform = {
|
|
127
|
-
timeUnit: unit,
|
|
128
|
-
field,
|
|
129
|
-
as: outputField,
|
|
130
|
-
};
|
|
131
|
-
generatedTransforms.push(timeUnitTransform);
|
|
132
|
-
|
|
133
|
-
// Update encoding to reference timeUnit output field, remove timeUnit property
|
|
134
|
-
const { timeUnit: _tu, ...rest } = current;
|
|
135
|
-
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
136
|
-
changed = true;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!changed) return spec;
|
|
141
|
-
|
|
142
|
-
// Prepend generated transforms before any user-defined transforms
|
|
143
|
-
const existingTransforms = (spec.transform as Transform[] | undefined) ?? [];
|
|
144
|
-
return {
|
|
145
|
-
...spec,
|
|
146
|
-
encoding: updatedEncoding,
|
|
147
|
-
transform: [...generatedTransforms, ...existingTransforms],
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// Top-level sugar (data wrapper, title, ignored keys, fixed size)
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
/** Unwrap the VL `data: { values: [...] }` object form to a bare array. */
|
|
156
|
-
function unwrapDataValues(spec: Record<string, unknown>): Record<string, unknown> {
|
|
157
|
-
const data = spec.data;
|
|
158
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
159
|
-
const values = (data as Record<string, unknown>).values;
|
|
160
|
-
if (Array.isArray(values)) {
|
|
161
|
-
return { ...spec, data: values };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return spec;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Expand the VL top-level `title` (string or `{ text, subtitle }`) and
|
|
169
|
-
* `subtitle` into `chrome`. Authored chrome keys win on conflict. Runs before
|
|
170
|
-
* breakpoint overrides are applied, so a per-breakpoint `overrides.<bp>.chrome`
|
|
171
|
-
* replaces the expanded chrome exactly as it replaces authored chrome.
|
|
172
|
-
*/
|
|
173
|
-
function expandTopLevelTitle(spec: Record<string, unknown>): Record<string, unknown> {
|
|
174
|
-
if (spec.title === undefined && spec.subtitle === undefined) return spec;
|
|
175
|
-
|
|
176
|
-
const expanded: Record<string, unknown> = {};
|
|
177
|
-
const title = spec.title;
|
|
178
|
-
if (typeof title === 'string') {
|
|
179
|
-
expanded.title = title;
|
|
180
|
-
} else if (title && typeof title === 'object' && !Array.isArray(title)) {
|
|
181
|
-
const t = title as { text?: unknown; subtitle?: unknown };
|
|
182
|
-
if (typeof t.text === 'string') expanded.title = t.text;
|
|
183
|
-
if (typeof t.subtitle === 'string') expanded.subtitle = t.subtitle;
|
|
184
|
-
}
|
|
185
|
-
if (typeof spec.subtitle === 'string') expanded.subtitle = spec.subtitle;
|
|
186
|
-
|
|
187
|
-
const { title: _title, subtitle: _subtitle, ...rest } = spec;
|
|
188
|
-
const chrome =
|
|
189
|
-
spec.chrome && typeof spec.chrome === 'object' ? (spec.chrome as Record<string, unknown>) : {};
|
|
190
|
-
return { ...rest, chrome: { ...expanded, ...chrome } };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Fold the top-level `description` (VL's alt-text field) into
|
|
195
|
-
* `a11y.description`. An authored `a11y.description` wins on conflict.
|
|
196
|
-
*/
|
|
197
|
-
function expandDescriptionSugar(spec: Record<string, unknown>): Record<string, unknown> {
|
|
198
|
-
if (typeof spec.description !== 'string') return spec;
|
|
199
|
-
const { description, ...rest } = spec;
|
|
200
|
-
const a11y =
|
|
201
|
-
spec.a11y && typeof spec.a11y === 'object' ? (spec.a11y as Record<string, unknown>) : {};
|
|
202
|
-
return { ...rest, a11y: { description, ...a11y } };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Keys accepted for VL compatibility but ignored; warned once and stripped. */
|
|
206
|
-
function stripIgnoredKeys(
|
|
207
|
-
spec: Record<string, unknown>,
|
|
208
|
-
warnings: string[],
|
|
209
|
-
): Record<string, unknown> {
|
|
210
|
-
if (!('$schema' in spec)) return spec;
|
|
211
|
-
warnings.push(
|
|
212
|
-
'[openchart] "$schema" is accepted for Vega-Lite compatibility but ignored; it can be removed.',
|
|
213
|
-
);
|
|
214
|
-
const { $schema: _schema, ...rest } = spec;
|
|
215
|
-
return rest;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Fixed-size specs (both `width` and `height` set) imply `responsive: false`
|
|
220
|
-
* unless the user set `responsive` explicitly. The width/height override
|
|
221
|
-
* itself is applied by compileChart/compileLayer on the compile options.
|
|
222
|
-
*/
|
|
223
|
-
function applyFixedSizeDefault(spec: Record<string, unknown>): Record<string, unknown> {
|
|
224
|
-
if (
|
|
225
|
-
typeof spec.width === 'number' &&
|
|
226
|
-
typeof spec.height === 'number' &&
|
|
227
|
-
spec.responsive === undefined
|
|
228
|
-
) {
|
|
229
|
-
return { ...spec, responsive: false };
|
|
230
|
-
}
|
|
231
|
-
return spec;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Rewrite the deprecated annotation `type: 'rule'` to the canonical
|
|
236
|
-
* `'refline'` (they collide with the `rule` mark type otherwise). Runs before
|
|
237
|
-
* validation so the canonical form is all validation/normalization ever sees.
|
|
238
|
-
*/
|
|
239
|
-
function expandAnnotationSugar(
|
|
240
|
-
spec: Record<string, unknown>,
|
|
241
|
-
warnings: string[],
|
|
242
|
-
): Record<string, unknown> {
|
|
243
|
-
const annotations = spec.annotations;
|
|
244
|
-
if (!Array.isArray(annotations) || annotations.length === 0) return spec;
|
|
245
|
-
|
|
246
|
-
let changed = false;
|
|
247
|
-
const updated = annotations.map((ann) => {
|
|
248
|
-
if (ann && typeof ann === 'object' && (ann as Record<string, unknown>).type === 'rule') {
|
|
249
|
-
warnings.push("[openchart] annotation type 'rule' is deprecated; use 'refline'.");
|
|
250
|
-
changed = true;
|
|
251
|
-
return { ...(ann as Record<string, unknown>), type: 'refline' };
|
|
252
|
-
}
|
|
253
|
-
return ann;
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
return changed ? { ...spec, annotations: updated } : spec;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Channel-level sugar (value defs, legend, axis null, scheme, theta, dead channels)
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
/** Channels declared in the spec types with zero engine implementation, warned and stripped. */
|
|
264
|
-
const DEAD_CHANNEL_MESSAGES: Record<string, string> = {
|
|
265
|
-
radius:
|
|
266
|
-
'[openchart] encoding.radius was removed in v8. This channel is stripped for backward compatibility. Use mark.innerRadius / mark.outerRadius to control donut radii.',
|
|
267
|
-
shape:
|
|
268
|
-
'[openchart] encoding.shape was removed in v8. This channel is stripped for backward compatibility. Differentiate series with encoding.color or encoding.strokeDash instead.',
|
|
269
|
-
href: '[openchart] encoding.href was removed in v8. This channel is stripped for backward compatibility. Handle link navigation in the host application instead.',
|
|
270
|
-
order:
|
|
271
|
-
'[openchart] encoding.order was removed in v8. This channel is stripped for backward compatibility. Use encoding.<channel>.sort or pre-sorted data order instead.',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Expand channel-level VL sugar. Mark-dependent sugar (value defs, theta) only
|
|
276
|
-
* runs when the spec has a mark, so the same function works on the shared
|
|
277
|
-
* encoding of a LayerSpec (which resolves those per leaf instead).
|
|
278
|
-
*/
|
|
279
|
-
function expandChannelSugar(
|
|
280
|
-
spec: Record<string, unknown>,
|
|
281
|
-
warnings: string[],
|
|
282
|
-
): Record<string, unknown> {
|
|
283
|
-
const encoding = spec.encoding as Record<string, unknown> | undefined;
|
|
284
|
-
if (!encoding || typeof encoding !== 'object') return spec;
|
|
285
|
-
|
|
286
|
-
const markType = markTypeOf(spec);
|
|
287
|
-
const updated: Record<string, unknown> = { ...encoding };
|
|
288
|
-
let changed = false;
|
|
289
|
-
let markExtra: Record<string, unknown> | undefined;
|
|
290
|
-
let legendMerge: Record<string, unknown> | undefined;
|
|
291
|
-
|
|
292
|
-
// Bare value defs (VL `color: { value }`) -> mark-level properties
|
|
293
|
-
if (markType) {
|
|
294
|
-
for (const channel of ['color', 'size', 'opacity'] as const) {
|
|
295
|
-
const ch = updated[channel] as Record<string, unknown> | undefined;
|
|
296
|
-
if (!ch || typeof ch !== 'object' || Array.isArray(ch)) continue;
|
|
297
|
-
if (!('value' in ch) || 'condition' in ch || 'field' in ch) continue;
|
|
298
|
-
const value = ch.value;
|
|
299
|
-
markExtra = markExtra ?? {};
|
|
300
|
-
if (channel === 'color') {
|
|
301
|
-
// Lines carry their color on stroke; every other mark family fills.
|
|
302
|
-
if (markType === 'line' && typeof value === 'string') markExtra.stroke = value;
|
|
303
|
-
else markExtra.fill = value;
|
|
304
|
-
} else if (typeof value === 'number') {
|
|
305
|
-
markExtra[channel] = value;
|
|
306
|
-
}
|
|
307
|
-
delete updated[channel];
|
|
308
|
-
changed = true;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Channel-level legend on color: `null` hides, a config merges (VL idiom)
|
|
313
|
-
const colorCh = updated.color as Record<string, unknown> | undefined;
|
|
314
|
-
if (
|
|
315
|
-
colorCh &&
|
|
316
|
-
typeof colorCh === 'object' &&
|
|
317
|
-
'legend' in colorCh &&
|
|
318
|
-
colorCh.legend !== undefined
|
|
319
|
-
) {
|
|
320
|
-
legendMerge =
|
|
321
|
-
colorCh.legend === null ? { show: false } : (colorCh.legend as Record<string, unknown>);
|
|
322
|
-
const { legend: _legend, ...rest } = colorCh;
|
|
323
|
-
updated.color = rest;
|
|
324
|
-
changed = true;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
for (const channel of Object.keys(updated)) {
|
|
328
|
-
const ch = updated[channel] as Record<string, unknown> | undefined;
|
|
329
|
-
if (!ch || typeof ch !== 'object' || Array.isArray(ch)) continue;
|
|
330
|
-
|
|
331
|
-
// axis: null -> false (VL uses null to hide an axis)
|
|
332
|
-
if (ch.axis === null) {
|
|
333
|
-
updated[channel] = { ...ch, axis: false };
|
|
334
|
-
changed = true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// scale.scheme -> scale.range via the core palette registry. Unknown
|
|
338
|
-
// names stay in place for validateSpec to reject with the supported
|
|
339
|
-
// list. An explicit range wins over scheme.
|
|
340
|
-
const current = updated[channel] as Record<string, unknown>;
|
|
341
|
-
const scale = current.scale as Record<string, unknown> | undefined;
|
|
342
|
-
if (scale && typeof scale.scheme === 'string') {
|
|
343
|
-
const stops = resolveSchemeName(scale.scheme);
|
|
344
|
-
if (stops) {
|
|
345
|
-
const { scheme: _scheme, ...scaleRest } = scale;
|
|
346
|
-
updated[channel] = { ...current, scale: { ...scaleRest, range: scale.range ?? stops } };
|
|
347
|
-
changed = true;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// scale.reverse on the color channel flips the ramp here, at the one place
|
|
352
|
-
// the stops are known. The color scale builders read scale.range directly
|
|
353
|
-
// and never look at reverse (only the positional builders do), so reversing
|
|
354
|
-
// the range is what makes `reverse` mean anything on color -- and it keeps
|
|
355
|
-
// the legend, which resolves its ramp from the same scale.range, in step.
|
|
356
|
-
// Positional channels keep their reverse flag for buildBand/Point/Continuous.
|
|
357
|
-
if (channel === 'color') {
|
|
358
|
-
const colorDef = updated[channel] as Record<string, unknown>;
|
|
359
|
-
const colorScale = colorDef.scale as Record<string, unknown> | undefined;
|
|
360
|
-
if (colorScale?.reverse && Array.isArray(colorScale.range)) {
|
|
361
|
-
const { reverse: _reverse, ...scaleRest } = colorScale;
|
|
362
|
-
updated[channel] = {
|
|
363
|
-
...colorDef,
|
|
364
|
-
scale: { ...scaleRest, range: [...(colorScale.range as string[])].reverse() },
|
|
365
|
-
};
|
|
366
|
-
changed = true;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// theta: the canonical arc value channel in v8, shared by waffle and
|
|
372
|
-
// parliament marks (the same part-to-whole value). The engine still reads
|
|
373
|
-
// encoding.y internally, so both directions of the alias converge on y:
|
|
374
|
-
// - theta present -> y is populated from theta (theta wins if both are set,
|
|
375
|
-
// since it's canonical now)
|
|
376
|
-
// - only y present -> y is kept and theta is populated from it, with a
|
|
377
|
-
// deprecation warning nudging authors toward theta
|
|
378
|
-
if (markType && (updated.theta || updated.y)) {
|
|
379
|
-
const thetaMark = markType === 'arc' || markType === 'waffle' || markType === 'parliament';
|
|
380
|
-
const hasTheta = updated.theta && typeof updated.theta === 'object';
|
|
381
|
-
const hasY = updated.y && typeof updated.y === 'object';
|
|
382
|
-
if (thetaMark && hasTheta) {
|
|
383
|
-
if (hasY && JSON.stringify(updated.y) !== JSON.stringify(updated.theta)) {
|
|
384
|
-
warnings.push(
|
|
385
|
-
`[openchart] encoding.theta and encoding.y are both set on ${markType}; theta takes precedence and y was dropped.`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
updated.y = updated.theta;
|
|
389
|
-
changed = true;
|
|
390
|
-
} else if (thetaMark && hasY && !hasTheta) {
|
|
391
|
-
updated.theta = updated.y;
|
|
392
|
-
warnings.push(
|
|
393
|
-
`[openchart] encoding.y on ${markType} marks is deprecated in v8; use encoding.theta for the value channel.`,
|
|
394
|
-
);
|
|
395
|
-
changed = true;
|
|
396
|
-
} else if (hasTheta && !thetaMark) {
|
|
397
|
-
warnings.push(
|
|
398
|
-
'[openchart] encoding.theta is only meaningful on arc, waffle, and parliament marks and was ignored.',
|
|
399
|
-
);
|
|
400
|
-
delete updated.theta;
|
|
401
|
-
changed = true;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Parliament: party colors carry real-world meaning (red/blue for US parties)
|
|
406
|
-
// and should never be left to the auto-cycling categorical palette. Nudge the
|
|
407
|
-
// author toward an explicit color scale range when none is set. Warning only,
|
|
408
|
-
// so the chart still renders (with palette colors) if they ignore it.
|
|
409
|
-
if (markType === 'parliament') {
|
|
410
|
-
const parliamentColor = updated.color as Record<string, unknown> | undefined;
|
|
411
|
-
const scale = parliamentColor?.scale as Record<string, unknown> | undefined;
|
|
412
|
-
const hasExplicitColors = Array.isArray(scale?.range) && (scale.range as unknown[]).length > 0;
|
|
413
|
-
if (parliamentColor && 'field' in parliamentColor && !hasExplicitColors) {
|
|
414
|
-
warnings.push(
|
|
415
|
-
'[openchart] parliament chart is using auto-palette colors. Party colors carry meaning; set them explicitly via encoding.color.scale.range (in party order), e.g. color: { field: "party", scale: { range: ["#1b7fa3", "#c44e52"] } }.',
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Dead channels: warn once and strip (behavior-identical; the engine ignores them)
|
|
421
|
-
for (const channel of Object.keys(DEAD_CHANNEL_MESSAGES)) {
|
|
422
|
-
if (updated[channel] !== undefined) {
|
|
423
|
-
warnings.push(DEAD_CHANNEL_MESSAGES[channel]);
|
|
424
|
-
delete updated[channel];
|
|
425
|
-
changed = true;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (!changed) return spec;
|
|
430
|
-
|
|
431
|
-
const result: Record<string, unknown> = { ...spec, encoding: updated };
|
|
432
|
-
if (markExtra) {
|
|
433
|
-
const markDef =
|
|
434
|
-
typeof spec.mark === 'string'
|
|
435
|
-
? { type: spec.mark }
|
|
436
|
-
: { ...(spec.mark as Record<string, unknown>) };
|
|
437
|
-
// Explicit mark-level values win over expanded channel constants
|
|
438
|
-
result.mark = { ...markExtra, ...markDef };
|
|
439
|
-
}
|
|
440
|
-
if (legendMerge) {
|
|
441
|
-
// Top-level legend keys win over the channel-level config
|
|
442
|
-
result.legend = { ...legendMerge, ...(spec.legend as Record<string, unknown> | undefined) };
|
|
443
|
-
}
|
|
444
|
-
return result;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ---------------------------------------------------------------------------
|
|
448
|
-
// Count aggregate without a field (VL histogram idiom)
|
|
449
|
-
// ---------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
/** Channels whose fields group the count aggregate (mirrors VL's implicit groupby). */
|
|
452
|
-
const COUNT_GROUP_CHANNELS = ['x', 'y', 'color', 'detail', 'strokeDash', 'facet'] as const;
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Desugar `{ aggregate: 'count' }` (no field) on x/y into an explicit
|
|
456
|
-
* aggregate transform grouped by the other encoded fields, rewriting the
|
|
457
|
-
* channel to the `__count` output field (VL's own convention). Runs after
|
|
458
|
-
* bin/timeUnit expansion so binned fields participate in the groupby, and the
|
|
459
|
-
* transform is appended after user transforms so it counts filtered rows.
|
|
460
|
-
*/
|
|
461
|
-
function expandCountAggregate(spec: Record<string, unknown>): Record<string, unknown> {
|
|
462
|
-
const encoding = spec.encoding as Record<string, unknown> | undefined;
|
|
463
|
-
if (!encoding || typeof encoding !== 'object') return spec;
|
|
464
|
-
|
|
465
|
-
const updated: Record<string, unknown> = { ...encoding };
|
|
466
|
-
let countTransform: AggregateTransform | undefined;
|
|
467
|
-
|
|
468
|
-
for (const channel of ['x', 'y'] as const) {
|
|
469
|
-
const ch = updated[channel] as Record<string, unknown> | undefined;
|
|
470
|
-
if (!ch || typeof ch !== 'object' || Array.isArray(ch)) continue;
|
|
471
|
-
if (ch.aggregate !== 'count' || ch.field != null) continue;
|
|
472
|
-
|
|
473
|
-
const groupby: string[] = [];
|
|
474
|
-
for (const other of COUNT_GROUP_CHANNELS) {
|
|
475
|
-
if (other === channel) continue;
|
|
476
|
-
const otherCh = updated[other] as Record<string, unknown> | undefined;
|
|
477
|
-
if (otherCh && typeof otherCh === 'object' && typeof otherCh.field === 'string') {
|
|
478
|
-
if (!groupby.includes(otherCh.field)) groupby.push(otherCh.field);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
countTransform = { aggregate: [{ op: 'count', field: '__count', as: '__count' }], groupby };
|
|
483
|
-
const { aggregate: _aggregate, ...rest } = ch;
|
|
484
|
-
updated[channel] = {
|
|
485
|
-
...rest,
|
|
486
|
-
field: '__count',
|
|
487
|
-
type: ch.type ?? 'quantitative',
|
|
488
|
-
title: ch.title ?? 'Count',
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (!countTransform) return spec;
|
|
493
|
-
const existing = (spec.transform as Transform[] | undefined) ?? [];
|
|
494
|
-
return { ...spec, encoding: updated, transform: [...existing, countTransform] };
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// ---------------------------------------------------------------------------
|
|
498
|
-
// Sort-by-value resolution (VL '-y', value arrays, { field, op, order })
|
|
499
|
-
// ---------------------------------------------------------------------------
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Resolve VL sort forms on categorical channels into an explicit
|
|
503
|
-
* `scale.domain`, which the scale builders already honor. Canonical
|
|
504
|
-
* 'ascending' / 'descending' / null pass through untouched. Skipped (and
|
|
505
|
-
* retried at leaf compile) when the data is not yet known, e.g. layer
|
|
506
|
-
* children inheriting parent data.
|
|
507
|
-
*/
|
|
508
|
-
function resolveSortSugar(
|
|
509
|
-
spec: Record<string, unknown>,
|
|
510
|
-
inheritedData?: DataRow[],
|
|
511
|
-
): Record<string, unknown> {
|
|
512
|
-
const encoding = spec.encoding as Record<string, unknown> | undefined;
|
|
513
|
-
if (!encoding || typeof encoding !== 'object') return spec;
|
|
514
|
-
const data = (Array.isArray(spec.data) ? spec.data : inheritedData) as DataRow[] | undefined;
|
|
515
|
-
|
|
516
|
-
const updated: Record<string, unknown> = { ...encoding };
|
|
517
|
-
let changed = false;
|
|
518
|
-
|
|
519
|
-
for (const channel of ['x', 'y', 'color'] as const) {
|
|
520
|
-
const ch = updated[channel] as Record<string, unknown> | undefined;
|
|
521
|
-
if (!ch || typeof ch !== 'object' || Array.isArray(ch) || !('sort' in ch)) continue;
|
|
522
|
-
const sort = ch.sort;
|
|
523
|
-
if (sort === undefined || sort === null || sort === 'ascending' || sort === 'descending') {
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const field = ch.field as string | undefined;
|
|
528
|
-
const scale = (ch.scale ?? {}) as Record<string, unknown>;
|
|
529
|
-
|
|
530
|
-
// An explicit domain wins over sort (matches canonical sort behavior)
|
|
531
|
-
if (scale.domain) {
|
|
532
|
-
const { sort: _sort, ...rest } = ch;
|
|
533
|
-
updated[channel] = rest;
|
|
534
|
-
changed = true;
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
if (!field || !data || data.length === 0) continue;
|
|
538
|
-
|
|
539
|
-
let domain: string[] | undefined;
|
|
540
|
-
if (Array.isArray(sort)) {
|
|
541
|
-
// Value-array sort: listed values first, remaining values in data order
|
|
542
|
-
const present = uniqueFieldStrings(data, field);
|
|
543
|
-
const listed = sort.map(String).filter((v) => present.includes(v));
|
|
544
|
-
const rest = present.filter((v) => !listed.includes(v));
|
|
545
|
-
domain = [...listed, ...rest];
|
|
546
|
-
} else {
|
|
547
|
-
// 'x' | '-x' | 'y' | '-y' or { field, op, order }
|
|
548
|
-
let targetField: string | undefined;
|
|
549
|
-
let op: AggregateOp = 'sum';
|
|
550
|
-
let descending = false;
|
|
551
|
-
let isCount = false;
|
|
552
|
-
if (typeof sort === 'string' && /^-?[xy]$/.test(sort)) {
|
|
553
|
-
descending = sort.startsWith('-');
|
|
554
|
-
const target = updated[sort.replace('-', '')] as Record<string, unknown> | undefined;
|
|
555
|
-
if (!target || typeof target !== 'object') continue;
|
|
556
|
-
targetField = target.field as string | undefined;
|
|
557
|
-
if (target.aggregate === 'count') isCount = true;
|
|
558
|
-
else if (typeof target.aggregate === 'string') op = target.aggregate as AggregateOp;
|
|
559
|
-
} else if (typeof sort === 'object' && 'field' in (sort as object)) {
|
|
560
|
-
const s = sort as { field: string; op?: AggregateOp; order?: string };
|
|
561
|
-
targetField = s.field;
|
|
562
|
-
if (s.op === 'count') isCount = true;
|
|
563
|
-
else if (s.op) op = s.op;
|
|
564
|
-
descending = s.order === 'descending';
|
|
565
|
-
} else {
|
|
566
|
-
// Unknown sort shape: fall back to data order
|
|
567
|
-
const { sort: _sort, ...rest } = ch;
|
|
568
|
-
updated[channel] = rest;
|
|
569
|
-
changed = true;
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
if (!targetField && !isCount) continue;
|
|
573
|
-
|
|
574
|
-
// Aggregate the target values per category (default op: sum, VL aligned)
|
|
575
|
-
const groups = new Map<string, number[]>();
|
|
576
|
-
for (const row of data) {
|
|
577
|
-
const raw = row[field];
|
|
578
|
-
if (raw == null) continue;
|
|
579
|
-
const key = String(raw);
|
|
580
|
-
let bucket = groups.get(key);
|
|
581
|
-
if (!bucket) {
|
|
582
|
-
bucket = [];
|
|
583
|
-
groups.set(key, bucket);
|
|
584
|
-
}
|
|
585
|
-
if (isCount) {
|
|
586
|
-
bucket.push(1);
|
|
587
|
-
} else {
|
|
588
|
-
const value = Number(row[targetField as string]);
|
|
589
|
-
if (Number.isFinite(value)) bucket.push(value);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const entries = [...groups.entries()].map(
|
|
593
|
-
([key, values]) => [key, computeAggregate(isCount ? 'count' : op, values)] as const,
|
|
594
|
-
);
|
|
595
|
-
entries.sort((a, b) => (descending ? b[1] - a[1] : a[1] - b[1]));
|
|
596
|
-
domain = entries.map(([key]) => key);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (!domain) continue;
|
|
600
|
-
const { sort: _sort, ...rest } = ch;
|
|
601
|
-
updated[channel] = { ...rest, scale: { ...scale, domain } };
|
|
602
|
-
changed = true;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return changed ? { ...spec, encoding: updated } : spec;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// ---------------------------------------------------------------------------
|
|
609
|
-
// Composition: chart and layer expansion
|
|
610
|
-
// ---------------------------------------------------------------------------
|
|
611
|
-
|
|
612
|
-
function expandChartSugar(
|
|
613
|
-
spec: Record<string, unknown>,
|
|
614
|
-
warnings: string[],
|
|
615
|
-
inheritedData?: DataRow[],
|
|
616
|
-
): Record<string, unknown> {
|
|
617
|
-
let out = unwrapDataValues(spec);
|
|
618
|
-
out = expandTopLevelTitle(out);
|
|
619
|
-
out = expandDescriptionSugar(out);
|
|
620
|
-
out = stripIgnoredKeys(out, warnings);
|
|
621
|
-
out = applyFixedSizeDefault(out);
|
|
622
|
-
out = expandAnnotationSugar(out, warnings);
|
|
623
|
-
out = expandChannelSugar(out, warnings);
|
|
624
|
-
out = expandEncodingSugar(out);
|
|
625
|
-
out = resolveSortSugar(out, inheritedData);
|
|
626
|
-
out = expandCountAggregate(out);
|
|
627
|
-
return out;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function expandLayerSugar(
|
|
631
|
-
spec: Record<string, unknown>,
|
|
632
|
-
warnings: string[],
|
|
633
|
-
inheritedData?: DataRow[],
|
|
634
|
-
parentEncoding?: Record<string, unknown>,
|
|
635
|
-
): Record<string, unknown> {
|
|
636
|
-
let out = unwrapDataValues(spec);
|
|
637
|
-
out = expandTopLevelTitle(out);
|
|
638
|
-
out = stripIgnoredKeys(out, warnings);
|
|
639
|
-
out = expandAnnotationSugar(out, warnings);
|
|
640
|
-
// Shared encoding gets the mark-independent channel sugar (axis, scheme,
|
|
641
|
-
// legend, dead channels) and sort resolution; mark-dependent sugar (value
|
|
642
|
-
// defs, theta) resolves per leaf after encoding inheritance.
|
|
643
|
-
out = expandChannelSugar(out, warnings);
|
|
644
|
-
out = resolveSortSugar(out, inheritedData);
|
|
645
|
-
|
|
646
|
-
const layerData = Array.isArray(out.data) ? (out.data as DataRow[]) : inheritedData;
|
|
647
|
-
const ownEncoding = out.encoding as Record<string, unknown> | undefined;
|
|
648
|
-
const mergedEncoding =
|
|
649
|
-
parentEncoding && ownEncoding
|
|
650
|
-
? { ...parentEncoding, ...ownEncoding }
|
|
651
|
-
: (ownEncoding ?? parentEncoding);
|
|
652
|
-
|
|
653
|
-
const layer = out.layer as unknown[];
|
|
654
|
-
const expandedChildren = layer.map((child) => {
|
|
655
|
-
if (!child || typeof child !== 'object' || Array.isArray(child)) return child;
|
|
656
|
-
const childObj = child as Record<string, unknown>;
|
|
657
|
-
if (Array.isArray(childObj.layer)) {
|
|
658
|
-
return expandLayerSugar(childObj, warnings, layerData, mergedEncoding);
|
|
659
|
-
}
|
|
660
|
-
if ('mark' in childObj) {
|
|
661
|
-
return expandChartSugar(childObj, warnings, layerData);
|
|
662
|
-
}
|
|
663
|
-
return child;
|
|
664
|
-
});
|
|
665
|
-
return { ...out, layer: expandedChildren };
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// ---------------------------------------------------------------------------
|
|
669
|
-
// Public API
|
|
670
|
-
// ---------------------------------------------------------------------------
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Expand deprecated `valueFormat` into `encoding.value.format` for
|
|
674
|
-
* sankey, tilemap, and barlist specs. Encoding-level format wins on conflict.
|
|
675
|
-
*/
|
|
676
|
-
function expandValueFormatSugar(
|
|
677
|
-
spec: Record<string, unknown>,
|
|
678
|
-
warnings: string[],
|
|
679
|
-
): Record<string, unknown> {
|
|
680
|
-
const type = spec.type as string | undefined;
|
|
681
|
-
if (type !== 'sankey' && type !== 'tilemap' && type !== 'barlist') return spec;
|
|
682
|
-
if (typeof spec.valueFormat !== 'string') return spec;
|
|
683
|
-
|
|
684
|
-
const encoding = spec.encoding as Record<string, unknown> | undefined;
|
|
685
|
-
const valueCh = encoding?.value as Record<string, unknown> | undefined;
|
|
686
|
-
|
|
687
|
-
// Only expand into encoding when encoding.value already exists (the shorthand
|
|
688
|
-
// tilemap/barlist API may not have explicit encoding — the normalizer infers it).
|
|
689
|
-
if (!valueCh || typeof valueCh !== 'object') {
|
|
690
|
-
warnings.push(
|
|
691
|
-
'[openchart] valueFormat is deprecated; set format on the encoding value channel instead (e.g. encoding.value.format).',
|
|
692
|
-
);
|
|
693
|
-
return spec;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (valueCh.format !== undefined) {
|
|
697
|
-
const { valueFormat: _vf, ...rest } = spec;
|
|
698
|
-
warnings.push(
|
|
699
|
-
'[openchart] valueFormat is deprecated; the encoding.value.format field takes precedence and valueFormat was ignored.',
|
|
700
|
-
);
|
|
701
|
-
return rest;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
warnings.push(
|
|
705
|
-
'[openchart] valueFormat is deprecated; set format on the encoding value channel instead (e.g. encoding.value.format).',
|
|
706
|
-
);
|
|
707
|
-
const { valueFormat, ...rest } = spec;
|
|
708
|
-
return {
|
|
709
|
-
...rest,
|
|
710
|
-
encoding: { ...encoding, value: { ...valueCh, format: valueFormat } },
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Expand VL-idiom and encoding-level sugar on a raw chart or layer spec.
|
|
716
|
-
* Non-chart specs (tables, graphs, sankey, tilemap, barlist) get valueFormat
|
|
717
|
-
* expansion only. Deprecation warnings are pushed onto `warnings`; callers
|
|
718
|
-
* surface them (compileChart/compileLayer console.warn each unique message
|
|
719
|
-
* once per compile).
|
|
720
|
-
*/
|
|
721
|
-
export function expandSpecSugar(
|
|
722
|
-
spec: Record<string, unknown>,
|
|
723
|
-
warnings: string[] = [],
|
|
724
|
-
): Record<string, unknown> {
|
|
725
|
-
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return spec;
|
|
726
|
-
if (Array.isArray(spec.layer) && typeof spec.type !== 'string') {
|
|
727
|
-
return expandLayerSugar(spec, warnings);
|
|
728
|
-
}
|
|
729
|
-
if (!('mark' in spec)) {
|
|
730
|
-
return expandValueFormatSugar(spec, warnings);
|
|
731
|
-
}
|
|
732
|
-
return expandChartSugar(spec, warnings);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/** Advisory-warning sink: a host callback, or the console.warn default. */
|
|
736
|
-
export type WarnSink = (message: string) => void;
|
|
737
|
-
|
|
738
|
-
const defaultWarnSink: WarnSink = (message) => console.warn(message);
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Surface sugar/deprecation warnings: emit each unique message once per compile.
|
|
742
|
-
* Duplicates within a compile (e.g. the same dead channel on two layer children)
|
|
743
|
-
* collapse to one warning.
|
|
744
|
-
*
|
|
745
|
-
* The engine is isomorphic and never touches the global `console` on its own:
|
|
746
|
-
* `sink` is the host-provided `CompileOptions.onWarn` when present, so a host can
|
|
747
|
-
* collect, reroute, or silence warnings (SSR, tests, dev overlays). It falls back
|
|
748
|
-
* to `console.warn` only when no sink is passed, preserving the prior behavior.
|
|
749
|
-
*/
|
|
750
|
-
export function emitSpecWarnings(warnings: string[], sink: WarnSink = defaultWarnSink): void {
|
|
751
|
-
if (warnings.length === 0) return;
|
|
752
|
-
const seen = new Set<string>();
|
|
753
|
-
for (const warning of warnings) {
|
|
754
|
-
if (seen.has(warning)) continue;
|
|
755
|
-
seen.add(warning);
|
|
756
|
-
sink(warning);
|
|
757
|
-
}
|
|
758
|
-
}
|