ggaction 0.0.5 → 0.0.7

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 (69) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +77 -14
  3. package/package.json +19 -2
  4. package/src/BasicChartProgram.js +10 -0
  5. package/src/actions/basic.js +21 -0
  6. package/src/actions/boxPlots/edit.js +455 -3
  7. package/src/actions/boxPlots/options.js +3 -3
  8. package/src/actions/canvas/actions.js +42 -0
  9. package/src/actions/canvas/index.js +5 -1
  10. package/src/actions/charts/basic.js +13 -0
  11. package/src/actions/composition/actions.js +13 -3
  12. package/src/actions/data/basic.js +11 -0
  13. package/src/actions/data/basicBin2d.js +43 -0
  14. package/src/actions/data/bin2d.js +138 -40
  15. package/src/actions/data/bin2dMaterialize.js +29 -0
  16. package/src/actions/data/index.js +6 -1
  17. package/src/actions/data/intervalEdit.js +76 -0
  18. package/src/actions/distributions/revision.js +326 -0
  19. package/src/actions/encodings/appearance.js +41 -3
  20. package/src/actions/encodings/basic.js +21 -0
  21. package/src/actions/encodings/density.js +215 -80
  22. package/src/actions/encodings/index.js +2 -0
  23. package/src/actions/encodings/position/index.js +5 -1
  24. package/src/actions/encodings/ranged.js +6 -2
  25. package/src/actions/encodings/remove.js +268 -0
  26. package/src/actions/errorBands/edit.js +121 -13
  27. package/src/actions/errorBars/edit.js +45 -8
  28. package/src/actions/errorBars/options.js +7 -1
  29. package/src/actions/facets/actions.js +136 -1
  30. package/src/actions/gradientPlots/edit.js +352 -4
  31. package/src/actions/guides/axes/edit.js +129 -47
  32. package/src/actions/guides/axes/index.js +10 -2
  33. package/src/actions/guides/basic.js +19 -0
  34. package/src/actions/guides/grids/grid.js +7 -3
  35. package/src/actions/guides/grids/index.js +1 -1
  36. package/src/actions/guides/legends/categorical/actions.js +19 -8
  37. package/src/actions/guides/legends/categorical/index.js +6 -2
  38. package/src/actions/guides/legends/continuous/common.js +3 -3
  39. package/src/actions/guides/legends/continuous/index.js +6 -2
  40. package/src/actions/guides/legends/edit.js +61 -3
  41. package/src/actions/guides/legends/remove.js +124 -23
  42. package/src/actions/guides/legends/strokeWidth.js +42 -14
  43. package/src/actions/marks/arc/actions.js +46 -14
  44. package/src/actions/marks/bar/index.js +5 -1
  45. package/src/actions/marks/basic.js +11 -0
  46. package/src/actions/marks/line/actions.js +5 -1
  47. package/src/actions/marks/line/index.js +4 -1
  48. package/src/actions/marks/point/edit.js +15 -3
  49. package/src/actions/marks/point/index.js +5 -1
  50. package/src/actions/marks/point/materialize.js +9 -3
  51. package/src/actions/marks/rect/actions.js +5 -1
  52. package/src/actions/marks/rect/index.js +4 -1
  53. package/src/actions/marks/rule/actions.js +4 -1
  54. package/src/actions/regression/edit.js +235 -74
  55. package/src/actions/scales/index.js +5 -1
  56. package/src/actions/selection/actions.js +122 -2
  57. package/src/actions/selection/index.js +6 -0
  58. package/src/basic.js +2 -0
  59. package/src/core/vocabulary.js +7 -0
  60. package/src/renderers/canvas/index.js +96 -60
  61. package/src/renderers/canvas/text.js +3 -14
  62. package/src/renderers/pdf.js +112 -0
  63. package/src/renderers/svg.js +559 -0
  64. package/src/renderers/text.js +11 -0
  65. package/types/basic.d.ts +107 -0
  66. package/types/index.d.ts +1 -0
  67. package/types/pdf.d.ts +26 -0
  68. package/types/program.d.ts +71 -7
  69. package/types/svg.d.ts +11 -0
@@ -162,7 +162,11 @@ const editRectMark = action(
162
162
  );
163
163
 
164
164
  export function registerRectMarkActions(ProgramClass) {
165
- ProgramClass.prototype.createRectMark = createRectMark;
166
165
  ProgramClass.prototype.editRectMark = editRectMark;
166
+ registerBasicRectMarkActions(ProgramClass);
167
+ }
168
+
169
+ export function registerBasicRectMarkActions(ProgramClass) {
170
+ ProgramClass.prototype.createRectMark = createRectMark;
167
171
  ProgramClass.prototype.rematerializeRectMark = rematerializeRectMark;
168
172
  }
@@ -1 +1,4 @@
1
- export { registerRectMarkActions } from "./actions.js";
1
+ export {
2
+ registerBasicRectMarkActions,
3
+ registerRectMarkActions
4
+ } from "./actions.js";
@@ -238,7 +238,10 @@ const rematerializeRuleMark = action(
238
238
  }
239
239
  }
240
240
 
241
- const resolvedConfig = resolved.markConfigs[id] ?? DEFAULT_RULE_CONFIG;
241
+ const resolvedConfig = {
242
+ ...DEFAULT_RULE_CONFIG,
243
+ ...resolved.markConfigs[id]
244
+ };
242
245
  const dashEncoding = layer.encoding?.strokeDash;
243
246
  const opacityEncoding = layer.encoding?.opacity;
244
247
  const strokeWidthEncoding = layer.encoding?.strokeWidth;
@@ -12,12 +12,12 @@ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
12
12
  import { planDerivedDataRevision } from
13
13
  "../../materialization/dataProvenance.js";
14
14
  import { findLayer } from "../../selectors/layers.js";
15
+ import { removeLegendKinds } from "../guides/legends/remove.js";
16
+ import { requireRegressionField } from "./resolve.js";
15
17
 
16
18
  const OPTIONS = Object.freeze([
17
- "target", "method", "degree", "span", "confidence", "interval", "band", "line"
18
- ]);
19
- const STATISTICAL_OPTIONS = Object.freeze([
20
- "method", "degree", "span", "confidence", "interval"
19
+ "target", "data", "x", "y", "groupBy", "method", "degree", "span",
20
+ "confidence", "interval", "band", "line"
21
21
  ]);
22
22
  const BAND_OPTIONS = Object.freeze([
23
23
  "color", "opacity", "stroke", "strokeWidth", "curve"
@@ -118,11 +118,137 @@ function resolveParameters(previous, args) {
118
118
  return normalizeRegressionParameters(raw);
119
119
  }
120
120
 
121
+ function removeOwnedColorLegends(program, target) {
122
+ const kinds = Object.entries(program.guideConfigs.legend ?? {})
123
+ .filter(([kind, config]) =>
124
+ config?.target === target &&
125
+ (
126
+ config.channels?.includes("color") ||
127
+ ["gradient", "interval"].includes(kind)
128
+ )
129
+ )
130
+ .map(([kind]) => kind);
131
+ return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
132
+ }
133
+
121
134
  function removeBand(program, id) {
122
- return program
135
+ const selectionIds = Object.entries(
136
+ program.materializationConfigs.selections ?? {}
137
+ ).filter(([, config]) => config.target === id).map(([selection]) => selection);
138
+ let next = program;
139
+ for (const [highlight, config] of Object.entries(
140
+ program.materializationConfigs.highlights ?? {}
141
+ )) {
142
+ if (config.target === id || selectionIds.includes(config.selection)) {
143
+ next = next._withoutMaterializationConfig(["highlights", highlight]);
144
+ }
145
+ }
146
+ for (const selection of selectionIds) {
147
+ next = next._withoutMaterializationConfig(["selections", selection]);
148
+ }
149
+ return next
123
150
  .editSemantic({ property: `layer[${id}]`, remove: true })
124
151
  .editGraphics({ target: id, remove: true })
125
- ._withoutMaterializationConfig(["marks", id]);
152
+ ._withoutMaterializationConfig(["marks", id])
153
+ ._withContext({
154
+ ...(program.context.currentMark === id ? { currentMark: undefined } : {}),
155
+ ...(selectionIds.includes(program.context.currentSelection)
156
+ ? { currentSelection: undefined }
157
+ : {})
158
+ });
159
+ }
160
+
161
+ function regressionColorScale(owner, current, groupBy) {
162
+ if (groupBy === undefined || groupBy === current.groupBy) {
163
+ return current.colorScale;
164
+ }
165
+ return owner.encoding?.color?.field === groupBy
166
+ ? owner.encoding.color.scale
167
+ : `${owner.id}RegressionColor`;
168
+ }
169
+
170
+ function updateRegressionLineSemantics(program, current, {
171
+ x,
172
+ y,
173
+ groupBy,
174
+ colorScale
175
+ }) {
176
+ let next = program
177
+ .editSemantic({
178
+ property: `layer[${current.lineId}].encoding.x.field`,
179
+ value: x
180
+ })
181
+ .editSemantic({
182
+ property: `layer[${current.lineId}].encoding.y.field`,
183
+ value: y
184
+ });
185
+ if (groupBy === undefined) {
186
+ if (findLayer(next, current.lineId).encoding?.group !== undefined) {
187
+ next = next.editSemantic({
188
+ property: `layer[${current.lineId}].encoding.group`,
189
+ remove: true
190
+ });
191
+ }
192
+ if (findLayer(next, current.lineId).encoding?.color !== undefined) {
193
+ next = next.editSemantic({
194
+ property: `layer[${current.lineId}].encoding.color`,
195
+ remove: true
196
+ });
197
+ next = removeOwnedColorLegends(next, current.lineId);
198
+ }
199
+ return next;
200
+ }
201
+ next = findLayer(next, current.lineId).encoding?.group === undefined
202
+ ? next.encodeGroup({ target: current.lineId, field: groupBy })
203
+ : next.editSemantic({
204
+ property: `layer[${current.lineId}].encoding.group.field`,
205
+ value: groupBy
206
+ });
207
+ const color = findLayer(next, current.lineId).encoding?.color;
208
+ if (color === undefined || color.scale !== colorScale) {
209
+ next = next.encodeColor({
210
+ target: current.lineId,
211
+ field: groupBy,
212
+ scale: { id: colorScale }
213
+ });
214
+ } else {
215
+ next = next.editSemantic({
216
+ property: `layer[${current.lineId}].encoding.color.field`,
217
+ value: groupBy
218
+ });
219
+ }
220
+ return next;
221
+ }
222
+
223
+ function updateRegressionBandSemantics(program, bandId, { x, groupBy }) {
224
+ if (bandId === undefined) return program;
225
+ let next = program.editSemantic({
226
+ property: `layer[${bandId}].encoding.x.field`,
227
+ value: x
228
+ });
229
+ const layer = findLayer(next, bandId);
230
+ if (groupBy === undefined && layer.encoding?.group !== undefined) {
231
+ next = next.editSemantic({
232
+ property: `layer[${bandId}].encoding.group`,
233
+ remove: true
234
+ });
235
+ } else if (groupBy !== undefined) {
236
+ next = layer.encoding?.group === undefined
237
+ ? next.encodeGroup({ target: bandId, field: groupBy })
238
+ : next.editSemantic({
239
+ property: `layer[${bandId}].encoding.group.field`,
240
+ value: groupBy
241
+ });
242
+ }
243
+ const config = next.markConfigs[bandId];
244
+ return next._withMarkConfig(bandId, {
245
+ ...config,
246
+ errorBand: {
247
+ ...config.errorBand,
248
+ position: { ...config.errorBand.position, field: x },
249
+ groupBy
250
+ }
251
+ });
126
252
  }
127
253
 
128
254
  export const editRegression = action(
@@ -137,10 +263,27 @@ export const editRegression = action(
137
263
  }
138
264
  const owner = resolveRegressionOwner(this, args.target);
139
265
  const current = this.markConfigs[owner.id].regression;
266
+ const source = Object.hasOwn(args, "data")
267
+ ? validateUserId(args.data, "Regression data id")
268
+ : current.source;
269
+ const x = Object.hasOwn(args, "x")
270
+ ? requireRegressionField(args.x, "Regression x")
271
+ : current.x;
272
+ const y = Object.hasOwn(args, "y")
273
+ ? requireRegressionField(args.y, "Regression y")
274
+ : current.y;
275
+ const groupBy = Object.hasOwn(args, "groupBy")
276
+ ? args.groupBy === false
277
+ ? undefined
278
+ : requireRegressionField(args.groupBy, "Regression groupBy")
279
+ : current.groupBy;
280
+ const colorScale = regressionColorScale(owner, current, groupBy);
140
281
  const parameters = resolveParameters(current.parameters, args);
141
- const changesStatistics = STATISTICAL_OPTIONS.some(
142
- key => Object.hasOwn(args, key)
143
- ) && JSON.stringify(parameters) !== JSON.stringify(current.parameters);
282
+ const changesProvenance = source !== current.source ||
283
+ x !== current.x || y !== current.y || groupBy !== current.groupBy;
284
+ const changesParameters = JSON.stringify(parameters) !==
285
+ JSON.stringify(current.parameters);
286
+ const changesStatistics = changesProvenance || changesParameters;
144
287
  const bandPatch = Object.hasOwn(args, "band")
145
288
  ? validateBandPatch(args.band)
146
289
  : undefined;
@@ -156,8 +299,6 @@ export const editRegression = action(
156
299
  (bandPatch === false
157
300
  ? false
158
301
  : hadBand || bandPatch !== undefined || current.parameters.method === "loess");
159
- let dataId = current.dataId;
160
- let next = this;
161
302
  const revision = changesStatistics
162
303
  ? planDerivedDataRevision(this, {
163
304
  owner: owner.id,
@@ -170,76 +311,96 @@ export const editRegression = action(
170
311
  })
171
312
  : undefined;
172
313
 
173
- if (changesStatistics) {
174
- dataId = revision.id;
175
- next = next.createRegressionData({
176
- id: dataId,
177
- source: current.source,
178
- x: current.x,
179
- y: current.y,
180
- ...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
181
- ...parameters
182
- });
183
- for (const rebind of revision.rebinds) {
184
- next = next.rebindLayerData(rebind);
314
+ const applyEdit = program => {
315
+ let dataId = current.dataId;
316
+ let next = program;
317
+ if (changesStatistics) {
318
+ dataId = revision.id;
319
+ next = next.createRegressionData({
320
+ id: dataId,
321
+ source,
322
+ x,
323
+ y,
324
+ ...(groupBy === undefined ? {} : { groupBy }),
325
+ ...parameters
326
+ });
327
+ for (const rebind of revision.rebinds) {
328
+ next = next.rebindLayerData(rebind);
329
+ }
185
330
  }
186
- }
187
331
 
188
- let bandId = current.bandId;
189
- if (hadBand && !wantsBand) {
190
- next = removeBand(next, current.bandId);
191
- bandId = undefined;
192
- } else if (!hadBand && wantsBand) {
193
- bandId = `${owner.id}RegressionBands`;
194
- next = next.createRegressionBand({
195
- id: bandId,
196
- data: dataId,
197
- x: current.x,
198
- lower: "__regression_ci_lower",
199
- upper: "__regression_ci_upper",
200
- ...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
201
- coordinate: current.coordinate,
202
- xScale: current.xScale,
203
- yScale: current.yScale,
204
- ...(bandPatch === undefined || bandPatch === false ? {} : bandPatch)
205
- });
206
- } else if (hadBand && changesStatistics) {
207
- const bandConfig = next.markConfigs[bandId];
208
- next = next._withMarkConfig(bandId, {
209
- ...bandConfig,
210
- errorBand: { ...bandConfig.errorBand, data: dataId }
332
+ let bandId = current.bandId;
333
+ if (hadBand && !wantsBand) {
334
+ next = removeBand(next, current.bandId);
335
+ bandId = undefined;
336
+ } else if (!hadBand && wantsBand) {
337
+ bandId = `${owner.id}RegressionBands`;
338
+ next = next.createRegressionBand({
339
+ id: bandId,
340
+ data: dataId,
341
+ x,
342
+ lower: "__regression_ci_lower",
343
+ upper: "__regression_ci_upper",
344
+ ...(groupBy === undefined ? {} : { groupBy }),
345
+ coordinate: current.coordinate,
346
+ xScale: current.xScale,
347
+ yScale: current.yScale,
348
+ ...(bandPatch === undefined || bandPatch === false ? {} : bandPatch)
211
349
  });
212
- }
350
+ } else if (hadBand && changesStatistics) {
351
+ const bandConfig = next.markConfigs[bandId];
352
+ next = next._withMarkConfig(bandId, {
353
+ ...bandConfig,
354
+ errorBand: { ...bandConfig.errorBand, data: dataId }
355
+ });
356
+ }
213
357
 
214
- if (bandId !== undefined) {
215
- if (
216
- bandPatch !== undefined &&
217
- bandPatch !== false &&
218
- Object.keys(bandPatch).length > 0 &&
219
- hadBand
220
- ) {
221
- next = next.editRegressionBand({ target: bandId, ...bandPatch });
358
+ if (changesProvenance) {
359
+ next = updateRegressionLineSemantics(next, current, {
360
+ x,
361
+ y,
362
+ groupBy,
363
+ colorScale
364
+ });
365
+ next = updateRegressionBandSemantics(next, bandId, { x, groupBy });
366
+ }
367
+ if (bandId !== undefined) {
368
+ if (
369
+ bandPatch !== undefined &&
370
+ bandPatch !== false &&
371
+ Object.keys(bandPatch).length > 0 &&
372
+ hadBand
373
+ ) {
374
+ next = next.editRegressionBand({ target: bandId, ...bandPatch });
375
+ } else if (changesStatistics) {
376
+ next = next.rematerializeAreaMark({ id: bandId });
377
+ }
378
+ }
379
+ if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
380
+ next = next.editRegressionLine({ target: current.lineId, ...linePatch });
222
381
  } else if (changesStatistics) {
223
- next = next.rematerializeAreaMark({ id: bandId });
382
+ next = next.rematerializeLineMark({ id: current.lineId });
224
383
  }
225
- }
226
- if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
227
- next = next.editRegressionLine({ target: current.lineId, ...linePatch });
228
- } else if (changesStatistics) {
229
- next = next.rematerializeLineMark({ id: current.lineId });
230
- }
231
384
 
232
- if (changesStatistics) {
233
- next = next.releaseDerivedData(revision.release);
234
- }
235
- return next._withMarkConfig(owner.id, {
236
- ...next.markConfigs[owner.id],
237
- regression: {
238
- ...current,
239
- dataId,
240
- bandId,
241
- parameters
385
+ if (changesStatistics) {
386
+ next = next.releaseDerivedData(revision.release);
242
387
  }
243
- });
388
+ return next._withMarkConfig(owner.id, {
389
+ ...next.markConfigs[owner.id],
390
+ regression: {
391
+ ...current,
392
+ source,
393
+ x,
394
+ y,
395
+ groupBy,
396
+ colorScale,
397
+ dataId,
398
+ bandId,
399
+ parameters
400
+ }
401
+ });
402
+ };
403
+ if (changesStatistics || hadBand !== wantsBand) applyEdit(this);
404
+ return applyEdit(this);
244
405
  }
245
406
  );
@@ -4,8 +4,12 @@ import { rematerializeScale } from "./materialize.js";
4
4
  import { setQuantitativeColorScale } from "./quantitativeColor.js";
5
5
 
6
6
  export function registerScaleActions(ProgramClass) {
7
- ProgramClass.prototype.createScale = createScale;
8
7
  ProgramClass.prototype.editScale = editScale;
8
+ registerBasicScaleActions(ProgramClass);
9
+ }
10
+
11
+ export function registerBasicScaleActions(ProgramClass) {
12
+ ProgramClass.prototype.createScale = createScale;
9
13
  ProgramClass.prototype.rematerializeScale = rematerializeScale;
10
14
  ProgramClass.prototype.setQuantitativeColorScale = setQuantitativeColorScale;
11
15
  }
@@ -17,6 +17,7 @@ import {
17
17
  findSelectionPolicy,
18
18
  requireSelectionPolicy
19
19
  } from "../../materialization/selection/policies/index.js";
20
+ import { legendGraphicIds } from "../../materialization/guides/resources.js";
20
21
  import {
21
22
  normalizeDimOthers,
22
23
  validateUnitInterval
@@ -27,6 +28,8 @@ const SELECTOR_KEYS = Object.freeze([
27
28
  "inclusive", "count", "groupBy", "ties"
28
29
  ]);
29
30
  const SELECT_OPTIONS = Object.freeze(["id", "target", ...SELECTOR_KEYS]);
31
+ const EDIT_SELECTION_OPTIONS = Object.freeze(["selection", ...SELECTOR_KEYS]);
32
+ const REMOVE_SELECTION_OPTIONS = Object.freeze(["selection"]);
30
33
  const HIGHLIGHT_OPTIONS = Object.freeze([
31
34
  "id", "target", "select", "selection", "color", "opacity", "fill",
32
35
  "stroke", "strokeWidth", "strokeDash", "shape", "size", "offset",
@@ -63,6 +66,51 @@ function hasLegendSelection(program, target, selection) {
63
66
  selector.field === legend.field;
64
67
  }
65
68
 
69
+ function categoricalLegendKind(program, target) {
70
+ return ["series", "color"].find(
71
+ kind => program.guideConfigs.legend?.[kind]?.target === target
72
+ );
73
+ }
74
+
75
+ function resetCategoricalLegendSymbols(program, target) {
76
+ const kind = categoricalLegendKind(program, target);
77
+ if (kind === undefined) return program;
78
+ let next = program;
79
+ for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
80
+ const graphic = next.graphicSpec.objects[id];
81
+ if (graphic === undefined) continue;
82
+ next = graphic.type === "collection"
83
+ ? next.editGraphics({ target: id, property: "items", value: [] })
84
+ : next.editGraphics({ target: id, property: "length", value: 0 });
85
+ }
86
+ return next.rematerializeLegendSymbols();
87
+ }
88
+
89
+ function targetHighlightEntries(program, target) {
90
+ return Object.entries(program.materializationConfigs.highlights ?? {})
91
+ .filter(([, config]) => config.target === target);
92
+ }
93
+
94
+ function rebuildTargetHighlights(program, target) {
95
+ const layer = resolveTarget(program, target, "highlight mark");
96
+ const highlights = targetHighlightEntries(program, target);
97
+ let baseline = program;
98
+ for (const [id] of highlights) {
99
+ baseline = baseline._withoutMaterializationConfig(["highlights", id]);
100
+ }
101
+ const graphic = baseline.graphicSpec.objects[target];
102
+ baseline = graphic.type === "collection"
103
+ ? baseline.editGraphics({ target, property: "items", value: [] })
104
+ : baseline.editGraphics({ target, property: "length", value: 0 });
105
+ baseline = baseline[requireSelectionPolicy(
106
+ layer.mark.type
107
+ ).rematerializeOp]({ id: target });
108
+ baseline = resetCategoricalLegendSymbols(baseline, target);
109
+ return highlights.length === 0
110
+ ? baseline
111
+ : baseline.rematerializeMarkHighlights({ target, highlights });
112
+ }
113
+
66
114
  function selectedKeys(args, resolved) {
67
115
  if (args.keys === undefined) return resolved.keys;
68
116
  if (!Array.isArray(args.keys) || !args.keys.every(key =>
@@ -86,6 +134,79 @@ export const selectMarks = action(
86
134
  }
87
135
  );
88
136
 
137
+ export const editMarkSelection = action(
138
+ {
139
+ op: "editMarkSelection",
140
+ description: "Replace one stored mark selector while preserving its identity and target."
141
+ },
142
+ function (args = {}) {
143
+ validateKeys(args, EDIT_SELECTION_OPTIONS, "editMarkSelection");
144
+ const current = resolveStoredSelection(this, args.selection);
145
+ const replacement = resolveMarkSelection(
146
+ this,
147
+ current.definition.target,
148
+ selectorFrom(args)
149
+ );
150
+ const next = this
151
+ ._withSelectionConfig(current.id, {
152
+ target: current.definition.target,
153
+ selector: replacement.selector
154
+ })
155
+ ._withContext({ currentSelection: current.id });
156
+ const hasDependentHighlight = Object.values(
157
+ next.materializationConfigs.highlights ?? {}
158
+ ).some(config => config.selection === current.id);
159
+ return hasDependentHighlight
160
+ ? rebuildTargetHighlights(next, current.definition.target)
161
+ : next;
162
+ }
163
+ );
164
+
165
+ export const removeMarkHighlight = action(
166
+ {
167
+ op: "removeMarkHighlight",
168
+ description: "Remove one stored highlight assignment and restore the clean mark baseline."
169
+ },
170
+ function (args = {}) {
171
+ validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkHighlight");
172
+ const current = resolveStoredSelection(this, args.selection);
173
+ const dependent = Object.entries(
174
+ this.materializationConfigs.highlights ?? {}
175
+ ).filter(([, config]) => config.selection === current.id);
176
+ if (dependent.length === 0) {
177
+ throw new Error(
178
+ `Selection "${current.id}" has no highlight assignment.`
179
+ );
180
+ }
181
+ let next = this;
182
+ for (const [id] of dependent) {
183
+ next = next._withoutMaterializationConfig(["highlights", id]);
184
+ }
185
+ return rebuildTargetHighlights(next, current.definition.target);
186
+ }
187
+ );
188
+
189
+ export const removeMarkSelection = action(
190
+ {
191
+ op: "removeMarkSelection",
192
+ description: "Remove one stored selection after removing its dependent highlight."
193
+ },
194
+ function (args = {}) {
195
+ validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkSelection");
196
+ const current = resolveStoredSelection(this, args.selection);
197
+ const hasDependentHighlight = Object.values(
198
+ this.materializationConfigs.highlights ?? {}
199
+ ).some(config => config.selection === current.id);
200
+ let next = hasDependentHighlight
201
+ ? this.removeMarkHighlight({ selection: current.id })
202
+ : this;
203
+ next = next._withoutMaterializationConfig(["selections", current.id]);
204
+ return this.context.currentSelection === current.id
205
+ ? next._withContext({ currentSelection: undefined })
206
+ : next;
207
+ }
208
+ );
209
+
89
210
  export const applyPointHighlight = action(
90
211
  { op: "applyPointHighlight", description: "Apply selected point appearance and geometry." },
91
212
  function (args = {}) {
@@ -393,8 +514,7 @@ export const highlightMarks = action(
393
514
  const existing = next.materializationConfigs.highlights?.[selection];
394
515
  if (existing !== undefined) {
395
516
  next = next._withoutMaterializationConfig(["highlights", selection]);
396
- const policy = requireSelectionPolicy(layer.mark.type);
397
- next = next[policy.rematerializeOp]({ id: resolved.definition.target });
517
+ next = rebuildTargetHighlights(next, resolved.definition.target);
398
518
  }
399
519
  const policy = requireSelectionPolicy(layer.mark.type);
400
520
  next = next[policy.applyHighlightOp]({ selection, style, keys });
@@ -5,14 +5,20 @@ import {
5
5
  applyPointHighlight,
6
6
  applyRuleHighlight,
7
7
  dimUnselectedMarkItems,
8
+ editMarkSelection,
8
9
  highlightMarks,
9
10
  placeSelectedMarkItemsLast,
10
11
  rematerializeMarkHighlights,
12
+ removeMarkHighlight,
13
+ removeMarkSelection,
11
14
  selectMarks
12
15
  } from "./actions.js";
13
16
 
14
17
  export function registerSelectionActions(ProgramClass) {
15
18
  ProgramClass.prototype.selectMarks = selectMarks;
19
+ ProgramClass.prototype.editMarkSelection = editMarkSelection;
20
+ ProgramClass.prototype.removeMarkHighlight = removeMarkHighlight;
21
+ ProgramClass.prototype.removeMarkSelection = removeMarkSelection;
16
22
  ProgramClass.prototype.highlightMarks = highlightMarks;
17
23
  ProgramClass.prototype.applyBarHighlight = applyBarHighlight;
18
24
  ProgramClass.prototype.applyRectHighlight = applyRectHighlight;
package/src/basic.js ADDED
@@ -0,0 +1,2 @@
1
+ export { chart } from "./BasicChartProgram.js";
2
+ export { render } from "./renderers/canvas/index.js";
@@ -198,6 +198,13 @@ export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
198
198
  "shape"
199
199
  ]);
200
200
 
201
+ export const LEGEND_CHANNELS = Object.freeze([
202
+ ...CATEGORICAL_LEGEND_CHANNELS,
203
+ "strokeWidth",
204
+ "size",
205
+ "opacity"
206
+ ]);
207
+
201
208
  export const LEGEND_CONFIG_KINDS = Object.freeze([
202
209
  "series",
203
210
  "color",