ggaction 0.0.3 → 0.0.4
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/CHANGELOG.md +28 -3
- package/README.md +95 -83
- package/package.json +11 -6
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +1 -1
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +151 -0
- package/src/actions/encodings/color/layout.js +91 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/index.js +2 -2
- package/src/actions/encodings/offset.js +1 -1
- package/src/actions/encodings/{position.js → position/index.js} +6 -6
- package/src/actions/encodings/position/resolve.js +1 -1
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/options.js +1 -1
- package/src/actions/errorBars/options.js +1 -1
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/facets/derive.js +2 -2
- package/src/actions/facets/guides.js +1 -1
- package/src/actions/guides/axes/labels.js +1 -1
- package/src/actions/guides/axes/ticks.js +1 -1
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/grids/resolve.js +1 -1
- package/src/actions/guides/legends/categorical/layout.js +4 -2
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +2 -20
- package/src/actions/guides/legends/remove.js +2 -20
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
- package/src/actions/guides/polar/index.js +1 -1
- package/src/actions/guides/polar/resolve.js +1 -1
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +2 -2
- package/src/actions/marks/area/actions.js +2 -2
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/rect/actions.js +1 -1
- package/src/actions/marks/rule/actions.js +1 -1
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +1 -1
- package/src/actions/primitives/semanticValue.js +1 -1
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +1 -1
- package/src/actions/scales/consumers/common.js +91 -0
- package/src/actions/scales/consumers/families.js +47 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +2 -2
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/materialize.js +2 -2
- package/src/core/ChartProgram.js +22 -236
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +1 -1
- package/src/grammar/density.js +1 -1
- package/src/grammar/{facets.js → facets/index.js} +5 -5
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polarLineCommands.js +1 -1
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +1 -1
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/transforms.js +1 -1
- package/src/materialization/bars/aggregate.js +1 -1
- package/src/materialization/bars/grouped.js +1 -1
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +6 -87
- package/src/materialization/encodings.js +2 -2
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/marks/capabilities.js +141 -0
- package/src/materialization/marks/index.js +105 -0
- package/src/materialization/marks/policies.js +87 -0
- package/src/materialization/rect.js +1 -1
- package/src/materialization/rowEncoding.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +1 -1
- package/src/materialization/scales/policies/series.js +1 -1
- package/src/materialization/scales/resolve.js +1 -1
- package/src/materialization/selection/items/bar.js +1 -1
- package/src/materialization/selection/styles.js +1 -1
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +4 -1
- package/src/actions/encodings/color.js +0 -424
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/facetGuides.js +0 -426
- package/src/materialization/marks.js +0 -316
package/src/core/ChartProgram.js
CHANGED
|
@@ -6,9 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
cloneAndFreeze,
|
|
8
8
|
freezeOwned,
|
|
9
|
-
|
|
10
|
-
isPlainObject,
|
|
11
|
-
removeOwnedPath
|
|
9
|
+
isPlainObject
|
|
12
10
|
} from "./immutable.js";
|
|
13
11
|
import {
|
|
14
12
|
createEmptyGraphicSpec,
|
|
@@ -16,207 +14,15 @@ import {
|
|
|
16
14
|
createTraceRoot
|
|
17
15
|
} from "./specs.js";
|
|
18
16
|
import {
|
|
19
|
-
FACET_SCALE_CHANNELS,
|
|
20
|
-
FACET_SCALE_RESOLUTIONS,
|
|
21
17
|
LEGEND_CONFIG_KINDS
|
|
22
18
|
} from "./vocabulary.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
throw new TypeError("ChartProgram children must be a plain object.");
|
|
31
|
-
}
|
|
32
|
-
if (isOwned(children)) return children;
|
|
33
|
-
const owned = {};
|
|
34
|
-
for (const [id, program] of Object.entries(children)) {
|
|
35
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
36
|
-
throw new TypeError("ChartProgram child IDs must be non-empty strings.");
|
|
37
|
-
}
|
|
38
|
-
if (!(program instanceof ChartProgram)) {
|
|
39
|
-
throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
|
|
40
|
-
}
|
|
41
|
-
owned[id] = program;
|
|
42
|
-
}
|
|
43
|
-
return freezeOwned(owned);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function ownCompositionSpec(compositionSpec, children) {
|
|
47
|
-
const childIds = Object.keys(children);
|
|
48
|
-
if (compositionSpec === undefined) {
|
|
49
|
-
if (childIds.length > 0) {
|
|
50
|
-
throw new Error("ChartProgram children require a compositionSpec.");
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
if (!isPlainObject(compositionSpec)) {
|
|
55
|
-
throw new TypeError("ChartProgram compositionSpec must be a plain object.");
|
|
56
|
-
}
|
|
57
|
-
const allowed = [
|
|
58
|
-
"id", "type", "direction", "children", "columns", "gap", "align",
|
|
59
|
-
"padding", "facet"
|
|
60
|
-
];
|
|
61
|
-
const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
|
|
62
|
-
if (unknown !== undefined) {
|
|
63
|
-
throw new Error(`Unknown compositionSpec property "${unknown}".`);
|
|
64
|
-
}
|
|
65
|
-
if (typeof compositionSpec.id !== "string" || compositionSpec.id.length === 0) {
|
|
66
|
-
throw new TypeError("compositionSpec.id must be a non-empty string.");
|
|
67
|
-
}
|
|
68
|
-
const facet = compositionSpec.type === "facet";
|
|
69
|
-
if (compositionSpec.type !== undefined && !facet) {
|
|
70
|
-
throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
|
|
71
|
-
}
|
|
72
|
-
if (facet) {
|
|
73
|
-
if (compositionSpec.direction !== undefined) {
|
|
74
|
-
throw new Error("Facet compositionSpec does not use direction.");
|
|
75
|
-
}
|
|
76
|
-
} else if (!["horizontal", "vertical"].includes(compositionSpec.direction)) {
|
|
77
|
-
throw new Error("compositionSpec.direction must be horizontal or vertical.");
|
|
78
|
-
}
|
|
79
|
-
if (
|
|
80
|
-
!Array.isArray(compositionSpec.children) ||
|
|
81
|
-
compositionSpec.children.length < (facet ? 1 : 2) ||
|
|
82
|
-
!compositionSpec.children.every(id => typeof id === "string" && id.length > 0)
|
|
83
|
-
) {
|
|
84
|
-
throw new TypeError(
|
|
85
|
-
facet
|
|
86
|
-
? "Facet compositionSpec.children requires at least one child ID."
|
|
87
|
-
: "compositionSpec.children requires at least two child IDs."
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
if (new Set(compositionSpec.children).size !== compositionSpec.children.length) {
|
|
91
|
-
throw new Error("compositionSpec.children must not contain duplicate IDs.");
|
|
92
|
-
}
|
|
93
|
-
if (
|
|
94
|
-
compositionSpec.children.length !== childIds.length ||
|
|
95
|
-
compositionSpec.children.some(id => !Object.hasOwn(children, id))
|
|
96
|
-
) {
|
|
97
|
-
throw new Error("compositionSpec.children must match ChartProgram children exactly.");
|
|
98
|
-
}
|
|
99
|
-
if (facet) {
|
|
100
|
-
if (
|
|
101
|
-
!Number.isInteger(compositionSpec.columns) ||
|
|
102
|
-
compositionSpec.columns <= 0 ||
|
|
103
|
-
compositionSpec.columns > compositionSpec.children.length
|
|
104
|
-
) {
|
|
105
|
-
throw new RangeError(
|
|
106
|
-
"Facet compositionSpec.columns must be a positive integer no larger than its children."
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (!isPlainObject(compositionSpec.facet)) {
|
|
110
|
-
throw new TypeError("Facet compositionSpec.facet must be a plain object.");
|
|
111
|
-
}
|
|
112
|
-
const facetKeys = ["data", "field", "values", "scales", "guides"];
|
|
113
|
-
const unknownFacet = Object.keys(compositionSpec.facet).find(
|
|
114
|
-
key => !facetKeys.includes(key)
|
|
115
|
-
);
|
|
116
|
-
if (unknownFacet !== undefined) {
|
|
117
|
-
throw new Error(`Unknown compositionSpec.facet property "${unknownFacet}".`);
|
|
118
|
-
}
|
|
119
|
-
for (const property of ["data", "field"]) {
|
|
120
|
-
if (
|
|
121
|
-
typeof compositionSpec.facet[property] !== "string" ||
|
|
122
|
-
compositionSpec.facet[property].length === 0
|
|
123
|
-
) {
|
|
124
|
-
throw new TypeError(`compositionSpec.facet.${property} must be a non-empty string.`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const values = compositionSpec.facet.values;
|
|
128
|
-
if (
|
|
129
|
-
!Array.isArray(values) ||
|
|
130
|
-
values.length !== compositionSpec.children.length ||
|
|
131
|
-
values.some(value => !(
|
|
132
|
-
typeof value === "string" ||
|
|
133
|
-
typeof value === "boolean" ||
|
|
134
|
-
(typeof value === "number" && Number.isFinite(value))
|
|
135
|
-
)) ||
|
|
136
|
-
new Set(values).size !== values.length
|
|
137
|
-
) {
|
|
138
|
-
throw new TypeError(
|
|
139
|
-
"compositionSpec.facet.values must contain one unique scalar per child."
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const scales = compositionSpec.facet.scales;
|
|
143
|
-
if (
|
|
144
|
-
!isPlainObject(scales) ||
|
|
145
|
-
Object.keys(scales).length !== FACET_SCALE_CHANNELS.length ||
|
|
146
|
-
FACET_SCALE_CHANNELS.some(channel =>
|
|
147
|
-
!FACET_SCALE_RESOLUTIONS.includes(scales[channel])
|
|
148
|
-
) ||
|
|
149
|
-
Object.keys(scales).some(channel => !FACET_SCALE_CHANNELS.includes(channel))
|
|
150
|
-
) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
"compositionSpec.facet.scales requires one shared or independent policy per supported channel."
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
const guides = compositionSpec.facet.guides;
|
|
156
|
-
if (
|
|
157
|
-
!isPlainObject(guides) ||
|
|
158
|
-
Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
|
|
159
|
-
!["each", "outer"].includes(guides.axes) ||
|
|
160
|
-
![false, "shared"].includes(guides.legend)
|
|
161
|
-
) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
} else if (
|
|
167
|
-
compositionSpec.columns !== undefined ||
|
|
168
|
-
compositionSpec.facet !== undefined
|
|
169
|
-
) {
|
|
170
|
-
throw new Error("Concat compositionSpec does not accept facet properties.");
|
|
171
|
-
}
|
|
172
|
-
if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
|
|
173
|
-
throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
|
|
174
|
-
}
|
|
175
|
-
if (!["start", "center", "end"].includes(compositionSpec.align)) {
|
|
176
|
-
throw new Error("compositionSpec.align must be start, center, or end.");
|
|
177
|
-
}
|
|
178
|
-
if (!isPlainObject(compositionSpec.padding)) {
|
|
179
|
-
throw new TypeError("compositionSpec.padding must be a plain object.");
|
|
180
|
-
}
|
|
181
|
-
for (const side of ["top", "right", "bottom", "left"]) {
|
|
182
|
-
if (!Number.isFinite(compositionSpec.padding[side]) || compositionSpec.padding[side] < 0) {
|
|
183
|
-
throw new RangeError(
|
|
184
|
-
`compositionSpec.padding.${side} must be a non-negative finite number.`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const paddingKeys = Object.keys(compositionSpec.padding);
|
|
189
|
-
if (
|
|
190
|
-
paddingKeys.length !== 4 ||
|
|
191
|
-
paddingKeys.some(key => !["top", "right", "bottom", "left"].includes(key))
|
|
192
|
-
) {
|
|
193
|
-
throw new Error("compositionSpec.padding must contain exactly four sides.");
|
|
194
|
-
}
|
|
195
|
-
return ownState(compositionSpec);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function createMaterializationConfigs(
|
|
199
|
-
markConfigs,
|
|
200
|
-
guideConfigs,
|
|
201
|
-
titleConfig,
|
|
202
|
-
canvasConfig
|
|
203
|
-
) {
|
|
204
|
-
return cloneAndFreeze({
|
|
205
|
-
marks: markConfigs,
|
|
206
|
-
guides: guideConfigs,
|
|
207
|
-
...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
|
|
208
|
-
...(titleConfig === undefined ? {} : { title: titleConfig })
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function updateConfigPath(value, path, config) {
|
|
213
|
-
if (path.length === 0) return cloneAndFreeze(config);
|
|
214
|
-
const [key, ...rest] = path;
|
|
215
|
-
return freezeOwned({
|
|
216
|
-
...value,
|
|
217
|
-
[key]: updateConfigPath(value?.[key] ?? {}, rest, config)
|
|
218
|
-
});
|
|
219
|
-
}
|
|
19
|
+
import { ownCompositionSpec } from "./compositionState.js";
|
|
20
|
+
import {
|
|
21
|
+
createMaterializationConfigs,
|
|
22
|
+
removeMaterializationConfig,
|
|
23
|
+
setMaterializationConfig
|
|
24
|
+
} from "./materializationState.js";
|
|
25
|
+
import { ownChildPrograms, ownProgramState } from "./programState.js";
|
|
220
26
|
|
|
221
27
|
export class ChartProgram {
|
|
222
28
|
constructor({
|
|
@@ -235,9 +41,9 @@ export class ChartProgram {
|
|
|
235
41
|
actionStack = [],
|
|
236
42
|
actionSequence
|
|
237
43
|
} = {}) {
|
|
238
|
-
this.semanticSpec =
|
|
239
|
-
this.graphicSpec =
|
|
240
|
-
this.resolvedScales =
|
|
44
|
+
this.semanticSpec = ownProgramState(semanticSpec);
|
|
45
|
+
this.graphicSpec = ownProgramState(graphicSpec);
|
|
46
|
+
this.resolvedScales = ownProgramState(resolvedScales);
|
|
241
47
|
this.materializationConfigs = materializationConfigs === undefined
|
|
242
48
|
? createMaterializationConfigs(
|
|
243
49
|
markConfigs,
|
|
@@ -245,12 +51,12 @@ export class ChartProgram {
|
|
|
245
51
|
titleConfig,
|
|
246
52
|
canvasConfig
|
|
247
53
|
)
|
|
248
|
-
:
|
|
249
|
-
this.children = ownChildPrograms(children);
|
|
54
|
+
: ownProgramState(materializationConfigs);
|
|
55
|
+
this.children = ownChildPrograms(children, ChartProgram);
|
|
250
56
|
this.compositionSpec = ownCompositionSpec(compositionSpec, this.children);
|
|
251
|
-
this.context =
|
|
252
|
-
this.trace =
|
|
253
|
-
this.actionStack =
|
|
57
|
+
this.context = ownProgramState(context);
|
|
58
|
+
this.trace = ownProgramState(trace);
|
|
59
|
+
this.actionStack = ownProgramState(actionStack);
|
|
254
60
|
Object.defineProperty(this, "_actionSequence", {
|
|
255
61
|
value: actionSequence ?? countActionNodes(this.trace),
|
|
256
62
|
enumerable: false
|
|
@@ -386,18 +192,8 @@ export class ChartProgram {
|
|
|
386
192
|
}
|
|
387
193
|
|
|
388
194
|
_withMaterializationConfig(path, config) {
|
|
389
|
-
if (
|
|
390
|
-
!Array.isArray(path) ||
|
|
391
|
-
path.length === 0 ||
|
|
392
|
-
!path.every(key => typeof key === "string" && key.length > 0)
|
|
393
|
-
) {
|
|
394
|
-
throw new TypeError("Materialization config path must contain names.");
|
|
395
|
-
}
|
|
396
|
-
if (!isPlainObject(config)) {
|
|
397
|
-
throw new TypeError("Materialization config must be a plain object.");
|
|
398
|
-
}
|
|
399
195
|
return this._clone({
|
|
400
|
-
materializationConfigs:
|
|
196
|
+
materializationConfigs: setMaterializationConfig(
|
|
401
197
|
this.materializationConfigs,
|
|
402
198
|
path,
|
|
403
199
|
config
|
|
@@ -406,22 +202,12 @@ export class ChartProgram {
|
|
|
406
202
|
}
|
|
407
203
|
|
|
408
204
|
_withoutMaterializationConfig(path) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
path
|
|
412
|
-
|
|
413
|
-
) {
|
|
414
|
-
throw new TypeError("Materialization config path must contain names.");
|
|
415
|
-
}
|
|
416
|
-
const removed = removeOwnedPath(this.materializationConfigs, path);
|
|
205
|
+
const removed = removeMaterializationConfig(
|
|
206
|
+
this.materializationConfigs,
|
|
207
|
+
path
|
|
208
|
+
);
|
|
417
209
|
if (!removed.removed) return this;
|
|
418
|
-
return this._clone({
|
|
419
|
-
materializationConfigs: freezeOwned({
|
|
420
|
-
...removed.value,
|
|
421
|
-
marks: removed.value.marks ?? freezeOwned({}),
|
|
422
|
-
guides: removed.value.guides ?? freezeOwned({})
|
|
423
|
-
})
|
|
424
|
-
});
|
|
210
|
+
return this._clone({ materializationConfigs: removed.value });
|
|
425
211
|
}
|
|
426
212
|
|
|
427
213
|
_withGuideConfig(channel, component, config) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { isPlainObject } from "./immutable.js";
|
|
2
|
+
import { ownProgramState } from "./programState.js";
|
|
3
|
+
import {
|
|
4
|
+
FACET_SCALE_CHANNELS,
|
|
5
|
+
FACET_SCALE_RESOLUTIONS
|
|
6
|
+
} from "./vocabulary.js";
|
|
7
|
+
|
|
8
|
+
function validateFacetSpec(compositionSpec) {
|
|
9
|
+
if (compositionSpec.direction !== undefined) {
|
|
10
|
+
throw new Error("Facet compositionSpec does not use direction.");
|
|
11
|
+
}
|
|
12
|
+
if (
|
|
13
|
+
!Number.isInteger(compositionSpec.columns) ||
|
|
14
|
+
compositionSpec.columns <= 0 ||
|
|
15
|
+
compositionSpec.columns > compositionSpec.children.length
|
|
16
|
+
) {
|
|
17
|
+
throw new RangeError(
|
|
18
|
+
"Facet compositionSpec.columns must be a positive integer no larger than its children."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (!isPlainObject(compositionSpec.facet)) {
|
|
22
|
+
throw new TypeError("Facet compositionSpec.facet must be a plain object.");
|
|
23
|
+
}
|
|
24
|
+
const facetKeys = ["data", "field", "values", "scales", "guides"];
|
|
25
|
+
const unknownFacet = Object.keys(compositionSpec.facet).find(
|
|
26
|
+
key => !facetKeys.includes(key)
|
|
27
|
+
);
|
|
28
|
+
if (unknownFacet !== undefined) {
|
|
29
|
+
throw new Error(`Unknown compositionSpec.facet property "${unknownFacet}".`);
|
|
30
|
+
}
|
|
31
|
+
for (const property of ["data", "field"]) {
|
|
32
|
+
if (
|
|
33
|
+
typeof compositionSpec.facet[property] !== "string" ||
|
|
34
|
+
compositionSpec.facet[property].length === 0
|
|
35
|
+
) {
|
|
36
|
+
throw new TypeError(`compositionSpec.facet.${property} must be a non-empty string.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const values = compositionSpec.facet.values;
|
|
40
|
+
if (
|
|
41
|
+
!Array.isArray(values) ||
|
|
42
|
+
values.length !== compositionSpec.children.length ||
|
|
43
|
+
values.some(value => !(
|
|
44
|
+
typeof value === "string" ||
|
|
45
|
+
typeof value === "boolean" ||
|
|
46
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
47
|
+
)) ||
|
|
48
|
+
new Set(values).size !== values.length
|
|
49
|
+
) {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
"compositionSpec.facet.values must contain one unique scalar per child."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const scales = compositionSpec.facet.scales;
|
|
55
|
+
if (
|
|
56
|
+
!isPlainObject(scales) ||
|
|
57
|
+
Object.keys(scales).length !== FACET_SCALE_CHANNELS.length ||
|
|
58
|
+
FACET_SCALE_CHANNELS.some(channel =>
|
|
59
|
+
!FACET_SCALE_RESOLUTIONS.includes(scales[channel])
|
|
60
|
+
) ||
|
|
61
|
+
Object.keys(scales).some(channel => !FACET_SCALE_CHANNELS.includes(channel))
|
|
62
|
+
) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"compositionSpec.facet.scales requires one shared or independent policy per supported channel."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const guides = compositionSpec.facet.guides;
|
|
68
|
+
if (
|
|
69
|
+
!isPlainObject(guides) ||
|
|
70
|
+
Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
|
|
71
|
+
!["each", "outer"].includes(guides.axes) ||
|
|
72
|
+
![false, "shared"].includes(guides.legend)
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateCompositionChildren(compositionSpec, childIds, facet) {
|
|
81
|
+
if (
|
|
82
|
+
!Array.isArray(compositionSpec.children) ||
|
|
83
|
+
compositionSpec.children.length < (facet ? 1 : 2) ||
|
|
84
|
+
!compositionSpec.children.every(id => typeof id === "string" && id.length > 0)
|
|
85
|
+
) {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
facet
|
|
88
|
+
? "Facet compositionSpec.children requires at least one child ID."
|
|
89
|
+
: "compositionSpec.children requires at least two child IDs."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (new Set(compositionSpec.children).size !== compositionSpec.children.length) {
|
|
93
|
+
throw new Error("compositionSpec.children must not contain duplicate IDs.");
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
compositionSpec.children.length !== childIds.length ||
|
|
97
|
+
compositionSpec.children.some(id => !childIds.includes(id))
|
|
98
|
+
) {
|
|
99
|
+
throw new Error("compositionSpec.children must match ChartProgram children exactly.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateLayout(compositionSpec) {
|
|
104
|
+
if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
|
|
105
|
+
throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
|
|
106
|
+
}
|
|
107
|
+
if (!["start", "center", "end"].includes(compositionSpec.align)) {
|
|
108
|
+
throw new Error("compositionSpec.align must be start, center, or end.");
|
|
109
|
+
}
|
|
110
|
+
if (!isPlainObject(compositionSpec.padding)) {
|
|
111
|
+
throw new TypeError("compositionSpec.padding must be a plain object.");
|
|
112
|
+
}
|
|
113
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
114
|
+
if (!Number.isFinite(compositionSpec.padding[side]) || compositionSpec.padding[side] < 0) {
|
|
115
|
+
throw new RangeError(
|
|
116
|
+
`compositionSpec.padding.${side} must be a non-negative finite number.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const paddingKeys = Object.keys(compositionSpec.padding);
|
|
121
|
+
if (
|
|
122
|
+
paddingKeys.length !== 4 ||
|
|
123
|
+
paddingKeys.some(key => !["top", "right", "bottom", "left"].includes(key))
|
|
124
|
+
) {
|
|
125
|
+
throw new Error("compositionSpec.padding must contain exactly four sides.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function ownCompositionSpec(compositionSpec, children) {
|
|
130
|
+
const childIds = Object.keys(children);
|
|
131
|
+
if (compositionSpec === undefined) {
|
|
132
|
+
if (childIds.length > 0) {
|
|
133
|
+
throw new Error("ChartProgram children require a compositionSpec.");
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (!isPlainObject(compositionSpec)) {
|
|
138
|
+
throw new TypeError("ChartProgram compositionSpec must be a plain object.");
|
|
139
|
+
}
|
|
140
|
+
const allowed = [
|
|
141
|
+
"id", "type", "direction", "children", "columns", "gap", "align",
|
|
142
|
+
"padding", "facet"
|
|
143
|
+
];
|
|
144
|
+
const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
|
|
145
|
+
if (unknown !== undefined) {
|
|
146
|
+
throw new Error(`Unknown compositionSpec property "${unknown}".`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof compositionSpec.id !== "string" || compositionSpec.id.length === 0) {
|
|
149
|
+
throw new TypeError("compositionSpec.id must be a non-empty string.");
|
|
150
|
+
}
|
|
151
|
+
const facet = compositionSpec.type === "facet";
|
|
152
|
+
if (compositionSpec.type !== undefined && !facet) {
|
|
153
|
+
throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
|
|
154
|
+
}
|
|
155
|
+
if (!facet && !["horizontal", "vertical"].includes(compositionSpec.direction)) {
|
|
156
|
+
throw new Error("compositionSpec.direction must be horizontal or vertical.");
|
|
157
|
+
}
|
|
158
|
+
validateCompositionChildren(compositionSpec, childIds, facet);
|
|
159
|
+
if (facet) {
|
|
160
|
+
validateFacetSpec(compositionSpec);
|
|
161
|
+
} else if (
|
|
162
|
+
compositionSpec.columns !== undefined ||
|
|
163
|
+
compositionSpec.facet !== undefined
|
|
164
|
+
) {
|
|
165
|
+
throw new Error("Concat compositionSpec does not accept facet properties.");
|
|
166
|
+
}
|
|
167
|
+
validateLayout(compositionSpec);
|
|
168
|
+
return ownProgramState(compositionSpec);
|
|
169
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneAndFreeze,
|
|
3
|
+
freezeOwned,
|
|
4
|
+
isPlainObject,
|
|
5
|
+
removeOwnedPath
|
|
6
|
+
} from "./immutable.js";
|
|
7
|
+
|
|
8
|
+
function validatePath(path) {
|
|
9
|
+
if (
|
|
10
|
+
!Array.isArray(path) ||
|
|
11
|
+
path.length === 0 ||
|
|
12
|
+
!path.every(key => typeof key === "string" && key.length > 0)
|
|
13
|
+
) {
|
|
14
|
+
throw new TypeError("Materialization config path must contain names.");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateConfig(config) {
|
|
19
|
+
if (!isPlainObject(config)) {
|
|
20
|
+
throw new TypeError("Materialization config must be a plain object.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function updateConfigPath(value, path, config) {
|
|
25
|
+
if (path.length === 0) return cloneAndFreeze(config);
|
|
26
|
+
const [key, ...rest] = path;
|
|
27
|
+
return freezeOwned({
|
|
28
|
+
...value,
|
|
29
|
+
[key]: updateConfigPath(value?.[key] ?? {}, rest, config)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createMaterializationConfigs(
|
|
34
|
+
markConfigs,
|
|
35
|
+
guideConfigs,
|
|
36
|
+
titleConfig,
|
|
37
|
+
canvasConfig
|
|
38
|
+
) {
|
|
39
|
+
return cloneAndFreeze({
|
|
40
|
+
marks: markConfigs,
|
|
41
|
+
guides: guideConfigs,
|
|
42
|
+
...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
|
|
43
|
+
...(titleConfig === undefined ? {} : { title: titleConfig })
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setMaterializationConfig(configs, path, config) {
|
|
48
|
+
validatePath(path);
|
|
49
|
+
validateConfig(config);
|
|
50
|
+
return updateConfigPath(configs, path, config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function removeMaterializationConfig(configs, path) {
|
|
54
|
+
validatePath(path);
|
|
55
|
+
const removed = removeOwnedPath(configs, path);
|
|
56
|
+
if (!removed.removed) return { removed: false, value: configs };
|
|
57
|
+
return {
|
|
58
|
+
removed: true,
|
|
59
|
+
value: freezeOwned({
|
|
60
|
+
...removed.value,
|
|
61
|
+
marks: removed.value.marks ?? freezeOwned({}),
|
|
62
|
+
guides: removed.value.guides ?? freezeOwned({})
|
|
63
|
+
})
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneAndFreeze,
|
|
3
|
+
freezeOwned,
|
|
4
|
+
isOwned,
|
|
5
|
+
isPlainObject
|
|
6
|
+
} from "./immutable.js";
|
|
7
|
+
|
|
8
|
+
export function ownProgramState(value) {
|
|
9
|
+
return isOwned(value) ? value : cloneAndFreeze(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ownChildPrograms(children, ProgramClass) {
|
|
13
|
+
if (!isPlainObject(children)) {
|
|
14
|
+
throw new TypeError("ChartProgram children must be a plain object.");
|
|
15
|
+
}
|
|
16
|
+
if (isOwned(children)) return children;
|
|
17
|
+
const owned = {};
|
|
18
|
+
for (const [id, program] of Object.entries(children)) {
|
|
19
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
20
|
+
throw new TypeError("ChartProgram child IDs must be non-empty strings.");
|
|
21
|
+
}
|
|
22
|
+
if (!(program instanceof ProgramClass)) {
|
|
23
|
+
throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
|
|
24
|
+
}
|
|
25
|
+
owned[id] = program;
|
|
26
|
+
}
|
|
27
|
+
return freezeOwned(owned);
|
|
28
|
+
}
|
package/src/grammar/arcs.js
CHANGED
package/src/grammar/density.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cloneAndFreeze, isPlainObject } from "
|
|
2
|
-
import { validateUserId } from "
|
|
3
|
-
import { BAR_GRAINS, resolveBarGrain } from "
|
|
4
|
-
import { planFacetDependencies } from "./
|
|
5
|
-
import { readNominalField } from "
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
|
|
4
|
+
import { planFacetDependencies } from "./dependencies.js";
|
|
5
|
+
import { readNominalField } from "../scales/index.js";
|
|
6
6
|
|
|
7
7
|
const SUPPORTED_MARKS = new Set(["point", "line", "area", "bar", "rule"]);
|
|
8
8
|
const SUPPORTED_BAR_GRAINS = new Set([
|