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,283 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import { isPlainObject } from "../../../core/immutable.js";
3
+ import { validateOptionObject } from "../../../core/validation.js";
4
+ import { resolvePalette } from "../../../grammar/palettes.js";
5
+ import {
6
+ normalizeTemporalValue,
7
+ readNominalField
8
+ } from "../../../grammar/scales/index.js";
9
+ import {
10
+ findDataset,
11
+ findSemanticScale,
12
+ hasDataset,
13
+ resolveEligibleLayer
14
+ } from "../../../selectors/index.js";
15
+
16
+ export function findHorizonArea(program, requested) {
17
+ const target = requested === undefined
18
+ ? undefined
19
+ : validateUserId(requested, "Horizon target id");
20
+ return resolveEligibleLayer(program, {
21
+ target,
22
+ label: "Horizon area",
23
+ predicate: layer => layer.mark?.type === "area"
24
+ });
25
+ }
26
+
27
+ export function resolveHorizonSource(program, layer, requested) {
28
+ const id = validateUserId(
29
+ requested ?? layer.data ?? program.context.currentData,
30
+ "Horizon source dataset id"
31
+ );
32
+ if (!hasDataset(program, id)) {
33
+ throw new Error(`Unknown Horizon source dataset "${id}".`);
34
+ }
35
+ return findDataset(program, id);
36
+ }
37
+
38
+ function fieldOption(value, role) {
39
+ if (typeof value === "string") return { field: value };
40
+ if (!isPlainObject(value)) {
41
+ throw new TypeError(`Horizon ${role} must be a field string or plain object.`);
42
+ }
43
+ validateOptionObject(
44
+ value,
45
+ ["field", "fieldType", "scale"],
46
+ `Horizon ${role}`
47
+ );
48
+ return value;
49
+ }
50
+
51
+ function inferredFieldType(rows, field, role) {
52
+ if (role === "y") {
53
+ const valid = rows.filter(row => Number.isFinite(row?.[field]));
54
+ if (valid.length === 0) {
55
+ throw new TypeError(`Horizon y field "${field}" requires a finite value.`);
56
+ }
57
+ return "quantitative";
58
+ }
59
+ if (rows.every(row => Number.isFinite(row?.[field]))) return "quantitative";
60
+ for (const [index, row] of rows.entries()) {
61
+ normalizeTemporalValue(row?.[field], field, index);
62
+ }
63
+ return "temporal";
64
+ }
65
+
66
+ function compatibleEncoding(encoding, role) {
67
+ return encoding?.field !== undefined && (
68
+ role === "x"
69
+ ? ["quantitative", "temporal"].includes(encoding.fieldType)
70
+ : encoding.fieldType === "quantitative"
71
+ );
72
+ }
73
+
74
+ function uniqueStoredEncoding(program, dataset, role) {
75
+ const candidates = program.semanticSpec.layers
76
+ .filter(layer => layer.data === dataset.id)
77
+ .map(layer => layer.encoding?.[role])
78
+ .filter(encoding => compatibleEncoding(encoding, role));
79
+ const unique = new Map(candidates.map(encoding => [
80
+ JSON.stringify([
81
+ encoding.field,
82
+ encoding.fieldType,
83
+ encoding.scale,
84
+ encoding.title
85
+ ]),
86
+ encoding
87
+ ]));
88
+ if (unique.size > 1) {
89
+ throw new Error(`Horizon ${role} inference is ambiguous; provide ${role}.`);
90
+ }
91
+ return [...unique.values()][0];
92
+ }
93
+
94
+ export function resolveHorizonField(program, layer, dataset, requested, role) {
95
+ const existing = compatibleEncoding(layer.encoding?.[role], role)
96
+ ? layer.encoding[role]
97
+ : undefined;
98
+ if (requested === undefined) {
99
+ const inferred = existing ?? uniqueStoredEncoding(program, dataset, role);
100
+ if (inferred === undefined) {
101
+ throw new Error(`encodeHorizon requires ${role} or one inferable ${role} encoding.`);
102
+ }
103
+ return {
104
+ field: inferred.field,
105
+ fieldType: inferred.fieldType,
106
+ ...(role === "x" && inferred.scale !== undefined
107
+ ? { scale: { id: inferred.scale } }
108
+ : {}),
109
+ ...(inferred.title === undefined ? {} : { title: inferred.title })
110
+ };
111
+ }
112
+ const option = fieldOption(requested, role);
113
+ if (typeof option.field !== "string" || option.field.length === 0) {
114
+ throw new TypeError(`Horizon ${role} field must be a non-empty string.`);
115
+ }
116
+ const matching = existing?.field === option.field ? existing : undefined;
117
+ if (option.scale !== undefined && !isPlainObject(option.scale)) {
118
+ throw new TypeError(`Horizon ${role} scale must be a plain object.`);
119
+ }
120
+ const fieldType = option.fieldType ?? matching?.fieldType ??
121
+ inferredFieldType(dataset.values, option.field, role);
122
+ if (
123
+ (role === "x" && !["quantitative", "temporal"].includes(fieldType)) ||
124
+ (role === "y" && fieldType !== "quantitative")
125
+ ) {
126
+ throw new Error(
127
+ `Horizon ${role} fieldType must be ${role === "x" ? "quantitative or temporal" : "quantitative"}.`
128
+ );
129
+ }
130
+ return {
131
+ field: option.field,
132
+ fieldType,
133
+ ...(option.scale !== undefined
134
+ ? { scale: option.scale }
135
+ : role === "x" && matching?.scale !== undefined
136
+ ? { scale: { id: matching.scale } }
137
+ : {}),
138
+ ...(matching?.title === undefined ? {} : { title: matching.title })
139
+ };
140
+ }
141
+
142
+ export function resolveHorizonGroupBy(layer, dataset, requested) {
143
+ if (requested === false) {
144
+ throw new Error("encodeHorizon groupBy must be a field string when provided.");
145
+ }
146
+ const value = requested ?? layer.encoding?.group?.field;
147
+ if (value === undefined) return undefined;
148
+ if (typeof value !== "string" || value.length === 0) {
149
+ throw new TypeError("Horizon groupBy must be a non-empty string.");
150
+ }
151
+ readNominalField(dataset.values, value);
152
+ return value;
153
+ }
154
+
155
+ export function horizonOutputFields(target) {
156
+ const prefix = `${target}Horizon`;
157
+ return {
158
+ x: `${prefix}X`, lower: `${prefix}Lower`, upper: `${prefix}Upper`,
159
+ group: `${prefix}Group`, color: `${prefix}Color`, sign: `${prefix}Sign`,
160
+ band: `${prefix}Band`, segment: `${prefix}Segment`
161
+ };
162
+ }
163
+
164
+ export function resolveHorizonPaletteColors(transform) {
165
+ const domain = [];
166
+ const range = [];
167
+ for (const sign of ["negative", "positive"]) {
168
+ const palette = transform.palette[sign];
169
+ const dense = palette.count === undefined && transform.bands > 1
170
+ ? resolvePalette({ ...palette, count: transform.bands * 2 }, transform.bands * 2)
171
+ : resolvePalette(palette, transform.bands);
172
+ const colors = palette.count === undefined && transform.bands > 1
173
+ ? Array.from({ length: transform.bands }, (_, index) =>
174
+ index === transform.bands - 1 ? dense.at(-1) : dense[index * 2])
175
+ : dense;
176
+ for (let band = 0; band < transform.bands; band += 1) {
177
+ domain.push(`${sign}:${band}`);
178
+ range.push(colors[band]);
179
+ }
180
+ }
181
+ return { domain, range };
182
+ }
183
+
184
+ export function resolveFoldedHorizonScaleOptions(target, requested) {
185
+ if (requested !== undefined && !isPlainObject(requested)) {
186
+ throw new TypeError("Horizon y scale must be a plain object.");
187
+ }
188
+ const options = requested ?? {};
189
+ if (
190
+ options.domain !== undefined &&
191
+ (!Array.isArray(options.domain) ||
192
+ options.domain.length !== 2 ||
193
+ options.domain[0] !== 0 ||
194
+ options.domain[1] !== 1)
195
+ ) {
196
+ throw new Error("Horizon y scale domain must be [0, 1].");
197
+ }
198
+ if (options.type !== undefined && options.type !== "linear") {
199
+ throw new Error('Horizon y scale type must be "linear".');
200
+ }
201
+ return {
202
+ ...options,
203
+ id: options.id ?? `${target}HorizonAmplitude`,
204
+ type: "linear",
205
+ domain: [0, 1],
206
+ nice: false,
207
+ zero: true
208
+ };
209
+ }
210
+
211
+ export function inferHorizonTitle(field) {
212
+ const words = field.replaceAll("_", " ");
213
+ return words.length === 0 ? field : words[0].toUpperCase() + words.slice(1);
214
+ }
215
+
216
+ export function validateUnusedHorizonEncodings(layer) {
217
+ const conflicts = ["x2", "y2", "color", "pathOrder"].filter(
218
+ channel => layer.encoding?.[channel] !== undefined
219
+ );
220
+ if (conflicts.length > 0) {
221
+ throw new Error(
222
+ `Horizon area target "${layer.id}" already has unsupported ${conflicts.join(", ")} encoding.`
223
+ );
224
+ }
225
+ }
226
+
227
+ export function findExistingHorizonScale(program, id, label) {
228
+ const scale = findSemanticScale(program, id);
229
+ if (scale === undefined) throw new Error(`${label} scale "${id}" is missing.`);
230
+ return scale;
231
+ }
232
+
233
+ export function resolveEditedHorizonField(
234
+ program,
235
+ layer,
236
+ source,
237
+ transform,
238
+ requested,
239
+ role
240
+ ) {
241
+ if (requested === undefined) {
242
+ return {
243
+ ...transform[role],
244
+ ...(role === "x" ? { title: layer.encoding.x.title } : {})
245
+ };
246
+ }
247
+ const prior = transform[role];
248
+ return resolveHorizonField(program, {
249
+ ...layer,
250
+ encoding: {
251
+ ...layer.encoding,
252
+ [role]: {
253
+ field: prior.field,
254
+ fieldType: prior.fieldType,
255
+ scale: layer.encoding[role].scale,
256
+ ...(role === "x" && layer.encoding.x.title !== undefined
257
+ ? { title: layer.encoding.x.title }
258
+ : {})
259
+ }
260
+ }
261
+ }, source, requested, role);
262
+ }
263
+
264
+ export function resolveEditedHorizonGroupBy(source, previous, requested) {
265
+ if (requested === undefined) return previous;
266
+ if (requested === false) return undefined;
267
+ if (typeof requested !== "string" || requested.length === 0) {
268
+ throw new TypeError("Horizon groupBy must be a field string or false.");
269
+ }
270
+ readNominalField(source.values, requested);
271
+ return requested;
272
+ }
273
+
274
+ export function mergeHorizonPalette(previous, requested) {
275
+ if (requested === undefined) return previous;
276
+ if (!isPlainObject(requested)) {
277
+ throw new TypeError("Horizon palette must be a plain object.");
278
+ }
279
+ return {
280
+ positive: requested.positive ?? previous.positive,
281
+ negative: requested.negative ?? previous.negative
282
+ };
283
+ }
@@ -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,31 @@ 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";
15
+ import { registerEncodingRemovalAction } from "./remove.js";
12
16
 
13
17
  export function registerEncodingActions(ProgramClass) {
14
18
  registerPositionEncodingActions(ProgramClass);
15
19
  registerRangedEncodingActions(ProgramClass);
16
20
  registerOffsetEncodingAction(ProgramClass);
21
+ registerPathOrderEncodingActions(ProgramClass);
17
22
  registerBarWidthEncodingAction(ProgramClass);
18
23
  registerHistogramEncodingAction(ProgramClass);
24
+ registerHorizonEncodingAction(ProgramClass);
19
25
  registerDensityEncodingAction(ProgramClass);
20
26
  registerColorEncodingAction(ProgramClass);
21
27
  registerStrokeDashEncodingActions(ProgramClass);
22
28
  registerAppearanceEncodingAction(ProgramClass);
23
29
  registerRuleAppearanceEncodingActions(ProgramClass);
24
30
  registerTextEncodingAction(ProgramClass);
31
+ registerParallelEncodingAction(ProgramClass);
32
+ registerEncodingRemovalAction(ProgramClass);
25
33
  }