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,268 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
4
+ import {
5
+ getPositionChannelDefinition,
6
+ POSITION_ENCODING_CHANNELS
7
+ } from "../../core/vocabulary.js";
8
+ import {
9
+ resolveBarChannels,
10
+ resolveBarColorLayout,
11
+ resolveBarOffsetChannel
12
+ } from "../../grammar/bars/policy.js";
13
+ import {
14
+ getMarkMaterializationStep,
15
+ getSourceDependentMarkSteps
16
+ } from "../../materialization/marks/index.js";
17
+ import { findLayer } from "../../selectors/layers.js";
18
+ import { removeLegendKinds } from "../guides/legends/remove.js";
19
+
20
+ const OPTIONS = Object.freeze(["target", "channel"]);
21
+ const REMOVABLE_CHANNELS = Object.freeze([
22
+ "x", "y", "x2", "y2", "xOffset", "yOffset", "theta", "radius",
23
+ "color", "strokeDash", "strokeWidth", "size", "shape", "group",
24
+ "opacity", "text"
25
+ ]);
26
+ const REMOVE_AXIS = Object.freeze({
27
+ x: "removeXAxis",
28
+ y: "removeYAxis",
29
+ theta: "removeThetaAxis",
30
+ radius: "removeRadialAxis"
31
+ });
32
+ const SPECIALIZED_LEGEND_KIND = Object.freeze({
33
+ color: Object.freeze(["gradient", "interval"]),
34
+ size: Object.freeze(["size"]),
35
+ opacity: Object.freeze(["opacity"]),
36
+ strokeWidth: Object.freeze(["strokeWidth"])
37
+ });
38
+
39
+ function resolveTarget(program, requested, channel) {
40
+ const candidates = program.semanticSpec.layers.filter(
41
+ layer => layer.encoding?.[channel] !== undefined
42
+ );
43
+ if (requested !== undefined) {
44
+ const id = validateUserId(requested, "Encoding target id");
45
+ const layer = findLayer(program, id);
46
+ if (layer === undefined) {
47
+ throw new Error(`Unknown encoding target "${id}".`);
48
+ }
49
+ if (layer.encoding?.[channel] === undefined) {
50
+ throw new Error(`Mark "${id}" has no ${channel} encoding.`);
51
+ }
52
+ return layer;
53
+ }
54
+ const current = candidates.find(
55
+ layer => layer.id === program.context.currentMark
56
+ );
57
+ if (current !== undefined) return current;
58
+ if (candidates.length === 1) return candidates[0];
59
+ if (candidates.length === 0) {
60
+ throw new Error(`removeEncoding requires an active ${channel} encoding.`);
61
+ }
62
+ throw new Error(`removeEncoding ${channel} target is ambiguous; provide target.`);
63
+ }
64
+
65
+ function activeCascade(layer, channel) {
66
+ const channels = new Set([channel]);
67
+ if (channel === "x") {
68
+ channels.add("x2");
69
+ channels.add("xOffset");
70
+ }
71
+ if (channel === "y") {
72
+ channels.add("y2");
73
+ channels.add("yOffset");
74
+ }
75
+ if (channel === "color" && layer.mark?.type === "bar") {
76
+ if (resolveBarColorLayout(layer) === "group") {
77
+ channels.add(resolveBarOffsetChannel(layer));
78
+ }
79
+ }
80
+ if (
81
+ channel === "group" &&
82
+ layer.mark?.type === "area" &&
83
+ layer.encoding?.color?.field !== undefined &&
84
+ layer.encoding.color.field === layer.encoding.group?.field
85
+ ) {
86
+ channels.add("color");
87
+ }
88
+ return [...channels].filter(active => layer.encoding?.[active] !== undefined);
89
+ }
90
+
91
+ function assertSelectionCompatibility(program, target, channels) {
92
+ for (const [id, selection] of Object.entries(
93
+ program.materializationConfigs.selections ?? {}
94
+ )) {
95
+ if (
96
+ selection.target === target &&
97
+ channels.includes(selection.selector?.channel)
98
+ ) {
99
+ throw new Error(
100
+ `Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ function removeChannelConfigs(program, target, channels) {
107
+ let next = program;
108
+ for (const channel of channels) {
109
+ if (["xOffset", "yOffset"].includes(channel)) {
110
+ next = next._withoutMaterializationConfig(["marks", target, channel]);
111
+ }
112
+ }
113
+ return next;
114
+ }
115
+
116
+ function restoreBarBaseline(program, layer, channels) {
117
+ if (layer.mark?.type !== "bar" || !channels.includes("color")) {
118
+ return program;
119
+ }
120
+ const measureChannel = resolveBarChannels(layer)?.measure;
121
+ if (
122
+ measureChannel === undefined ||
123
+ layer.encoding?.[measureChannel]?.stack !== "normalize"
124
+ ) {
125
+ return program;
126
+ }
127
+ return program.editSemantic({
128
+ property: `layer[${layer.id}].encoding.${measureChannel}.stack`,
129
+ value: "zero"
130
+ });
131
+ }
132
+
133
+ function hasPrimaryScaleConsumer(program, channel, scale) {
134
+ return program.semanticSpec.layers.some(
135
+ layer => layer.encoding?.[channel]?.scale === scale
136
+ );
137
+ }
138
+
139
+ function cleanupPositionGuides(program, removed) {
140
+ let next = program;
141
+ for (const { channel, scale } of removed) {
142
+ const definition = getPositionChannelDefinition(channel);
143
+ if (
144
+ definition?.guideChannel === undefined ||
145
+ hasPrimaryScaleConsumer(next, channel, scale)
146
+ ) {
147
+ continue;
148
+ }
149
+ const axis = next.semanticSpec.guides.axis?.[definition.guideChannel];
150
+ if (axis?.scale === scale) {
151
+ next = next[REMOVE_AXIS[definition.guideChannel]]({
152
+ scale,
153
+ ...(axis.coordinate === undefined ? {} : { coordinate: axis.coordinate })
154
+ });
155
+ }
156
+ const grid = next.semanticSpec.guides.grid?.[definition.gridDirection];
157
+ if (grid?.scale === scale) {
158
+ next = next.removeGrid({ [definition.gridDirection]: true });
159
+ }
160
+ }
161
+ return next;
162
+ }
163
+
164
+ function reconcileCategoricalLegend(program, target, channels) {
165
+ const entry = ["series", "color"]
166
+ .map(kind => [kind, program.guideConfigs.legend?.[kind]])
167
+ .find(([, config]) =>
168
+ config?.target === target &&
169
+ config.channels.some(channel => channels.includes(channel))
170
+ );
171
+ if (entry === undefined) return program;
172
+ const [kind, config] = entry;
173
+ const layer = findLayer(program, target);
174
+ const remaining = config.channels.filter(
175
+ channel => layer.encoding?.[channel]?.scale !== undefined
176
+ );
177
+ if (remaining.length === 0) {
178
+ return removeLegendKinds(program, [kind]);
179
+ }
180
+ const next = removeLegendKinds(program, [kind]);
181
+ return next.createLegend({
182
+ target,
183
+ channels: remaining,
184
+ position: config.position,
185
+ align: config.align,
186
+ direction: config.direction,
187
+ ...(config.columns === undefined ? {} : { columns: config.columns }),
188
+ offset: config.offset,
189
+ titlePosition: config.titlePosition,
190
+ ...(config.inferredTitle ? {} : { title: config.title }),
191
+ symbol: config.symbol,
192
+ labels: config.labels,
193
+ titleStyle: config.titleStyle,
194
+ itemGap: config.itemGap,
195
+ border: config.border
196
+ });
197
+ }
198
+
199
+ function cleanupLegends(program, target, channels) {
200
+ let next = reconcileCategoricalLegend(program, target, channels);
201
+ const kinds = [...new Set(channels.flatMap(
202
+ channel => SPECIALIZED_LEGEND_KIND[channel] ?? []
203
+ ))].filter(kind => next.guideConfigs.legend?.[kind]?.target === target);
204
+ return kinds.length === 0 ? next : removeLegendKinds(next, kinds);
205
+ }
206
+
207
+ function clearGraphic(program, target) {
208
+ const graphic = program.graphicSpec.objects[target];
209
+ if (graphic === undefined) return program;
210
+ return graphic.type === "collection"
211
+ ? program.editGraphics({ target, property: "items", value: [] })
212
+ : program.editGraphics({ target, property: "length", value: 0 });
213
+ }
214
+
215
+ function rematerializeTarget(program, target) {
216
+ const layer = findLayer(program, target);
217
+ const step = getMarkMaterializationStep(program, layer);
218
+ const baseline = clearGraphic(program, target);
219
+ let next = step === undefined
220
+ ? baseline
221
+ : baseline[step.op](step.args);
222
+ for (const dependent of getSourceDependentMarkSteps(next, target)) {
223
+ next = next[dependent.op](dependent.args);
224
+ }
225
+ return next;
226
+ }
227
+
228
+ export const removeEncoding = action(
229
+ {
230
+ op: "removeEncoding",
231
+ description: "Remove one semantic encoding and its owned materialized state."
232
+ },
233
+ function (args = {}) {
234
+ validateOptionObject(args, OPTIONS, "removeEncoding");
235
+ if (!REMOVABLE_CHANNELS.includes(args.channel)) {
236
+ throw new Error(
237
+ `Unsupported removable encoding channel "${args.channel}".`
238
+ );
239
+ }
240
+ const layer = resolveTarget(this, args.target, args.channel);
241
+ const channels = activeCascade(layer, args.channel);
242
+ assertSelectionCompatibility(this, layer.id, channels);
243
+ const removedPositions = channels
244
+ .filter(channel => POSITION_ENCODING_CHANNELS.includes(channel))
245
+ .map(channel => ({
246
+ channel,
247
+ scale: layer.encoding[channel].scale
248
+ }))
249
+ .filter(item => item.scale !== undefined);
250
+
251
+ let next = this;
252
+ for (const channel of channels) {
253
+ next = next.editSemantic({
254
+ property: `layer[${layer.id}].encoding.${channel}`,
255
+ remove: true
256
+ });
257
+ }
258
+ next = removeChannelConfigs(next, layer.id, channels);
259
+ next = restoreBarBaseline(next, layer, channels);
260
+ next = cleanupLegends(next, layer.id, channels);
261
+ next = cleanupPositionGuides(next, removedPositions);
262
+ return rematerializeTarget(next, layer.id);
263
+ }
264
+ );
265
+
266
+ export function registerEncodingRemovalAction(ProgramClass) {
267
+ ProgramClass.prototype.removeEncoding = removeEncoding;
268
+ }
@@ -3,9 +3,22 @@ import {
3
3
  validateRuleStroke,
4
4
  validateRuleStrokeWidth
5
5
  } from "../../grammar/ruleAppearance.js";
6
- import { resolveTarget, validateOptions } from "./shared.js";
6
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
7
+ import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
8
+ import {
9
+ applyEncodingScale,
10
+ resolveReassignmentScaleOptions,
11
+ resolveTarget,
12
+ validateOptions
13
+ } from "./shared.js";
14
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
15
+ import { planEncodingRematerialization } from "../../materialization/encodings.js";
16
+ import { findLayer } from "../../selectors/layers.js";
7
17
 
8
- const OPTIONS = Object.freeze(["target", "value"]);
18
+ const STROKE_OPTIONS = Object.freeze(["target", "value"]);
19
+ const WIDTH_OPTIONS = Object.freeze([
20
+ "target", "value", "field", "fieldType", "scale"
21
+ ]);
9
22
 
10
23
  const encodeStroke = action(
11
24
  {
@@ -13,7 +26,7 @@ const encodeStroke = action(
13
26
  description: "Set a constant graphical stroke on a rule mark."
14
27
  },
15
28
  function (args = {}) {
16
- validateOptions(args, OPTIONS, "encodeStroke");
29
+ validateOptions(args, STROKE_OPTIONS, "encodeStroke");
17
30
  const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
18
31
  return this
19
32
  ._withMarkConfig(id, {
@@ -30,14 +43,79 @@ const encodeStrokeWidth = action(
30
43
  description: "Set a constant graphical stroke width on a rule mark."
31
44
  },
32
45
  function (args = {}) {
33
- validateOptions(args, OPTIONS, "encodeStrokeWidth");
34
- const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
35
- return this
36
- ._withMarkConfig(id, {
37
- ...this.markConfigs[id],
38
- strokeWidth: validateRuleStrokeWidth(args.value)
46
+ validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
47
+ const hasValue = Object.hasOwn(args, "value");
48
+ const hasField = Object.hasOwn(args, "field");
49
+ if (hasValue === hasField) {
50
+ throw new Error("encodeStrokeWidth requires exactly one of value or field.");
51
+ }
52
+ if (hasValue) {
53
+ const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
54
+ let next = this.guideConfigs.legend?.strokeWidth?.target === id
55
+ ? this.removeLegend({ target: id })
56
+ : this;
57
+ if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
58
+ next = next.editSemantic({
59
+ property: `layer[${id}].encoding.strokeWidth`,
60
+ remove: true
61
+ });
62
+ }
63
+ return next
64
+ ._withMarkConfig(id, {
65
+ ...next.markConfigs[id],
66
+ strokeWidth: validateRuleStrokeWidth(args.value)
67
+ })
68
+ .rematerializeRuleMark({ id });
69
+ }
70
+ const { id, dataset, layer } = resolveTarget(
71
+ this,
72
+ args.target,
73
+ ["rule", "line"],
74
+ "rule or line mark"
75
+ );
76
+ const fieldType = args.fieldType ?? "quantitative";
77
+ if (fieldType !== "quantitative") {
78
+ throw new Error("encodeStrokeWidth requires a quantitative field.");
79
+ }
80
+ const values = readQuantitativeField(dataset.values, args.field);
81
+ if (values.some(value => value < 0)) {
82
+ throw new RangeError(
83
+ `encodeStrokeWidth field "${args.field}" cannot contain negative values.`
84
+ );
85
+ }
86
+ const previous = layer.encoding?.strokeWidth;
87
+ const requestedScale = resolveReassignmentScaleOptions(
88
+ previous,
89
+ args.scale ?? {}
90
+ );
91
+ const scale = resolveStrokeWidthScaleDefinition(this, requestedScale);
92
+ const { strokeWidth, ...config } = this.markConfigs[id] ?? {};
93
+ void strokeWidth;
94
+ let next = this
95
+ ._withMarkConfig(id, config)
96
+ .editSemantic({
97
+ property: `layer[${id}].encoding.strokeWidth.field`,
98
+ value: args.field
39
99
  })
40
- .rematerializeRuleMark({ id });
100
+ .editSemantic({
101
+ property: `layer[${id}].encoding.strokeWidth.fieldType`,
102
+ value: fieldType
103
+ })
104
+ .editSemantic({
105
+ property: `layer[${id}].encoding.strokeWidth.scale`,
106
+ value: scale.id
107
+ });
108
+ next = applyEncodingScale(next, scale, requestedScale, {
109
+ reassignment: previous?.scale === scale.id
110
+ });
111
+ return applyMaterializationPlan(
112
+ next,
113
+ planEncodingRematerialization(next, {
114
+ target: id,
115
+ channel: "strokeWidth",
116
+ scale: scale.id
117
+ })
118
+ );
41
119
  }
42
120
  );
43
121
 
@@ -1,4 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import {
4
5
  validateOptionObject,
@@ -8,12 +9,15 @@ import {
8
9
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
9
10
  import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
10
11
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
12
+ import { planIntervalEdit } from "../data/intervalEdit.js";
11
13
  import {
12
14
  ERROR_BAND_BOUNDARY_OPTIONS,
13
15
  resolveBoundaryAppearance
14
16
  } from "./options.js";
15
17
 
16
- const EDIT_OPTIONS = Object.freeze(["target", "fill", "opacity", "curve"]);
18
+ const EDIT_OPTIONS = Object.freeze([
19
+ "target", "fill", "opacity", "curve", "statistics", "boundaries"
20
+ ]);
17
21
  const BOUNDARY_EDIT_OPTIONS = Object.freeze([
18
22
  "target", "boundary", ...ERROR_BAND_BOUNDARY_OPTIONS
19
23
  ]);
@@ -49,13 +53,16 @@ function currentBoundaryAppearance(program, id) {
49
53
  }
50
54
 
51
55
  function defaultBoundaryAppearance(program, owner) {
52
- return {
53
- stroke: DEFAULT_COLORS.mark,
54
- strokeWidth: 1,
55
- strokeDash: "solid",
56
- opacity: 1,
57
- curve: program.markConfigs[owner]?.curve ?? "linear"
58
- };
56
+ return resolveBoundaryAppearance({}, {
57
+ defaults: {
58
+ stroke: DEFAULT_COLORS.mark,
59
+ strokeWidth: 1,
60
+ strokeDash: "solid",
61
+ opacity: 1,
62
+ curve: program.markConfigs[owner]?.curve ?? "linear"
63
+ },
64
+ operation: "editErrorBand boundaries"
65
+ });
59
66
  }
60
67
 
61
68
  function createBoundary(program, owner, id, bound, appearance) {
@@ -82,6 +89,34 @@ function createBoundary(program, owner, id, bound, appearance) {
82
89
  });
83
90
  }
84
91
 
92
+ function removeBoundary(program, id) {
93
+ if (findLayer(program, id) === undefined) return program;
94
+ const selectionIds = Object.entries(
95
+ program.materializationConfigs.selections ?? {}
96
+ ).filter(([, config]) => config.target === id).map(([selection]) => selection);
97
+ let next = program;
98
+ for (const [highlight, config] of Object.entries(
99
+ program.materializationConfigs.highlights ?? {}
100
+ )) {
101
+ if (config.target === id || selectionIds.includes(config.selection)) {
102
+ next = next._withoutMaterializationConfig(["highlights", highlight]);
103
+ }
104
+ }
105
+ for (const selection of selectionIds) {
106
+ next = next._withoutMaterializationConfig(["selections", selection]);
107
+ }
108
+ return next
109
+ .editSemantic({ property: `layer[${id}]`, remove: true })
110
+ .editGraphics({ target: id, remove: true })
111
+ ._withoutMaterializationConfig(["marks", id])
112
+ ._withContext({
113
+ ...(program.context.currentMark === id ? { currentMark: undefined } : {}),
114
+ ...(selectionIds.includes(program.context.currentSelection)
115
+ ? { currentSelection: undefined }
116
+ : {})
117
+ });
118
+ }
119
+
85
120
  export const rematerializeErrorBandBoundary = action(
86
121
  {
87
122
  op: "rematerializeErrorBandBoundary",
@@ -121,8 +156,9 @@ export const editErrorBand = action(
121
156
  },
122
157
  function (args = {}) {
123
158
  validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
124
- if (!["fill", "opacity", "curve"].some(key => Object.hasOwn(args, key))) {
125
- throw new Error("editErrorBand requires fill, opacity, or curve.");
159
+ if (!["fill", "opacity", "curve", "statistics", "boundaries"]
160
+ .some(key => Object.hasOwn(args, key))) {
161
+ throw new Error("editErrorBand requires at least one change.");
126
162
  }
127
163
  const owner = resolveOwner(this, args.target);
128
164
  const config = { ...this.markConfigs[owner.id] };
@@ -136,9 +172,81 @@ export const editErrorBand = action(
136
172
  if (Object.hasOwn(args, "curve")) {
137
173
  config.curve = validateCurveInterpolation(args.curve);
138
174
  }
139
- return this
140
- ._withMarkConfig(owner.id, { ...config, errorBand })
141
- .rematerializeAreaMark({ id: owner.id });
175
+ const interval = Object.hasOwn(args, "statistics")
176
+ ? planIntervalEdit(this, {
177
+ owner: owner.id,
178
+ data: errorBand.data,
179
+ consumers: [
180
+ owner.id,
181
+ ...[errorBand.lowerBoundaryId, errorBand.upperBoundaryId]
182
+ .filter(id => findLayer(this, id) !== undefined)
183
+ ],
184
+ statistics: args.statistics,
185
+ operation: "editErrorBand"
186
+ })
187
+ : { changed: false };
188
+ if (
189
+ Object.hasOwn(args, "boundaries") &&
190
+ args.boundaries !== false &&
191
+ !isPlainObject(args.boundaries)
192
+ ) {
193
+ throw new TypeError(
194
+ "editErrorBand boundaries must be false or a plain object."
195
+ );
196
+ }
197
+
198
+ const applyEdit = program => {
199
+ let next = program;
200
+ if (interval.changed) {
201
+ next = next.createIntervalData(interval.dataArgs);
202
+ for (const rebind of interval.revision.rebinds) {
203
+ next = next.rebindLayerData(rebind);
204
+ }
205
+ errorBand.data = interval.revision.id;
206
+ }
207
+ next = next
208
+ ._withMarkConfig(owner.id, { ...config, errorBand })
209
+ .rematerializeAreaMark({ id: owner.id });
210
+ if (Object.hasOwn(args, "boundaries")) {
211
+ if (args.boundaries === false) {
212
+ next = removeBoundary(next, errorBand.lowerBoundaryId);
213
+ next = removeBoundary(next, errorBand.upperBoundaryId);
214
+ } else if (Object.keys(args.boundaries).length === 0) {
215
+ for (const [id, bound] of [
216
+ [errorBand.lowerBoundaryId, errorBand.lowerField],
217
+ [errorBand.upperBoundaryId, errorBand.upperField]
218
+ ]) {
219
+ next = findLayer(next, id) === undefined
220
+ ? createBoundary(
221
+ next,
222
+ owner.id,
223
+ id,
224
+ bound,
225
+ defaultBoundaryAppearance(next, owner.id)
226
+ )
227
+ : interval.changed
228
+ ? next.rematerializeLineMark({ id })
229
+ : next;
230
+ }
231
+ } else {
232
+ next = next.editErrorBandBoundary({
233
+ target: owner.id,
234
+ ...args.boundaries
235
+ });
236
+ }
237
+ } else if (interval.changed) {
238
+ for (const id of [errorBand.lowerBoundaryId, errorBand.upperBoundaryId]) {
239
+ if (findLayer(next, id) !== undefined) {
240
+ next = next.rematerializeLineMark({ id });
241
+ }
242
+ }
243
+ }
244
+ return interval.changed
245
+ ? next.releaseDerivedData(interval.revision.release)
246
+ : next;
247
+ };
248
+ if (interval.changed || Object.hasOwn(args, "boundaries")) applyEdit(this);
249
+ return applyEdit(this);
142
250
  }
143
251
  );
144
252
 
@@ -2,7 +2,9 @@ import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
4
  import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
5
+ import { planIntervalEdit } from "../data/intervalEdit.js";
5
6
  import {
7
+ ERROR_BAR_APPEARANCE_OPTIONS,
6
8
  ERROR_BAR_EDIT_OPTIONS,
7
9
  resolveErrorBarAppearance
8
10
  } from "./options.js";
@@ -112,19 +114,54 @@ export const editErrorBar = action(
112
114
  validateKeys(args, ERROR_BAR_EDIT_OPTIONS, "editErrorBar");
113
115
  const editable = ERROR_BAR_EDIT_OPTIONS.filter(option => option !== "target");
114
116
  if (!editable.some(option => Object.hasOwn(args, option))) {
115
- throw new Error("editErrorBar requires at least one appearance change.");
117
+ throw new Error("editErrorBar requires at least one change.");
116
118
  }
117
119
  const owner = resolveOwner(this, args.target);
118
120
  const current = this.markConfigs[owner.id].errorBar;
119
- const appearance = resolveErrorBarAppearance(args, {
121
+ const appearanceArgs = Object.fromEntries(
122
+ ERROR_BAR_APPEARANCE_OPTIONS
123
+ .filter(key => Object.hasOwn(args, key))
124
+ .map(key => [key, args[key]])
125
+ );
126
+ const appearance = resolveErrorBarAppearance(appearanceArgs, {
120
127
  defaults: current,
121
128
  operation: "editErrorBar"
122
129
  });
123
- return this
124
- ._withMarkConfig(owner.id, {
125
- ...this.markConfigs[owner.id],
126
- errorBar: { ...current, ...appearance }
127
- })
128
- .rematerializeErrorBar({ id: owner.id });
130
+ const interval = Object.hasOwn(args, "statistics")
131
+ ? planIntervalEdit(this, {
132
+ owner: owner.id,
133
+ data: current.data,
134
+ consumers: [
135
+ owner.id,
136
+ ...[current.lowerCapId, current.upperCapId]
137
+ .filter(id => findLayer(this, id) !== undefined)
138
+ ],
139
+ statistics: args.statistics,
140
+ operation: "editErrorBar"
141
+ })
142
+ : { changed: false };
143
+
144
+ const applyEdit = program => {
145
+ let next = program;
146
+ if (interval.changed) {
147
+ next = next.createIntervalData(interval.dataArgs);
148
+ for (const rebind of interval.revision.rebinds) {
149
+ next = next.rebindLayerData(rebind);
150
+ }
151
+ }
152
+ next = next._withMarkConfig(owner.id, {
153
+ ...next.markConfigs[owner.id],
154
+ errorBar: {
155
+ ...current,
156
+ ...appearance,
157
+ ...(interval.changed ? { data: interval.revision.id } : {})
158
+ }
159
+ }).rematerializeErrorBar({ id: owner.id });
160
+ return interval.changed
161
+ ? next.releaseDerivedData(interval.revision.release)
162
+ : next;
163
+ };
164
+ if (interval.changed) applyEdit(this);
165
+ return applyEdit(this);
129
166
  }
130
167
  );
@@ -13,9 +13,15 @@ import {
13
13
 
14
14
  export const ERROR_BAR_EDIT_OPTIONS = Object.freeze([
15
15
  "target", "caps", "capSize", "stroke", "strokeWidth",
16
- "strokeDash", "opacity"
16
+ "strokeDash", "opacity", "statistics"
17
17
  ]);
18
18
 
19
+ export const ERROR_BAR_APPEARANCE_OPTIONS = Object.freeze(
20
+ ERROR_BAR_EDIT_OPTIONS.filter(option =>
21
+ option !== "target" && option !== "statistics"
22
+ )
23
+ );
24
+
19
25
  export function resolveErrorBarAppearance(args, {
20
26
  defaults,
21
27
  operation