ggaction 0.0.4 → 0.0.5

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 (157) hide show
  1. package/CHANGELOG.md +30 -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 +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  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/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -0,0 +1,364 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
4
+ import {
5
+ deriveHorizon,
6
+ validateHorizonTransform
7
+ } from "../../grammar/horizon.js";
8
+ import {
9
+ normalizeTemporalValue,
10
+ readQuantitativeField
11
+ } from "../../grammar/scales/index.js";
12
+ import {
13
+ resolveColorScaleDefinition,
14
+ resolvePositionScaleDefinition
15
+ } from "../scales/definitions.js";
16
+ import {
17
+ findDataset,
18
+ hasDataset,
19
+ resolveEligibleLayer
20
+ } from "../../selectors/index.js";
21
+ import { applyMaterializationPlan } from
22
+ "../../materialization/dependencies.js";
23
+ import { planHorizonRematerialization } from
24
+ "../../materialization/horizon.js";
25
+ import { planDerivedDataRevision } from
26
+ "../../materialization/dataProvenance.js";
27
+ import { changedScaleEditPatch } from "../scales/patch.js";
28
+ import {
29
+ findExistingHorizonScale,
30
+ findHorizonArea,
31
+ horizonOutputFields,
32
+ inferHorizonTitle,
33
+ mergeHorizonPalette,
34
+ resolveEditedHorizonField,
35
+ resolveEditedHorizonGroupBy,
36
+ resolveFoldedHorizonScaleOptions,
37
+ resolveHorizonField,
38
+ resolveHorizonGroupBy,
39
+ resolveHorizonPaletteColors,
40
+ resolveHorizonSource,
41
+ validateUnusedHorizonEncodings
42
+ } from "./horizon/resolve.js";
43
+
44
+ const OPTIONS = Object.freeze([
45
+ "target", "source", "x", "y", "groupBy", "bands", "baseline", "extent",
46
+ "resolve", "missing", "overflow", "palette"
47
+ ]);
48
+ const EDIT_OPTIONS = OPTIONS;
49
+ const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
50
+
51
+ function clearOwnedEncodings(program, layer) {
52
+ let next = program;
53
+ for (const channel of ["x", "y", "group"]) {
54
+ if (layer.encoding?.[channel] === undefined) continue;
55
+ next = next.editSemantic({
56
+ property: `layer[${layer.id}].encoding.${channel}`,
57
+ remove: true
58
+ });
59
+ }
60
+ return next;
61
+ }
62
+
63
+ function applyHorizonEncoding(program, {
64
+ layer,
65
+ transform,
66
+ xScale,
67
+ yScale,
68
+ colorScale
69
+ }) {
70
+ let next = clearOwnedEncodings(program, layer)
71
+ .encodeX({
72
+ target: layer.id,
73
+ field: transform.as.x,
74
+ fieldType: transform.x.fieldType,
75
+ scale: xScale
76
+ })
77
+ .editSemantic({
78
+ property: `layer[${layer.id}].encoding.x.title`,
79
+ value: layer.encoding?.x?.title ?? inferHorizonTitle(transform.x.field)
80
+ })
81
+ .encodeY({
82
+ target: layer.id,
83
+ field: transform.as.lower,
84
+ fieldType: "quantitative",
85
+ scale: yScale
86
+ })
87
+ .encodeGroup({ target: layer.id, field: transform.as.group })
88
+ .encodeY2({
89
+ target: layer.id,
90
+ field: transform.as.upper,
91
+ fieldType: "quantitative"
92
+ })
93
+ .encodeColor({
94
+ target: layer.id,
95
+ field: transform.as.color,
96
+ fieldType: "nominal",
97
+ scale: colorScale
98
+ })
99
+ .editAreaMark({ target: layer.id, opacity: 1 });
100
+ return next;
101
+ }
102
+
103
+ function applyScaleDefinition(program, current, definition) {
104
+ const patch = changedScaleEditPatch(current, definition);
105
+ return patch === undefined ? program : program.editScale(patch);
106
+ }
107
+
108
+ const editHorizon = action(
109
+ {
110
+ op: "editHorizon",
111
+ description: "Revise one Horizon transform and rematerialize its consumers."
112
+ },
113
+ function (args = {}) {
114
+ validateOptionObject(args, EDIT_OPTIONS, "editHorizon");
115
+ if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
116
+ throw new Error("editHorizon requires at least one Horizon option.");
117
+ }
118
+ const layer = resolveEligibleLayer(this, {
119
+ target: args.target,
120
+ label: "Horizon area",
121
+ predicate: candidate => {
122
+ const dataset = findDataset(this, candidate.data);
123
+ return candidate.mark?.type === "area" &&
124
+ dataset?.transform?.length === 1 &&
125
+ dataset.transform[0].type === "horizon";
126
+ }
127
+ });
128
+ const previous = findDataset(this, layer.data);
129
+ const prior = previous.transform[0];
130
+ const sourceId = validateUserId(
131
+ args.source ?? previous.source,
132
+ "Horizon source dataset id"
133
+ );
134
+ const source = findDataset(this, sourceId);
135
+ if (source === undefined) {
136
+ throw new Error(`Unknown Horizon source dataset "${sourceId}".`);
137
+ }
138
+ const x = resolveEditedHorizonField(this, layer, source, prior, args.x, "x");
139
+ const y = resolveEditedHorizonField(this, layer, source, prior, args.y, "y");
140
+ const groupBy = resolveEditedHorizonGroupBy(
141
+ source,
142
+ prior.groupBy,
143
+ args.groupBy
144
+ );
145
+ const candidate = validateHorizonTransform({
146
+ type: "horizon",
147
+ x: { field: x.field, fieldType: x.fieldType },
148
+ y: { field: y.field, fieldType: y.fieldType },
149
+ ...(groupBy === undefined ? {} : { groupBy }),
150
+ bands: args.bands ?? prior.bands,
151
+ baseline: args.baseline ?? prior.baseline,
152
+ extent: args.extent ?? prior.extent,
153
+ resolve: args.resolve ?? prior.resolve,
154
+ missing: args.missing ?? prior.missing,
155
+ overflow: args.overflow ?? prior.overflow,
156
+ palette: mergeHorizonPalette(prior.palette, args.palette),
157
+ as: prior.as
158
+ });
159
+ const preflight = deriveHorizon(source.values, candidate);
160
+ const revision = planDerivedDataRevision(this, {
161
+ owner: layer.id,
162
+ role: "HorizonData",
163
+ previous: previous.id,
164
+ consumers: [layer.id]
165
+ });
166
+ let next = this
167
+ .createHorizonData({
168
+ id: revision.id,
169
+ source: source.id,
170
+ x: candidate.x,
171
+ y: candidate.y,
172
+ ...(groupBy === undefined ? {} : { groupBy }),
173
+ bands: candidate.bands,
174
+ baseline: candidate.baseline,
175
+ extent: candidate.extent,
176
+ resolve: candidate.resolve,
177
+ missing: candidate.missing,
178
+ overflow: candidate.overflow,
179
+ palette: candidate.palette,
180
+ as: candidate.as
181
+ })
182
+ .rebindLayerData(revision.rebinds[0])
183
+ .releaseDerivedData(revision.release);
184
+
185
+ if (layer.encoding.x.fieldType !== x.fieldType) {
186
+ next = next.editSemantic({
187
+ property: `layer[${layer.id}].encoding.x.fieldType`,
188
+ value: x.fieldType
189
+ });
190
+ }
191
+ if (Object.hasOwn(args, "x")) {
192
+ next = next.editSemantic({
193
+ property: `layer[${layer.id}].encoding.x.title`,
194
+ value: inferHorizonTitle(x.field)
195
+ });
196
+ }
197
+
198
+ const currentX = findExistingHorizonScale(
199
+ next,
200
+ layer.encoding.x.scale,
201
+ "Horizon x"
202
+ );
203
+ const requestedXScale = x.scale ?? {};
204
+ if (requestedXScale.id !== undefined && requestedXScale.id !== currentX.id) {
205
+ throw new Error("editHorizon x scale cannot change its id.");
206
+ }
207
+ const xTypeChanged = prior.x.fieldType !== x.fieldType;
208
+ const emptyXDomain = preflight.values.length === 0 &&
209
+ requestedXScale.domain === undefined &&
210
+ currentX.domain === "auto"
211
+ ? source.values.map((row, index) => x.fieldType === "temporal"
212
+ ? normalizeTemporalValue(row[x.field], x.field, index)
213
+ : row[x.field])
214
+ : undefined;
215
+ const nextX = resolvePositionScaleDefinition(next, "x", x.fieldType, {
216
+ ...requestedXScale,
217
+ id: currentX.id,
218
+ ...(emptyXDomain === undefined
219
+ ? {}
220
+ : { domain: [Math.min(...emptyXDomain), Math.max(...emptyXDomain)] }),
221
+ ...(xTypeChanged && requestedXScale.type === undefined
222
+ ? { type: x.fieldType === "temporal" ? "time" : "linear" }
223
+ : {})
224
+ });
225
+ next = applyScaleDefinition(next, currentX, nextX);
226
+
227
+ const currentY = findExistingHorizonScale(
228
+ next,
229
+ layer.encoding.y.scale,
230
+ "Horizon y"
231
+ );
232
+ const requestedYScale = y.scale ?? {};
233
+ if (requestedYScale.id !== undefined && requestedYScale.id !== currentY.id) {
234
+ throw new Error("editHorizon y scale cannot change its id.");
235
+ }
236
+ const nextY = resolveFoldedHorizonScaleOptions(layer.id, {
237
+ ...currentY,
238
+ ...requestedYScale,
239
+ id: currentY.id
240
+ });
241
+ next = applyScaleDefinition(next, currentY, nextY);
242
+
243
+ const colors = resolveHorizonPaletteColors(candidate);
244
+ const currentColor = findExistingHorizonScale(
245
+ next,
246
+ layer.encoding.color.scale,
247
+ "Horizon color"
248
+ );
249
+ const nextColor = resolveColorScaleDefinition(next, {
250
+ id: currentColor.id,
251
+ type: "ordinal",
252
+ domain: colors.domain,
253
+ range: colors.range
254
+ });
255
+ next = applyScaleDefinition(next, currentColor, nextColor);
256
+
257
+ if (preflight.values.length === 0) {
258
+ return next.editGraphics({
259
+ target: layer.id,
260
+ property: "length",
261
+ value: 0
262
+ });
263
+ }
264
+ return applyMaterializationPlan(
265
+ next,
266
+ planHorizonRematerialization(next, layer.id)
267
+ );
268
+ }
269
+ );
270
+
271
+ const encodeHorizon = action(
272
+ {
273
+ op: "encodeHorizon",
274
+ description: "Derive and encode one folded Horizon area."
275
+ },
276
+ function (args = {}) {
277
+ validateOptionObject(args, OPTIONS, "encodeHorizon");
278
+ const layer = findHorizonArea(this, args.target);
279
+ validateUnusedHorizonEncodings(layer);
280
+ const source = resolveHorizonSource(this, layer, args.source);
281
+ const x = resolveHorizonField(this, layer, source, args.x, "x");
282
+ const y = resolveHorizonField(this, layer, source, args.y, "y");
283
+ const groupBy = resolveHorizonGroupBy(layer, source, args.groupBy);
284
+ const as = horizonOutputFields(layer.id);
285
+ const transform = validateHorizonTransform({
286
+ type: "horizon",
287
+ x: { field: x.field, fieldType: x.fieldType },
288
+ y: { field: y.field, fieldType: y.fieldType },
289
+ ...(groupBy === undefined ? {} : { groupBy }),
290
+ bands: args.bands ?? 3,
291
+ baseline: args.baseline ?? 0,
292
+ extent: args.extent ?? "auto",
293
+ resolve: args.resolve ?? "shared",
294
+ missing: args.missing ?? "break",
295
+ overflow: args.overflow ?? "clip",
296
+ palette: args.palette ?? {},
297
+ as
298
+ });
299
+ const preflight = deriveHorizon(source.values, transform);
300
+ const id = `${layer.id}HorizonData`;
301
+ if (hasDataset(this, id)) {
302
+ throw new Error(`Dataset "${id}" already exists.`);
303
+ }
304
+ const xScale = {
305
+ ...(x.scale ?? {}),
306
+ ...(preflight.values.length === 0 && x.scale?.domain === undefined
307
+ ? {
308
+ domain: source.values.map((row, index) =>
309
+ x.fieldType === "temporal"
310
+ ? normalizeTemporalValue(row[x.field], x.field, index)
311
+ : row[x.field]
312
+ )
313
+ }
314
+ : {})
315
+ };
316
+ if (Array.isArray(xScale.domain) && xScale.domain.length > 2) {
317
+ xScale.domain = [Math.min(...xScale.domain), Math.max(...xScale.domain)];
318
+ }
319
+ const yScale = resolveFoldedHorizonScaleOptions(layer.id, y.scale);
320
+ const colors = resolveHorizonPaletteColors(transform);
321
+ const colorScale = {
322
+ id: `${layer.id}HorizonColor`,
323
+ type: "ordinal",
324
+ domain: colors.domain,
325
+ range: colors.range
326
+ };
327
+ resolvePositionScaleDefinition(this, "x", x.fieldType, xScale);
328
+ resolvePositionScaleDefinition(this, "y", "quantitative", yScale);
329
+ resolveColorScaleDefinition(this, colorScale);
330
+ if (x.fieldType === "quantitative") readQuantitativeField(source.values, x.field);
331
+
332
+ let next = this
333
+ .createHorizonData({
334
+ id,
335
+ source: source.id,
336
+ x: transform.x,
337
+ y: transform.y,
338
+ ...(groupBy === undefined ? {} : { groupBy }),
339
+ bands: transform.bands,
340
+ baseline: transform.baseline,
341
+ extent: transform.extent,
342
+ resolve: transform.resolve,
343
+ missing: transform.missing,
344
+ overflow: transform.overflow,
345
+ palette: transform.palette,
346
+ as
347
+ })
348
+ .rebindLayerData({ id: layer.id, data: id });
349
+ const stored = findDataset(next, id).transform[0];
350
+ next = applyHorizonEncoding(next, {
351
+ layer,
352
+ transform: stored,
353
+ xScale,
354
+ yScale,
355
+ colorScale
356
+ });
357
+ return next;
358
+ }
359
+ );
360
+
361
+ export function registerHorizonEncodingAction(ProgramClass) {
362
+ ProgramClass.prototype.encodeHorizon = encodeHorizon;
363
+ ProgramClass.prototype.editHorizon = editHorizon;
364
+ }
@@ -3,23 +3,29 @@ import { registerBarWidthEncodingAction } from "./barWidth.js";
3
3
  import { registerColorEncodingAction } from "./color/index.js";
4
4
  import { registerDensityEncodingAction } from "./density.js";
5
5
  import { registerHistogramEncodingAction } from "./histogram.js";
6
+ import { registerHorizonEncodingAction } from "./horizon.js";
6
7
  import { registerOffsetEncodingAction } from "./offset.js";
8
+ import { registerPathOrderEncodingActions } from "./pathOrder.js";
7
9
  import { registerPositionEncodingActions } from "./position/index.js";
8
10
  import { registerRangedEncodingActions } from "./ranged.js";
9
11
  import { registerStrokeDashEncodingActions } from "./strokeDash.js";
10
12
  import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
11
13
  import { registerTextEncodingAction } from "./text.js";
14
+ import { registerParallelEncodingAction } from "./parallel.js";
12
15
 
13
16
  export function registerEncodingActions(ProgramClass) {
14
17
  registerPositionEncodingActions(ProgramClass);
15
18
  registerRangedEncodingActions(ProgramClass);
16
19
  registerOffsetEncodingAction(ProgramClass);
20
+ registerPathOrderEncodingActions(ProgramClass);
17
21
  registerBarWidthEncodingAction(ProgramClass);
18
22
  registerHistogramEncodingAction(ProgramClass);
23
+ registerHorizonEncodingAction(ProgramClass);
19
24
  registerDensityEncodingAction(ProgramClass);
20
25
  registerColorEncodingAction(ProgramClass);
21
26
  registerStrokeDashEncodingActions(ProgramClass);
22
27
  registerAppearanceEncodingAction(ProgramClass);
23
28
  registerRuleAppearanceEncodingActions(ProgramClass);
24
29
  registerTextEncodingAction(ProgramClass);
30
+ registerParallelEncodingAction(ProgramClass);
25
31
  }
@@ -0,0 +1,115 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateOptionObject } from "../../core/validation.js";
3
+ import {
4
+ normalizeParallelDimensions,
5
+ validateParallelKeyField,
6
+ validateParallelMissingPolicy,
7
+ validateParallelRows
8
+ } from "../../grammar/parallelCoordinates.js";
9
+ import { resolvePositionScaleDefinition } from "../scales/definitions.js";
10
+ import { applyEncodingScale, resolveTarget } from "./shared.js";
11
+ import { resolveParallelCoordinate } from "../coordinates/parallel.js";
12
+
13
+ const OPTIONS = Object.freeze([
14
+ "target", "coordinate", "dimensions", "key", "missing"
15
+ ]);
16
+
17
+ export const encodeParallelCoordinates = action(
18
+ {
19
+ op: "encodeParallelCoordinates",
20
+ description: "Atomically encode ordered Parallel-coordinate dimensions."
21
+ },
22
+ function (args = {}) {
23
+ validateOptionObject(args, OPTIONS, "encodeParallelCoordinates");
24
+ const { id: target, dataset, layer } = resolveTarget(
25
+ this,
26
+ args.target,
27
+ ["line"],
28
+ "line mark"
29
+ );
30
+ if (
31
+ ["x", "y", "theta", "radius"].some(
32
+ channel => layer.encoding?.[channel] !== undefined
33
+ )
34
+ ) {
35
+ throw new Error(
36
+ `Parallel encoding cannot mix with existing position encodings on layer "${target}".`
37
+ );
38
+ }
39
+ const coordinate = resolveParallelCoordinate(this, {
40
+ requested: args.coordinate,
41
+ layer,
42
+ operation: "encodeParallelCoordinates"
43
+ });
44
+ const dimensions = normalizeParallelDimensions(
45
+ dataset.values,
46
+ args.dimensions,
47
+ target
48
+ );
49
+ const key = args.key === undefined
50
+ ? undefined
51
+ : validateParallelKeyField(args.key);
52
+ const missing = validateParallelMissingPolicy(args.missing ?? "break");
53
+ validateParallelRows(dataset.values, dimensions, { key, missing });
54
+
55
+ const definitions = dimensions.map(dimension =>
56
+ resolvePositionScaleDefinition(
57
+ this,
58
+ "y",
59
+ dimension.fieldType,
60
+ { ...dimension.scaleOptions, id: dimension.scale },
61
+ dimension.fieldType === "ordinal" ? { discreteType: "point" } : {}
62
+ )
63
+ );
64
+ const storedDimensions = dimensions.map(({ scaleOptions, ...dimension }) => {
65
+ void scaleOptions;
66
+ return dimension;
67
+ });
68
+
69
+ let next = this;
70
+ if (coordinate.create) {
71
+ next = next.createCoordinate({ id: coordinate.id, type: "parallel" });
72
+ }
73
+ if (layer.coordinate === undefined) {
74
+ next = next.editSemantic({
75
+ property: `layer[${target}].coordinate`,
76
+ value: coordinate.id
77
+ });
78
+ }
79
+ next = next
80
+ .editSemantic({
81
+ property: `layer[${target}].encoding.parallel.dimensions`,
82
+ value: storedDimensions
83
+ })
84
+ .editSemantic({
85
+ property: `layer[${target}].encoding.parallel.missing`,
86
+ value: missing
87
+ });
88
+ if (key === undefined && layer.encoding?.parallel?.key !== undefined) {
89
+ next = next.editSemantic({
90
+ property: `layer[${target}].encoding.parallel.key`,
91
+ remove: true
92
+ });
93
+ } else if (key !== undefined) {
94
+ next = next.editSemantic({
95
+ property: `layer[${target}].encoding.parallel.key`,
96
+ value: key
97
+ });
98
+ }
99
+ for (let index = 0; index < definitions.length; index += 1) {
100
+ next = applyEncodingScale(next, definitions[index], dimensions[index].scaleOptions);
101
+ }
102
+ for (const definition of definitions) {
103
+ next = next.rematerializeScale({
104
+ id: definition.id,
105
+ guides: false,
106
+ marks: false
107
+ });
108
+ }
109
+ return next.rematerializeLineMark({ id: target, scales: false });
110
+ }
111
+ );
112
+
113
+ export function registerParallelEncodingAction(ProgramClass) {
114
+ ProgramClass.prototype.encodeParallelCoordinates = encodeParallelCoordinates;
115
+ }
@@ -0,0 +1,131 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateOptionObject
6
+ } from "../../core/validation.js";
7
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
8
+ import { validatePathOrderDirection } from "../../grammar/pathOrder.js";
9
+ import {
10
+ assertPathOrderCompatible,
11
+ pathOrderCompatibility
12
+ } from "../../materialization/marks/pathOrder.js";
13
+ import {
14
+ canMaterializeArea,
15
+ canMaterializeLine
16
+ } from "../../materialization/marks/index.js";
17
+ import { findLayer } from "../../selectors/layers.js";
18
+
19
+ const ENCODE_OPTIONS = Object.freeze([
20
+ "target", "field", "fieldType", "order"
21
+ ]);
22
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
23
+
24
+ function resolvePathOrderLayer(program, target, { active = false } = {}) {
25
+ const requested = target === undefined
26
+ ? undefined
27
+ : validateUserId(target, "Path order target");
28
+ if (requested !== undefined) {
29
+ const layer = findLayer(program, requested);
30
+ if (layer === undefined) {
31
+ throw new Error(`Unknown path order target "${requested}".`);
32
+ }
33
+ if (active && layer.encoding?.pathOrder === undefined) {
34
+ throw new Error(`Mark "${requested}" has no path order encoding.`);
35
+ }
36
+ assertPathOrderCompatible(
37
+ program,
38
+ layer,
39
+ active ? "removePathOrder" : "encodePathOrder"
40
+ );
41
+ return layer;
42
+ }
43
+ const pathLayers = program.semanticSpec.layers.filter(layer =>
44
+ ["line", "area"].includes(layer.mark?.type)
45
+ );
46
+ const candidates = pathLayers.filter(layer =>
47
+ pathOrderCompatibility(program, layer).compatible &&
48
+ (!active || layer.encoding?.pathOrder !== undefined)
49
+ );
50
+ const current = candidates.find(
51
+ layer => layer.id === program.context.currentMark
52
+ );
53
+ if (current !== undefined) return current;
54
+ if (candidates.length === 1) return candidates[0];
55
+ if (candidates.length > 1) {
56
+ throw new Error("path order target is ambiguous; provide target.");
57
+ }
58
+ if (!active && pathLayers.length === 1) {
59
+ assertPathOrderCompatible(program, pathLayers[0], "encodePathOrder");
60
+ }
61
+ if (active && pathLayers.length === 1) {
62
+ throw new Error(`Mark "${pathLayers[0].id}" has no path order encoding.`);
63
+ }
64
+ throw new Error("path order requires an eligible layer.");
65
+ }
66
+
67
+ function rematerializePath(program, layer) {
68
+ if (layer.mark.type === "line") {
69
+ return canMaterializeLine(program, layer)
70
+ ? program.rematerializeLineMark({ id: layer.id })
71
+ : program;
72
+ }
73
+ return canMaterializeArea(program, layer)
74
+ ? program.rematerializeAreaMark({ id: layer.id })
75
+ : program;
76
+ }
77
+
78
+ const encodePathOrder = action(
79
+ {
80
+ op: "encodePathOrder",
81
+ description: "Order vertices within each Cartesian path series."
82
+ },
83
+ function (args = {}) {
84
+ validateOptionObject(args, ENCODE_OPTIONS, "encodePathOrder");
85
+ const field = validateNonEmptyString(args.field, "Path order field");
86
+ const fieldType = args.fieldType ?? "quantitative";
87
+ if (fieldType !== "quantitative") {
88
+ throw new Error("encodePathOrder requires a quantitative field.");
89
+ }
90
+ const order = validatePathOrderDirection(args.order ?? "ascending");
91
+ const layer = resolvePathOrderLayer(this, args.target);
92
+ const dataset = assertPathOrderCompatible(this, layer, "encodePathOrder");
93
+ readQuantitativeField(dataset.values, field);
94
+
95
+ const next = this
96
+ .editSemantic({
97
+ property: `layer[${layer.id}].encoding.pathOrder.field`,
98
+ value: field
99
+ })
100
+ .editSemantic({
101
+ property: `layer[${layer.id}].encoding.pathOrder.fieldType`,
102
+ value: fieldType
103
+ })
104
+ .editSemantic({
105
+ property: `layer[${layer.id}].encoding.pathOrder.order`,
106
+ value: order
107
+ });
108
+ return rematerializePath(next, findLayer(next, layer.id));
109
+ }
110
+ );
111
+
112
+ const removePathOrder = action(
113
+ {
114
+ op: "removePathOrder",
115
+ description: "Remove explicit path order and restore automatic ordering."
116
+ },
117
+ function (args = {}) {
118
+ validateOptionObject(args, REMOVE_OPTIONS, "removePathOrder");
119
+ const layer = resolvePathOrderLayer(this, args.target, { active: true });
120
+ const next = this.editSemantic({
121
+ property: `layer[${layer.id}].encoding.pathOrder`,
122
+ remove: true
123
+ });
124
+ return rematerializePath(next, findLayer(next, layer.id));
125
+ }
126
+ );
127
+
128
+ export function registerPathOrderEncodingActions(ProgramClass) {
129
+ ProgramClass.prototype.encodePathOrder = encodePathOrder;
130
+ ProgramClass.prototype.removePathOrder = removePathOrder;
131
+ }