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.
Files changed (108) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +95 -83
  3. package/package.json +11 -6
  4. package/src/actions/data/regression.js +1 -1
  5. package/src/actions/encodings/appearance.js +1 -1
  6. package/src/actions/encodings/color/continuous.js +137 -0
  7. package/src/actions/encodings/color/index.js +151 -0
  8. package/src/actions/encodings/color/layout.js +91 -0
  9. package/src/actions/encodings/color/policy.js +88 -0
  10. package/src/actions/encodings/index.js +2 -2
  11. package/src/actions/encodings/offset.js +1 -1
  12. package/src/actions/encodings/{position.js → position/index.js} +6 -6
  13. package/src/actions/encodings/position/resolve.js +1 -1
  14. package/src/actions/encodings/ranged.js +2 -2
  15. package/src/actions/encodings/strokeDash.js +1 -1
  16. package/src/actions/encodings/text.js +1 -1
  17. package/src/actions/errorBands/options.js +1 -1
  18. package/src/actions/errorBars/options.js +1 -1
  19. package/src/actions/facets/actions.js +1 -1
  20. package/src/actions/facets/derive.js +2 -2
  21. package/src/actions/facets/guides.js +1 -1
  22. package/src/actions/guides/axes/labels.js +1 -1
  23. package/src/actions/guides/axes/ticks.js +1 -1
  24. package/src/actions/guides/axes/titles.js +1 -1
  25. package/src/actions/guides/grids/resolve.js +1 -1
  26. package/src/actions/guides/legends/categorical/layout.js +4 -2
  27. package/src/actions/guides/legends/continuous/interval.js +1 -1
  28. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  29. package/src/actions/guides/legends/edit.js +2 -20
  30. package/src/actions/guides/legends/remove.js +2 -20
  31. package/src/actions/guides/legends/size.js +1 -1
  32. package/src/actions/guides/legends/target.js +25 -0
  33. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  34. package/src/actions/guides/polar/index.js +1 -1
  35. package/src/actions/guides/polar/resolve.js +1 -1
  36. package/src/actions/guides/tickValues.js +1 -1
  37. package/src/actions/intervals/resolve.js +1 -1
  38. package/src/actions/marks/arc/actions.js +2 -2
  39. package/src/actions/marks/area/actions.js +2 -2
  40. package/src/actions/marks/bar/edit.js +1 -1
  41. package/src/actions/marks/line/actions.js +3 -3
  42. package/src/actions/marks/rect/actions.js +1 -1
  43. package/src/actions/marks/rule/actions.js +1 -1
  44. package/src/actions/marks/shared.js +1 -1
  45. package/src/actions/marks/text/actions.js +1 -1
  46. package/src/actions/primitives/semanticValue.js +1 -1
  47. package/src/actions/regression/components.js +1 -1
  48. package/src/actions/regression/create.js +1 -1
  49. package/src/actions/regression/edit.js +1 -1
  50. package/src/actions/scales/consumers/common.js +91 -0
  51. package/src/actions/scales/consumers/families.js +47 -0
  52. package/src/actions/scales/consumers/index.js +41 -0
  53. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  54. package/src/actions/scales/create.js +1 -1
  55. package/src/actions/scales/definitions.js +2 -2
  56. package/src/actions/scales/edit.js +3 -3
  57. package/src/actions/scales/materialize.js +2 -2
  58. package/src/core/ChartProgram.js +22 -236
  59. package/src/core/compositionState.js +169 -0
  60. package/src/core/materializationState.js +65 -0
  61. package/src/core/programState.js +28 -0
  62. package/src/grammar/arcs.js +1 -1
  63. package/src/grammar/areaSeries.js +1 -1
  64. package/src/grammar/bars/aggregate.js +1 -1
  65. package/src/grammar/density.js +1 -1
  66. package/src/grammar/{facets.js → facets/index.js} +5 -5
  67. package/src/grammar/interval.js +2 -96
  68. package/src/grammar/lineSeries.js +1 -1
  69. package/src/grammar/palettes.js +5 -7
  70. package/src/grammar/polarLineCommands.js +1 -1
  71. package/src/grammar/regression/derive.js +97 -0
  72. package/src/grammar/regression/index.js +22 -0
  73. package/src/grammar/regression/models.js +282 -0
  74. package/src/grammar/regression/parameters.js +106 -0
  75. package/src/grammar/rules.js +1 -1
  76. package/src/grammar/scales/color.js +4 -2
  77. package/src/grammar/statistics/studentT.js +98 -0
  78. package/src/grammar/transforms.js +1 -1
  79. package/src/materialization/bars/aggregate.js +1 -1
  80. package/src/materialization/bars/grouped.js +1 -1
  81. package/src/materialization/bars/histogram.js +1 -1
  82. package/src/materialization/bars/ranged.js +1 -1
  83. package/src/materialization/density.js +1 -1
  84. package/src/materialization/dependencies.js +6 -87
  85. package/src/materialization/encodings.js +2 -2
  86. package/src/materialization/facetGuides/index.js +2 -0
  87. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  88. package/src/materialization/facetGuides/placement.js +126 -0
  89. package/src/materialization/facetGuides/preparation.js +188 -0
  90. package/src/materialization/facets.js +1 -1
  91. package/src/materialization/marks/capabilities.js +141 -0
  92. package/src/materialization/marks/index.js +105 -0
  93. package/src/materialization/marks/policies.js +87 -0
  94. package/src/materialization/rect.js +1 -1
  95. package/src/materialization/rowEncoding.js +1 -1
  96. package/src/materialization/scaleGuideDependencies.js +94 -0
  97. package/src/materialization/scales/map.js +1 -1
  98. package/src/materialization/scales/policies/series.js +1 -1
  99. package/src/materialization/scales/resolve.js +1 -1
  100. package/src/materialization/selection/items/bar.js +1 -1
  101. package/src/materialization/selection/styles.js +1 -1
  102. package/src/renderers/canvas/text.js +14 -1
  103. package/types/extension.d.ts +4 -1
  104. package/src/actions/encodings/color.js +0 -424
  105. package/src/grammar/regression.js +0 -578
  106. package/src/grammar/scales.js +0 -1
  107. package/src/materialization/facetGuides.js +0 -426
  108. package/src/materialization/marks.js +0 -316
@@ -6,9 +6,7 @@ import {
6
6
  import {
7
7
  cloneAndFreeze,
8
8
  freezeOwned,
9
- isOwned,
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
- function ownState(value) {
25
- return isOwned(value) ? value : cloneAndFreeze(value);
26
- }
27
-
28
- function ownChildPrograms(children) {
29
- if (!isPlainObject(children)) {
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 = ownState(semanticSpec);
239
- this.graphicSpec = ownState(graphicSpec);
240
- this.resolvedScales = ownState(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
- : ownState(materializationConfigs);
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 = ownState(context);
252
- this.trace = ownState(trace);
253
- this.actionStack = ownState(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: updateConfigPath(
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
- if (
410
- !Array.isArray(path) ||
411
- path.length === 0 ||
412
- !path.every(key => typeof key === "string" && key.length > 0)
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
+ }
@@ -4,7 +4,7 @@ import {
4
4
  mapOrdinalPositionValues,
5
5
  readNominalField,
6
6
  readQuantitativeField
7
- } from "./scales.js";
7
+ } from "./scales/index.js";
8
8
 
9
9
  function requireArcLayer(layer) {
10
10
  if (layer?.mark?.type !== "arc") {
@@ -3,7 +3,7 @@ import {
3
3
  readNominalField,
4
4
  readQuantitativeField,
5
5
  readTemporalField
6
- } from "./scales.js";
6
+ } from "./scales/index.js";
7
7
  import {
8
8
  layoutSeriesPartition,
9
9
  validateColorLayout
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
- import { readNominalField, readTemporalField } from "../scales.js";
2
+ import { readNominalField, readTemporalField } from "../scales/index.js";
3
3
  import {
4
4
  aggregateRows,
5
5
  validateAggregateFieldType,
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
- import { isNominalValue } from "./scales.js";
2
+ import { isNominalValue } from "./scales/index.js";
3
3
 
4
4
  const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
5
5
 
@@ -1,8 +1,8 @@
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 "./facets/dependencies.js";
5
- import { readNominalField } from "./scales.js";
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([