ggaction 0.0.5 → 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 (35) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/actions/boxPlots/edit.js +455 -3
  5. package/src/actions/boxPlots/options.js +3 -3
  6. package/src/actions/composition/actions.js +13 -3
  7. package/src/actions/data/bin2d.js +140 -17
  8. package/src/actions/data/index.js +6 -1
  9. package/src/actions/data/intervalEdit.js +76 -0
  10. package/src/actions/distributions/revision.js +326 -0
  11. package/src/actions/encodings/appearance.js +35 -1
  12. package/src/actions/encodings/density.js +215 -80
  13. package/src/actions/encodings/index.js +2 -0
  14. package/src/actions/encodings/remove.js +268 -0
  15. package/src/actions/errorBands/edit.js +121 -13
  16. package/src/actions/errorBars/edit.js +45 -8
  17. package/src/actions/errorBars/options.js +7 -1
  18. package/src/actions/facets/actions.js +136 -1
  19. package/src/actions/gradientPlots/edit.js +352 -4
  20. package/src/actions/guides/axes/edit.js +129 -47
  21. package/src/actions/guides/legends/categorical/actions.js +19 -8
  22. package/src/actions/guides/legends/continuous/common.js +3 -3
  23. package/src/actions/guides/legends/edit.js +61 -3
  24. package/src/actions/guides/legends/remove.js +124 -23
  25. package/src/actions/guides/legends/strokeWidth.js +42 -14
  26. package/src/actions/marks/arc/actions.js +46 -14
  27. package/src/actions/marks/point/edit.js +15 -3
  28. package/src/actions/marks/point/materialize.js +9 -3
  29. package/src/actions/marks/rule/actions.js +4 -1
  30. package/src/actions/regression/edit.js +235 -74
  31. package/src/actions/selection/actions.js +122 -2
  32. package/src/actions/selection/index.js +6 -0
  33. package/src/core/vocabulary.js +7 -0
  34. package/types/index.d.ts +1 -0
  35. package/types/program.d.ts +71 -7
@@ -15,6 +15,7 @@ import { planDerivedDataRevision } from
15
15
  import { validateOptions } from "./shared.js";
16
16
  import { normalizeDensityPlacement } from "../../grammar/density.js";
17
17
  import { scaleEditPatch } from "../scales/patch.js";
18
+ import { removeLegendKinds } from "../guides/legends/remove.js";
18
19
  import {
19
20
  requireDensityField,
20
21
  resolveDensityCategoryScaleOptions,
@@ -29,8 +30,8 @@ const OPTIONS = Object.freeze([
29
30
  "valueScale", "densityScale", "placement"
30
31
  ]);
31
32
  const EDIT_OPTIONS = Object.freeze([
32
- "target", "bandwidth", "extent", "steps", "kernel", "normalization",
33
- "placement"
33
+ "target", "source", "field", "groupBy", "bandwidth", "extent", "steps",
34
+ "kernel", "normalization", "placement"
34
35
  ]);
35
36
  const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
36
37
 
@@ -190,6 +191,43 @@ function findDensityArea(program, requested) {
190
191
  });
191
192
  }
192
193
 
194
+ function densityCategoryField(layer, transform, groupBy) {
195
+ if (transform.placement === undefined) return undefined;
196
+ if (transform.placement.categoryField !== transform.groupBy) {
197
+ return transform.placement.categoryField;
198
+ }
199
+ return groupBy ?? `${layer.id}DensityCategory`;
200
+ }
201
+
202
+ function assertGroupingSelectionCompatibility(program, layer, changesGroup) {
203
+ if (!changesGroup) return;
204
+ const referenced = Object.entries(
205
+ program.materializationConfigs.selections ?? {}
206
+ ).find(([, selection]) =>
207
+ selection.target === layer.id &&
208
+ ["group", "color"].includes(selection.selector?.channel)
209
+ );
210
+ if (referenced !== undefined) {
211
+ throw new Error(
212
+ `editDensity cannot change groupBy while selection "${referenced[0]}" ` +
213
+ `references ${referenced[1].selector.channel}.`
214
+ );
215
+ }
216
+ }
217
+
218
+ function removeOwnedColorLegends(program, target) {
219
+ const kinds = Object.entries(program.guideConfigs.legend ?? {})
220
+ .filter(([kind, config]) =>
221
+ config?.target === target &&
222
+ (
223
+ config.channels?.includes("color") ||
224
+ ["gradient", "interval"].includes(kind)
225
+ )
226
+ )
227
+ .map(([kind]) => kind);
228
+ return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
229
+ }
230
+
193
231
  const editDensity = action(
194
232
  {
195
233
  op: "editDensity",
@@ -203,21 +241,55 @@ const editDensity = action(
203
241
  const layer = findDensityArea(this, args.target);
204
242
  const previous = findDataset(this, layer.data);
205
243
  const transform = previous.transform[0];
244
+ const source = Object.hasOwn(args, "source")
245
+ ? validateUserId(args.source, "Density source dataset id")
246
+ : previous.source;
247
+ const field = Object.hasOwn(args, "field")
248
+ ? requireDensityField(args.field, "Density field")
249
+ : transform.field;
250
+ const groupBy = Object.hasOwn(args, "groupBy")
251
+ ? args.groupBy === false
252
+ ? undefined
253
+ : requireDensityField(args.groupBy, "Density groupBy")
254
+ : transform.groupBy;
255
+ const changesGroup = groupBy !== transform.groupBy;
256
+ assertGroupingSelectionCompatibility(this, layer, changesGroup);
257
+ const categoryField = densityCategoryField(layer, transform, groupBy);
206
258
  const requestedPlacement = Object.hasOwn(args, "placement")
207
259
  ? normalizeDensityPlacement(args.placement, {
208
260
  densityChannel: transform.placement?.channel ?? "x",
209
- groupBy: transform.groupBy,
210
- categoryField: transform.groupBy ??
211
- transform.placement?.categoryField ??
261
+ groupBy,
262
+ categoryField: categoryField ?? groupBy ??
212
263
  `${layer.id}DensityCategory`
213
264
  })
214
- : transform.placement;
265
+ : transform.placement === undefined || !changesGroup
266
+ ? transform.placement
267
+ : normalizeDensityPlacement({
268
+ type: "category",
269
+ ...(transform.placement.side === undefined
270
+ ? {}
271
+ : { side: transform.placement.side }),
272
+ width: transform.placement.width,
273
+ ...(transform.placement.split === undefined
274
+ ? {}
275
+ : { split: transform.placement.split })
276
+ }, {
277
+ densityChannel: transform.placement.channel,
278
+ groupBy,
279
+ categoryField
280
+ });
215
281
  const colorField = layer.encoding?.color?.field;
282
+ const colorFollowsGroup = transform.groupBy !== undefined &&
283
+ colorField === transform.groupBy;
216
284
  const availableColorFields = [
217
- transform.groupBy,
285
+ groupBy,
218
286
  requestedPlacement?.split?.field
219
287
  ].filter(Boolean);
220
- if (colorField !== undefined && !availableColorFields.includes(colorField)) {
288
+ if (
289
+ colorField !== undefined &&
290
+ !colorFollowsGroup &&
291
+ !availableColorFields.includes(colorField)
292
+ ) {
221
293
  throw new Error(
222
294
  `editDensity placement would remove color field "${colorField}" from the density series.`
223
295
  );
@@ -234,11 +306,11 @@ const editDensity = action(
234
306
 
235
307
  const dataArgs = {
236
308
  id: revision.id,
237
- source: previous.source,
238
- field: transform.field,
239
- ...(transform.groupBy === undefined
309
+ source,
310
+ field,
311
+ ...(groupBy === undefined
240
312
  ? {}
241
- : { groupBy: transform.groupBy }),
313
+ : { groupBy }),
242
314
  bandwidth: option("bandwidth"),
243
315
  extent: option("extent"),
244
316
  steps: option("steps"),
@@ -249,78 +321,141 @@ const editDensity = action(
249
321
  ? {}
250
322
  : { placement: requestedPlacement })
251
323
  };
252
- let next = requestedPlacement === undefined
253
- ? this.createDensityData(dataArgs)
254
- : this.createCategoricalDensityData(dataArgs);
255
- next = next.rebindLayerData(revision.rebinds[0])
256
- .releaseDerivedData(revision.release);
257
-
258
324
  const changesPlacementMode = Object.hasOwn(args, "placement") &&
259
325
  (transform.placement === undefined) !== (requestedPlacement === undefined);
260
- if (changesPlacementMode) {
261
- const xScaleId = layer.encoding?.x?.scale;
262
- const yScaleId = layer.encoding?.y?.scale;
263
- const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
264
- this,
265
- layer,
266
- transform,
267
- requestedPlacement,
268
- args.placement
269
- );
270
- next = next
271
- .editSemantic({ property: `layer[${layer.id}].encoding.x`, remove: true })
272
- .editSemantic({ property: `layer[${layer.id}].encoding.y`, remove: true });
273
- next = next
274
- .editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
275
- .editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
276
- const densityChannel = requestedPlacement?.channel ?? (
277
- layer.encoding.x.field === transform.as[1] ? "x" : "y"
278
- );
279
- const definition = resolveDensityPositionDefinition({
280
- layer: { densityChannel },
281
- output: transform.as,
282
- groupBy: transform.groupBy,
283
- placement: requestedPlacement,
284
- coordinate: layer.coordinate,
285
- valueScale: {
286
- id: requestedPlacement?.channel === "x" ? yScaleId : xScaleId,
287
- ...scaleDefinitions[requestedPlacement?.channel === "x" ? "y" : "x"]
288
- },
289
- densityScale: requestedPlacement === undefined
290
- ? {
291
- id: densityChannel === "x" ? xScaleId : yScaleId,
292
- ...scaleDefinitions[densityChannel]
293
- }
294
- : undefined,
295
- placementScale: requestedPlacement === undefined
296
- ? undefined
297
- : {
298
- id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
299
- ...scaleDefinitions[requestedPlacement.channel]
300
- }
301
- });
302
- next = applyDensityPosition(next, layer.id, definition);
303
- } else if (
304
- requestedPlacement !== undefined &&
305
- Object.hasOwn(args.placement ?? {}, "scale")
306
- ) {
307
- const channel = requestedPlacement.channel;
308
- const scaleId = layer.encoding[channel].scale;
309
- const requestedScale = resolveDensityCategoryScaleOptions(
310
- args.placement.scale,
311
- scaleId
312
- );
313
- if (requestedScale.id !== scaleId) {
314
- throw new Error("editDensity placement scale cannot change its id.");
326
+ const applyEdit = program => {
327
+ let next = requestedPlacement === undefined
328
+ ? program.createDensityData(dataArgs)
329
+ : program.createCategoricalDensityData(dataArgs);
330
+ next = next.rebindLayerData(revision.rebinds[0])
331
+ .releaseDerivedData(revision.release);
332
+
333
+ if (changesGroup) {
334
+ if (groupBy === undefined) {
335
+ if (findLayer(next, layer.id).encoding?.group !== undefined) {
336
+ next = next.editSemantic({
337
+ property: `layer[${layer.id}].encoding.group`,
338
+ remove: true
339
+ });
340
+ }
341
+ if (colorFollowsGroup) {
342
+ next = next.editSemantic({
343
+ property: `layer[${layer.id}].encoding.color`,
344
+ remove: true
345
+ });
346
+ next = removeOwnedColorLegends(next, layer.id);
347
+ }
348
+ } else {
349
+ next = next
350
+ .editSemantic({
351
+ property: `layer[${layer.id}].encoding.group.field`,
352
+ value: groupBy
353
+ })
354
+ .editSemantic({
355
+ property: `layer[${layer.id}].encoding.group.fieldType`,
356
+ value: "nominal"
357
+ });
358
+ if (colorFollowsGroup) {
359
+ next = next.editSemantic({
360
+ property: `layer[${layer.id}].encoding.color.field`,
361
+ value: groupBy
362
+ });
363
+ }
364
+ }
365
+ if (
366
+ requestedPlacement !== undefined &&
367
+ transform.placement?.categoryField !== requestedPlacement.categoryField
368
+ ) {
369
+ next = next.editSemantic({
370
+ property: `layer[${layer.id}].encoding.${requestedPlacement.channel}.field`,
371
+ value: requestedPlacement.categoryField
372
+ });
373
+ }
315
374
  }
316
- next = next.editScale(scaleEditPatch(scaleId, requestedScale));
317
- }
318
375
 
319
- next = applyMaterializationPlan(
320
- next,
321
- planDensityRematerialization(next, layer.id)
322
- );
323
- return next;
376
+ if (changesPlacementMode) {
377
+ const xScaleId = layer.encoding?.x?.scale;
378
+ const yScaleId = layer.encoding?.y?.scale;
379
+ const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
380
+ program,
381
+ layer,
382
+ transform,
383
+ requestedPlacement,
384
+ args.placement
385
+ );
386
+ next = next
387
+ .editSemantic({
388
+ property: `layer[${layer.id}].encoding.x`,
389
+ remove: true
390
+ })
391
+ .editSemantic({
392
+ property: `layer[${layer.id}].encoding.y`,
393
+ remove: true
394
+ });
395
+ next = next
396
+ .editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
397
+ .editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
398
+ const densityChannel = requestedPlacement?.channel ?? (
399
+ layer.encoding.x.field === transform.as[1] ? "x" : "y"
400
+ );
401
+ const valueChannel = requestedPlacement?.channel === "x" ? "y" : "x";
402
+ const definition = resolveDensityPositionDefinition({
403
+ layer: { densityChannel },
404
+ output: transform.as,
405
+ groupBy,
406
+ placement: requestedPlacement,
407
+ coordinate: layer.coordinate,
408
+ valueScale: {
409
+ id: valueChannel === "y" ? yScaleId : xScaleId,
410
+ ...scaleDefinitions[valueChannel]
411
+ },
412
+ densityScale: requestedPlacement === undefined
413
+ ? {
414
+ id: densityChannel === "x" ? xScaleId : yScaleId,
415
+ ...scaleDefinitions[densityChannel]
416
+ }
417
+ : undefined,
418
+ placementScale: requestedPlacement === undefined
419
+ ? undefined
420
+ : {
421
+ id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
422
+ ...scaleDefinitions[requestedPlacement.channel]
423
+ }
424
+ });
425
+ next = applyDensityPosition(next, layer.id, definition, groupBy);
426
+ } else if (
427
+ requestedPlacement !== undefined &&
428
+ Object.hasOwn(args.placement ?? {}, "scale")
429
+ ) {
430
+ const channel = requestedPlacement.channel;
431
+ const scaleId = layer.encoding[channel].scale;
432
+ const requestedScale = resolveDensityCategoryScaleOptions(
433
+ args.placement.scale,
434
+ scaleId
435
+ );
436
+ if (requestedScale.id !== scaleId) {
437
+ throw new Error("editDensity placement scale cannot change its id.");
438
+ }
439
+ next = next.editScale(scaleEditPatch(scaleId, requestedScale));
440
+ }
441
+
442
+ next = applyMaterializationPlan(
443
+ next,
444
+ planDensityRematerialization(next, layer.id)
445
+ );
446
+ if (
447
+ changesGroup &&
448
+ colorFollowsGroup &&
449
+ groupBy !== undefined &&
450
+ Object.values(next.guideConfigs.legend ?? {})
451
+ .some(config => config?.target === layer.id)
452
+ ) {
453
+ next = next.rematerializeLegend();
454
+ }
455
+ return next;
456
+ };
457
+ applyEdit(this);
458
+ return applyEdit(this);
324
459
  }
325
460
  );
326
461
 
@@ -12,6 +12,7 @@ import { registerStrokeDashEncodingActions } from "./strokeDash.js";
12
12
  import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
13
13
  import { registerTextEncodingAction } from "./text.js";
14
14
  import { registerParallelEncodingAction } from "./parallel.js";
15
+ import { registerEncodingRemovalAction } from "./remove.js";
15
16
 
16
17
  export function registerEncodingActions(ProgramClass) {
17
18
  registerPositionEncodingActions(ProgramClass);
@@ -28,4 +29,5 @@ export function registerEncodingActions(ProgramClass) {
28
29
  registerRuleAppearanceEncodingActions(ProgramClass);
29
30
  registerTextEncodingAction(ProgramClass);
30
31
  registerParallelEncodingAction(ProgramClass);
32
+ registerEncodingRemovalAction(ProgramClass);
31
33
  }
@@ -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
+ }