ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -0,0 +1,321 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../../core/validation.js";
4
+ import { DEFAULT_TEXT_MARK } from "../../../grammar/text.js";
5
+ import { findGraphicParent } from "../../../grammar/schemas/graphicTree.js";
6
+ import { validateConcreteGraphicValue } from
7
+ "../../../grammar/schemas/concreteGraphic.js";
8
+ import {
9
+ normalizeLabelLayoutGeometry,
10
+ resolveLabelLayout,
11
+ resolveLabelLeader
12
+ } from "../../../layout/labels.js";
13
+ import { resolveGraphicBounds } from "../../../layout/canvas.js";
14
+ import { canMaterializeText } from "../../../materialization/marks/index.js";
15
+ import { resolveTextGraphicEntries } from "../../../materialization/text.js";
16
+ import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
17
+
18
+ const LAYOUT_OPTIONS = Object.freeze([
19
+ "target", "axis", "padding", "maxDisplacement", "bounds", "leader"
20
+ ]);
21
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
22
+ const MATERIALIZE_OPTIONS = Object.freeze(["id", "rematerializeBase"]);
23
+ const LEADER_OPTIONS = Object.freeze([
24
+ "stroke", "strokeWidth", "strokeDash", "opacity"
25
+ ]);
26
+
27
+ function normalizeLeader(value) {
28
+ if (value === false || value === undefined) return false;
29
+ validateOptionObject(value, LEADER_OPTIONS, "label leader");
30
+ const normalized = {
31
+ stroke: "#94a3b8",
32
+ strokeWidth: 1,
33
+ strokeDash: [],
34
+ opacity: 1,
35
+ ...value
36
+ };
37
+ for (const [property, propertyValue] of Object.entries(normalized)) {
38
+ validateConcreteGraphicValue("line", property, propertyValue);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function normalizePolicy(args) {
44
+ const geometry = normalizeLabelLayoutGeometry(Object.fromEntries(
45
+ Object.entries(args).filter(
46
+ ([key]) => key !== "target" && key !== "leader"
47
+ )
48
+ ));
49
+ return { ...geometry, leader: normalizeLeader(args.leader) };
50
+ }
51
+
52
+ function leaderId(target) {
53
+ return `${target}-label-leaders`;
54
+ }
55
+
56
+ function requireCompleteText(program, requested, operation) {
57
+ const target = requested === undefined
58
+ ? undefined
59
+ : validateUserId(requested, "Text mark id");
60
+ if (target !== undefined) {
61
+ const layer = findLayer(program, target);
62
+ if (layer?.mark?.type !== "text") {
63
+ throw new Error(`Unknown ${operation} text mark target "${target}".`);
64
+ }
65
+ if (!canMaterializeText(program, layer)) {
66
+ throw new Error(`${operation} requires a complete text mark.`);
67
+ }
68
+ return layer;
69
+ }
70
+ return resolveEligibleLayer(program, {
71
+ predicate: layer =>
72
+ layer.mark?.type === "text" && canMaterializeText(program, layer),
73
+ label: `${operation} text mark`
74
+ });
75
+ }
76
+
77
+ function requireConfiguredText(program, requested) {
78
+ const target = requested === undefined
79
+ ? undefined
80
+ : validateUserId(requested, "Text mark id");
81
+ return resolveEligibleLayer(program, {
82
+ target,
83
+ predicate: layer =>
84
+ layer.mark?.type === "text" &&
85
+ program.materializationConfigs.labelLayouts?.[layer.id] !== undefined,
86
+ label: "removeLabelLayout text mark"
87
+ });
88
+ }
89
+
90
+ function concreteBounds(program, kind) {
91
+ if (kind === "plot") {
92
+ const plot = resolveGraphicBounds(program);
93
+ return {
94
+ left: plot.x,
95
+ right: plot.x + plot.width,
96
+ top: plot.y,
97
+ bottom: plot.y + plot.height
98
+ };
99
+ }
100
+ const canvas = program.graphicSpec.objects.canvas;
101
+ if (
102
+ canvas?.type !== "canvas" ||
103
+ !Number.isFinite(canvas.properties.width) ||
104
+ !Number.isFinite(canvas.properties.height)
105
+ ) {
106
+ throw new Error("Canvas label bounds require concrete Canvas dimensions.");
107
+ }
108
+ const x = canvas.properties.x ?? 0;
109
+ const y = canvas.properties.y ?? 0;
110
+ return {
111
+ left: x,
112
+ right: x + canvas.properties.width,
113
+ top: y,
114
+ bottom: y + canvas.properties.height
115
+ };
116
+ }
117
+
118
+ function layoutInputs(program, layer) {
119
+ const entries = resolveTextGraphicEntries(
120
+ program,
121
+ layer,
122
+ program.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
123
+ );
124
+ const graphic = program.graphicSpec.objects[layer.id];
125
+ if (graphic?.type !== "text" || !Array.isArray(graphic.items)) {
126
+ throw new Error(`Text mark "${layer.id}" requires text collection graphics.`);
127
+ }
128
+ if (entries.length !== graphic.items.length) {
129
+ throw new Error(`Text mark "${layer.id}" base materialization is inconsistent.`);
130
+ }
131
+ return entries.map((entry, index) => ({
132
+ id: graphic.items[index].id,
133
+ x: entry.graphic.properties.x,
134
+ y: entry.graphic.properties.y,
135
+ sourceX: entry.anchor.x,
136
+ sourceY: entry.anchor.y,
137
+ text: entry.graphic.properties.text,
138
+ fontSize: entry.graphic.properties.fontSize,
139
+ textAlign: entry.graphic.properties.textAlign,
140
+ textBaseline: entry.graphic.properties.textBaseline,
141
+ rotation: entry.graphic.properties.rotation
142
+ }));
143
+ }
144
+
145
+ function leaderPlacement(program, layer) {
146
+ const targetParent = findGraphicParent(program.graphicSpec, layer.id);
147
+ if (targetParent?.kind !== "parent") {
148
+ throw new Error("Label leaders require the text graphic to have a parent container.");
149
+ }
150
+ const source = layer.source === undefined
151
+ ? undefined
152
+ : program.graphicSpec.objects[layer.source];
153
+ const sourceParent = source === undefined
154
+ ? undefined
155
+ : findGraphicParent(program.graphicSpec, layer.source);
156
+ const before = sourceParent?.kind === "parent" && sourceParent.id === targetParent.id
157
+ ? layer.source
158
+ : layer.id;
159
+ return { parent: targetParent.id, before };
160
+ }
161
+
162
+ function removeLeaderGraphic(program, id) {
163
+ return program.graphicSpec.objects[id] === undefined
164
+ ? program
165
+ : program.editGraphics({ target: id, remove: true });
166
+ }
167
+
168
+ function materializeLeaders(program, layer, config, resolved) {
169
+ let next = removeLeaderGraphic(program, config.leaderId);
170
+ if (config.leader === false) return { program: next, count: 0 };
171
+ const leaders = resolved.items.map(resolveLabelLeader).filter(Boolean);
172
+ if (leaders.length === 0) return { program: next, count: 0 };
173
+ next = next
174
+ .createGraphics({
175
+ id: config.leaderId,
176
+ type: "line",
177
+ length: leaders.length,
178
+ ...leaderPlacement(next, layer)
179
+ })
180
+ .editGraphics({
181
+ target: config.leaderId,
182
+ property: "x1",
183
+ value: leaders.map(line => line.x1)
184
+ })
185
+ .editGraphics({
186
+ target: config.leaderId,
187
+ property: "y1",
188
+ value: leaders.map(line => line.y1)
189
+ })
190
+ .editGraphics({
191
+ target: config.leaderId,
192
+ property: "x2",
193
+ value: leaders.map(line => line.x2)
194
+ })
195
+ .editGraphics({
196
+ target: config.leaderId,
197
+ property: "y2",
198
+ value: leaders.map(line => line.y2)
199
+ });
200
+ for (const property of ["stroke", "strokeWidth", "strokeDash", "opacity"]) {
201
+ next = next.editGraphics({
202
+ target: config.leaderId,
203
+ property,
204
+ value: property === "strokeDash"
205
+ ? leaders.map(() => config.leader.strokeDash)
206
+ : config.leader[property]
207
+ });
208
+ }
209
+ return { program: next, count: leaders.length };
210
+ }
211
+
212
+ const materializeLabelLayout = action(
213
+ {
214
+ op: "materializeLabelLayout",
215
+ description: "Resolve concrete label positions and optional leader lines."
216
+ },
217
+ function (args = {}) {
218
+ validateOptionObject(args, MATERIALIZE_OPTIONS, "materializeLabelLayout");
219
+ const id = validateUserId(args.id, "Text mark id");
220
+ const rematerializeBase = args.rematerializeBase ?? true;
221
+ if (typeof rematerializeBase !== "boolean") {
222
+ throw new TypeError("materializeLabelLayout rematerializeBase must be a boolean.");
223
+ }
224
+ const layer = findLayer(this, id);
225
+ if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
226
+ const config = this.materializationConfigs.labelLayouts?.[id];
227
+ if (config === undefined) {
228
+ throw new Error(`Text mark "${id}" has no label layout policy.`);
229
+ }
230
+ let next = rematerializeBase
231
+ ? this.rematerializeTextMark({ id, replayLayout: false })
232
+ : this;
233
+ const resolved = resolveLabelLayout({
234
+ items: canMaterializeText(next, layer) ? layoutInputs(next, layer) : [],
235
+ bounds: concreteBounds(next, config.bounds),
236
+ axis: config.axis,
237
+ padding: config.padding,
238
+ maxDisplacement: config.maxDisplacement
239
+ });
240
+ next = next
241
+ .editGraphics({
242
+ target: id,
243
+ property: "x",
244
+ value: resolved.items.map(item => item.x)
245
+ })
246
+ .editGraphics({
247
+ target: id,
248
+ property: "y",
249
+ value: resolved.items.map(item => item.y)
250
+ });
251
+ const leaders = materializeLeaders(next, layer, config, resolved);
252
+ next = leaders.program;
253
+ return next._withMaterializationConfig(["labelLayouts", id], {
254
+ axis: config.axis,
255
+ padding: config.padding,
256
+ maxDisplacement: config.maxDisplacement,
257
+ bounds: config.bounds,
258
+ leader: config.leader,
259
+ leaderId: config.leaderId,
260
+ resolution: {
261
+ overlapBefore: resolved.overlapBefore,
262
+ overlapAfter: resolved.overlapAfter,
263
+ displaced: resolved.items.filter(item => item.distance > 0).length,
264
+ leaders: leaders.count,
265
+ maximumDisplacement: resolved.items.reduce(
266
+ (maximum, item) => Math.max(maximum, item.distance),
267
+ 0
268
+ ),
269
+ warnings: resolved.warnings
270
+ }
271
+ });
272
+ }
273
+ );
274
+
275
+ const layoutLabels = action(
276
+ {
277
+ op: "layoutLabels",
278
+ description: "Assign deterministic collision-aware layout to a text mark."
279
+ },
280
+ function (args = {}) {
281
+ validateOptionObject(args, LAYOUT_OPTIONS, "layoutLabels");
282
+ const layer = requireCompleteText(this, args.target, "layoutLabels");
283
+ const policy = normalizePolicy(args);
284
+ const generatedId = leaderId(layer.id);
285
+ const existingConfig = this.materializationConfigs.labelLayouts?.[layer.id];
286
+ if (
287
+ this.graphicSpec.objects[generatedId] !== undefined &&
288
+ existingConfig?.leaderId !== generatedId
289
+ ) {
290
+ throw new Error(`Label layout leader graphic "${generatedId}" already exists.`);
291
+ }
292
+ return this
293
+ ._withMaterializationConfig(["labelLayouts", layer.id], {
294
+ ...policy,
295
+ leaderId: generatedId
296
+ })
297
+ .materializeLabelLayout({ id: layer.id });
298
+ }
299
+ );
300
+
301
+ const removeLabelLayout = action(
302
+ {
303
+ op: "removeLabelLayout",
304
+ description: "Remove label layout and restore semantic base text positions."
305
+ },
306
+ function (args = {}) {
307
+ validateOptionObject(args, REMOVE_OPTIONS, "removeLabelLayout");
308
+ const layer = requireConfiguredText(this, args.target);
309
+ const config = this.materializationConfigs.labelLayouts[layer.id];
310
+ return removeLeaderGraphic(
311
+ this._withoutMaterializationConfig(["labelLayouts", layer.id]),
312
+ config.leaderId
313
+ ).rematerializeTextMark({ id: layer.id });
314
+ }
315
+ );
316
+
317
+ export function registerTextLabelLayoutActions(ProgramClass) {
318
+ ProgramClass.prototype.layoutLabels = layoutLabels;
319
+ ProgramClass.prototype.removeLabelLayout = removeLabelLayout;
320
+ ProgramClass.prototype.materializeLabelLayout = materializeLabelLayout;
321
+ }
@@ -0,0 +1,22 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import { isPlainObject } from "../../../core/immutable.js";
3
+ import { validateDatasetTransforms } from "../../../grammar/transforms.js";
4
+ import { hasDataset } from "../../../selectors/datasets.js";
5
+
6
+ export function validateDatasetSemanticValue(program, parsed, value) {
7
+ const property = parsed.path[0];
8
+ if (property === "values") {
9
+ if (!Array.isArray(value) || !value.every(isPlainObject)) {
10
+ throw new TypeError("Dataset values must be an array of plain row objects.");
11
+ }
12
+ return;
13
+ }
14
+ if (property === "source") {
15
+ validateUserId(value, "Dataset source id");
16
+ if (!hasDataset(program, value)) {
17
+ throw new Error(`Unknown source dataset "${value}".`);
18
+ }
19
+ return;
20
+ }
21
+ if (property === "transform") validateDatasetTransforms(value);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import { CATEGORICAL_LEGEND_CHANNELS } from "../../../core/vocabulary.js";
3
+ import { validateNonEmptySemanticString } from "./shared.js";
4
+
5
+ function validateLegend(property, value) {
6
+ if (property === "title") {
7
+ validateNonEmptySemanticString(value, "Legend title");
8
+ return;
9
+ }
10
+ if (!Array.isArray(value) || value.length === 0) {
11
+ throw new TypeError(`Legend ${property} must be a non-empty array.`);
12
+ }
13
+ if (new Set(value).size !== value.length) {
14
+ throw new Error(`Legend ${property} must not contain duplicates.`);
15
+ }
16
+ if (property === "channels") {
17
+ if (!value.every(channel => CATEGORICAL_LEGEND_CHANNELS.includes(channel))) {
18
+ throw new Error("Legend channels support only color, strokeDash, and shape.");
19
+ }
20
+ return;
21
+ }
22
+ for (const id of value) validateUserId(id, "Legend scale id");
23
+ }
24
+
25
+ export function validateGuideSemanticValue(_program, parsed, value) {
26
+ const property = parsed.path.at(-1);
27
+ if (parsed.id === "legend.series") {
28
+ validateLegend(property, value);
29
+ return;
30
+ }
31
+ if (!parsed.id.startsWith("grid.") && !parsed.id.startsWith("axis.")) return;
32
+ if (parsed.id === "axis.parallel" && property === "scales") {
33
+ if (!Array.isArray(value) || value.length < 2) {
34
+ throw new TypeError("Parallel axis scales must contain at least two ids.");
35
+ }
36
+ value.forEach(id => validateUserId(id, "Parallel axis scale id"));
37
+ } else if (property === "title") {
38
+ validateNonEmptySemanticString(value, "Axis title");
39
+ } else {
40
+ const guideKind = parsed.id.startsWith("grid.") ? "Grid" : "Axis";
41
+ validateUserId(value, `${guideKind} ${property} id`);
42
+ }
43
+ }
@@ -0,0 +1,23 @@
1
+ import { validateCoordinateType } from "../../../grammar/coordinates.js";
2
+ import { validateDatasetSemanticValue } from "./dataset.js";
3
+ import { validateGuideSemanticValue } from "./guide.js";
4
+ import { validateLayerSemanticValue } from "./layer.js";
5
+ import { validateScaleSemanticValue } from "./scale.js";
6
+ import { validateNonEmptySemanticString } from "./shared.js";
7
+
8
+ const VALIDATORS = Object.freeze({
9
+ dataset: validateDatasetSemanticValue,
10
+ layer: validateLayerSemanticValue,
11
+ scale: validateScaleSemanticValue,
12
+ coordinate(_program, parsed, value) {
13
+ if (parsed.path[0] === "type") validateCoordinateType(value);
14
+ },
15
+ guide: validateGuideSemanticValue,
16
+ title(_program, parsed, value) {
17
+ validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
18
+ }
19
+ });
20
+
21
+ export function validateSemanticValue(program, parsed, value) {
22
+ VALIDATORS[parsed.kind]?.(program, parsed, value);
23
+ }
@@ -0,0 +1,74 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import {
3
+ COLOR_LAYOUTS,
4
+ MARK_TYPES,
5
+ STACK_MODES
6
+ } from "../../../core/vocabulary.js";
7
+ import { validateAggregate } from "../../../grammar/aggregate.js";
8
+ import {
9
+ normalizeHistogramBin,
10
+ validateHistogramBinBoundaries,
11
+ validateHistogramBinStep
12
+ } from "../../../grammar/histogram.js";
13
+ import {
14
+ validateParallelDimensions,
15
+ validateParallelKeyField,
16
+ validateParallelMissingPolicy
17
+ } from "../../../grammar/parallelCoordinates.js";
18
+ import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
19
+ import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
20
+ import { findLayer } from "../../../selectors/layers.js";
21
+ import { validateNonEmptySemanticString } from "./shared.js";
22
+
23
+ function validateLayerSource(program, parsed, value) {
24
+ validateUserId(value, "Layer source id");
25
+ if (value === parsed.id) {
26
+ throw new Error("A layer cannot use itself as its source.");
27
+ }
28
+ const source = findLayer(program, value);
29
+ if (source === undefined) {
30
+ throw new Error(`Unknown source layer "${value}".`);
31
+ }
32
+ if (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
33
+ throw new Error(
34
+ `Layer source "${value}" must be a point, bar, rule, or rect mark.`
35
+ );
36
+ }
37
+ }
38
+
39
+ export function validateLayerSemanticValue(program, parsed, value) {
40
+ const property = parsed.path.join(".");
41
+ if (property === "mark.type" && !MARK_TYPES.includes(value)) {
42
+ throw new Error(`Unknown mark type "${value}".`);
43
+ }
44
+ if (property === "source") validateLayerSource(program, parsed, value);
45
+ if (property.endsWith(".title")) {
46
+ validateNonEmptySemanticString(value, "Encoding title");
47
+ }
48
+ if (property === "encoding.pathOrder.fieldType" && value !== "quantitative") {
49
+ throw new Error("Path order field type must be quantitative.");
50
+ }
51
+ if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
52
+ if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
53
+ if (property === "encoding.parallel.dimensions") {
54
+ validateParallelDimensions(value, { normalized: true });
55
+ }
56
+ if (property === "encoding.parallel.key") validateParallelKeyField(value);
57
+ if (property === "encoding.parallel.missing") {
58
+ validateParallelMissingPolicy(value);
59
+ }
60
+ if (property.endsWith(".aggregate")) validateAggregate(value);
61
+ if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
62
+ if (property.endsWith(".bin.step")) validateHistogramBinStep(value);
63
+ if (property.endsWith(".bin.boundaries")) validateHistogramBinBoundaries(value);
64
+ if (
65
+ property.endsWith(".stack") &&
66
+ value !== null &&
67
+ !STACK_MODES.includes(value)
68
+ ) {
69
+ throw new Error(`Unsupported stack "${value}".`);
70
+ }
71
+ if (property === "encoding.color.layout" && !COLOR_LAYOUTS.includes(value)) {
72
+ throw new Error(`Unsupported color layout "${value}".`);
73
+ }
74
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ validateContinuousColorInterpolation,
3
+ validateScalePropertyForType,
4
+ validateSemanticScaleDomain,
5
+ validateSemanticScaleRange,
6
+ validateSemanticScaleType
7
+ } from "../../../grammar/scales/index.js";
8
+ import { findSemanticScale } from "../../../selectors/scales.js";
9
+
10
+ function validateOwnedProperty(existing, property) {
11
+ if (existing?.type !== undefined) {
12
+ validateScalePropertyForType(existing.type, property);
13
+ }
14
+ }
15
+
16
+ export function validateScaleSemanticValue(program, parsed, value) {
17
+ const property = parsed.path.join(".");
18
+ const existing = findSemanticScale(program, parsed.id);
19
+ if (property === "type") {
20
+ validateSemanticScaleType(value);
21
+ for (const owned of [
22
+ "nice", "zero", "clamp", "base", "exponent", "constant",
23
+ "paddingInner", "paddingOuter", "padding", "align"
24
+ ]) {
25
+ if (existing?.[owned] !== undefined) validateScalePropertyForType(value, owned);
26
+ }
27
+ return;
28
+ }
29
+ if (property === "domain") return validateSemanticScaleDomain(value);
30
+ if (property === "range") return validateSemanticScaleRange(value);
31
+ if (["nice", "zero", "clamp", "reverse"].includes(property)) {
32
+ if (typeof value !== "boolean") {
33
+ throw new TypeError(`Scale ${property} must be a boolean.`);
34
+ }
35
+ if (property !== "reverse") validateOwnedProperty(existing, property);
36
+ return;
37
+ }
38
+ if (["base", "exponent", "constant"].includes(property)) {
39
+ if (!Number.isFinite(value) || value <= 0) {
40
+ throw new RangeError(`Scale ${property} must be a positive finite number.`);
41
+ }
42
+ if (property === "base" && value === 1) {
43
+ throw new RangeError("Scale base must not equal 1.");
44
+ }
45
+ validateOwnedProperty(existing, property);
46
+ return;
47
+ }
48
+ if (property === "interpolate") {
49
+ validateContinuousColorInterpolation(value);
50
+ return;
51
+ }
52
+ if (property === "paddingInner") {
53
+ if (!Number.isFinite(value) || value < 0 || value >= 1) {
54
+ throw new RangeError(
55
+ "Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
56
+ );
57
+ }
58
+ validateOwnedProperty(existing, property);
59
+ return;
60
+ }
61
+ if (property === "paddingOuter" || property === "padding") {
62
+ if (!Number.isFinite(value) || value < 0) {
63
+ throw new RangeError(`Scale ${property} must be a non-negative finite number.`);
64
+ }
65
+ validateOwnedProperty(existing, property);
66
+ return;
67
+ }
68
+ if (property === "align") {
69
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
70
+ throw new RangeError("Scale align must be between 0 and 1.");
71
+ }
72
+ validateOwnedProperty(existing, property);
73
+ }
74
+ }
@@ -0,0 +1,5 @@
1
+ export function validateNonEmptySemanticString(value, label) {
2
+ if (typeof value !== "string" || value.length === 0) {
3
+ throw new TypeError(`${label} must be a non-empty string.`);
4
+ }
5
+ }