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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to `ggaction` are recorded in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.0.6] - 2026-07-23
8
+
9
+ ### Added
10
+
11
+ - Added explicit edit and removal lifecycles for encodings, point appearance, selections, highlights, legends, and
12
+ Cartesian axis components while preserving immutable state and meaningful action traces.
13
+ - Added revision workflows for 2D bins, interval statistics, regression, density, box plots, gradient plots, and facet
14
+ policies, including deterministic rematerialization after data-role and layout changes.
15
+
16
+ ### Changed
17
+
18
+ - Expanded installed-package, Browser Canvas, Node PNG, TypeScript, action-contract, and cross-capability coverage for
19
+ the complete authoring lifecycle surface.
20
+ - Improved documentation entry points, navigation, chart discovery, gallery filtering, responsive presentation, and
21
+ generated API routing for the completed lifecycle actions.
22
+
23
+ ### Fixed
24
+
25
+ - Preserved unaffected shared-legend channels when selectively removing an encoding, and prevented removed resources
26
+ from returning after later rematerialization or facet replay.
27
+ - Kept documentation search results under the deployed `/ggaction/` base path and improved gallery text contrast.
28
+ - Allowed the approved Polar chart ink bounds to absorb the observed 1.5-pixel platform rasterization variance while
29
+ retaining exact primitive/public pixel comparisons and the existing density, color, and region checks.
30
+
7
31
  ## [0.0.5] - 2026-07-21
8
32
 
9
33
  ### Added
@@ -111,6 +135,7 @@ All notable changes to `ggaction` are recorded in this file.
111
135
  - Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
112
136
  not imply complete polar rendering.
113
137
 
138
+ [0.0.6]: https://github.com/ggaction/ggaction/releases/tag/v0.0.6
114
139
  [0.0.5]: https://github.com/ggaction/ggaction/releases/tag/v0.0.5
115
140
  [0.0.4]: https://github.com/ggaction/ggaction/releases/tag/v0.0.4
116
141
  [0.0.3]: https://github.com/ggaction/ggaction/releases/tag/v0.0.3
package/README.md CHANGED
@@ -142,7 +142,7 @@ All entries include TypeScript declarations. The default entry is browser-safe;
142
142
 
143
143
  ## Status and development
144
144
 
145
- > **Status:** `0.0.5` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
145
+ > **Status:** `0.0.6` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
146
146
 
147
147
  ```bash
148
148
  npm install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ggaction",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Build charts through immutable, traceable graphical actions.",
5
5
  "license": "MIT",
6
6
  "author": "Hyeon Jeon",
@@ -8,16 +8,27 @@ import { planDerivedDataRevision } from
8
8
  "../../materialization/dataProvenance.js";
9
9
  import { findDataset } from "../../selectors/datasets.js";
10
10
  import { findLayer } from "../../selectors/layers.js";
11
+ import {
12
+ assertDistributionScaleHandoff,
13
+ clearCartesianPositions,
14
+ rebindDistributionGuides,
15
+ resolveDistributionScalePlan,
16
+ setCartesianPosition,
17
+ setCartesianRange
18
+ } from "../distributions/revision.js";
11
19
  import {
12
20
  resolveBoxAppearance,
13
21
  resolveBoxMedianAppearance,
14
22
  resolveBoxOutlierAppearance,
23
+ resolveBoxPosition,
15
24
  resolveBoxWhisker,
16
25
  resolveBoxWidth
17
26
  } from "./options.js";
27
+ import { resolveBoxOrientation } from "./resolve.js";
18
28
 
19
29
  const OPTIONS = Object.freeze([
20
- "target", "whisker", "width", "outliers", "box", "median", "outlier"
30
+ "target", "data", "x", "y", "whisker", "width", "outliers", "box",
31
+ "median", "outlier"
21
32
  ]);
22
33
 
23
34
  function resolveBoxOwner(program, requested) {
@@ -63,10 +74,287 @@ function resolveEditedWhisker(current, value) {
63
74
  }
64
75
 
65
76
  function removeOwnedMark(program, id) {
66
- return program
77
+ const selections = Object.entries(
78
+ program.materializationConfigs.selections ?? {}
79
+ ).filter(([, config]) => config.target === id).map(([selection]) => selection);
80
+ let next = program;
81
+ for (const [highlight, config] of Object.entries(
82
+ program.materializationConfigs.highlights ?? {}
83
+ )) {
84
+ if (config.target === id || selections.includes(config.selection)) {
85
+ next = next._withoutMaterializationConfig(["highlights", highlight]);
86
+ }
87
+ }
88
+ for (const selection of selections) {
89
+ next = next._withoutMaterializationConfig(["selections", selection]);
90
+ }
91
+ return next
67
92
  .editSemantic({ property: `layer[${id}]`, remove: true })
68
93
  .editGraphics({ target: id, remove: true })
69
- ._withoutMaterializationConfig(["marks", id]);
94
+ ._withoutMaterializationConfig(["marks", id])
95
+ ._withContext({
96
+ ...(program.context.currentMark === id ? { currentMark: undefined } : {}),
97
+ ...(selections.includes(program.context.currentSelection)
98
+ ? { currentSelection: undefined }
99
+ : {})
100
+ });
101
+ }
102
+
103
+ function currentBoxPositions(owner, current) {
104
+ const categoryChannel = current.orientation === "vertical" ? "x" : "y";
105
+ const measureChannel = current.orientation === "vertical" ? "y" : "x";
106
+ const categoryEncoding = owner.encoding[categoryChannel];
107
+ const measureEncoding = owner.encoding[measureChannel];
108
+ return {
109
+ x: current.orientation === "vertical"
110
+ ? {
111
+ field: current.category,
112
+ fieldType: categoryEncoding.fieldType,
113
+ scale: categoryEncoding.scale
114
+ }
115
+ : {
116
+ field: current.measure,
117
+ fieldType: "quantitative",
118
+ scale: measureEncoding.scale
119
+ },
120
+ y: current.orientation === "vertical"
121
+ ? {
122
+ field: current.measure,
123
+ fieldType: "quantitative",
124
+ scale: measureEncoding.scale
125
+ }
126
+ : {
127
+ field: current.category,
128
+ fieldType: categoryEncoding.fieldType,
129
+ scale: categoryEncoding.scale
130
+ },
131
+ categoryScale: categoryEncoding.scale,
132
+ measureScale: measureEncoding.scale
133
+ };
134
+ }
135
+
136
+ function requirePositionField(position, label) {
137
+ if (typeof position.field !== "string" || position.field.length === 0) {
138
+ throw new TypeError(`${label} field must be a non-empty string.`);
139
+ }
140
+ return { fieldType: position.fieldType ?? "quantitative", ...position };
141
+ }
142
+
143
+ function roleCandidate(program, owner, current, args) {
144
+ const previous = currentBoxPositions(owner, current);
145
+ const x = requirePositionField(
146
+ Object.hasOwn(args, "x")
147
+ ? resolveBoxPosition(args.x, "x", "editBoxPlot")
148
+ : previous.x,
149
+ "editBoxPlot x"
150
+ );
151
+ const y = requirePositionField(
152
+ Object.hasOwn(args, "y")
153
+ ? resolveBoxPosition(args.y, "y", "editBoxPlot")
154
+ : previous.y,
155
+ "editBoxPlot y"
156
+ );
157
+ const orientation = resolveBoxOrientation(x, y);
158
+ if (orientation === undefined) {
159
+ throw new Error(
160
+ "editBoxPlot requires one categorical axis and one quantitative axis."
161
+ );
162
+ }
163
+ const xRoleScale = orientation === "vertical"
164
+ ? previous.categoryScale
165
+ : previous.measureScale;
166
+ const yRoleScale = orientation === "vertical"
167
+ ? previous.measureScale
168
+ : previous.categoryScale;
169
+ const xScale = resolveDistributionScalePlan(program, {
170
+ channel: "x",
171
+ fieldType: x.fieldType,
172
+ requested: x.scale,
173
+ fallback: xRoleScale,
174
+ defaults: ["ordinal", "nominal"].includes(x.fieldType)
175
+ ? { discreteType: "band" }
176
+ : { nice: true, zero: false }
177
+ });
178
+ const yScale = resolveDistributionScalePlan(program, {
179
+ channel: "y",
180
+ fieldType: y.fieldType,
181
+ requested: y.scale,
182
+ fallback: yRoleScale,
183
+ defaults: ["ordinal", "nominal"].includes(y.fieldType)
184
+ ? { discreteType: "band" }
185
+ : { nice: true, zero: false }
186
+ });
187
+ return {
188
+ source: Object.hasOwn(args, "data")
189
+ ? validateUserId(args.data, "Box-plot data id")
190
+ : current.source,
191
+ orientation,
192
+ x: { ...x, scale: xScale.id },
193
+ y: { ...y, scale: yScale.id },
194
+ xScale,
195
+ yScale,
196
+ category: orientation === "vertical" ? x.field : y.field,
197
+ categoryType: orientation === "vertical" ? x.fieldType : y.fieldType,
198
+ measure: orientation === "vertical" ? y.field : x.field,
199
+ previous
200
+ };
201
+ }
202
+
203
+ function sameRoleCandidate(current, candidate) {
204
+ return candidate.source === current.source &&
205
+ candidate.orientation === current.orientation &&
206
+ candidate.category === current.category &&
207
+ candidate.measure === current.measure &&
208
+ candidate.x.fieldType === candidate.previous.x.fieldType &&
209
+ candidate.y.fieldType === candidate.previous.y.fieldType &&
210
+ candidate.xScale.id === candidate.previous.x.scale &&
211
+ candidate.yScale.id === candidate.previous.y.scale &&
212
+ candidate.xScale.edit === undefined &&
213
+ candidate.yScale.edit === undefined;
214
+ }
215
+
216
+ function updateBoxPositions(program, owner, current, candidate, {
217
+ hasOutliers
218
+ }) {
219
+ const whiskerConfig = program.markConfigs[current.whiskerId];
220
+ const capIds = [
221
+ whiskerConfig.errorBar.lowerCapId,
222
+ whiskerConfig.errorBar.upperCapId
223
+ ].filter(Boolean);
224
+ const owned = [owner.id, current.whiskerId, ...capIds, current.medianId,
225
+ ...(findLayer(program, current.outlierId) ? [current.outlierId] : [])];
226
+ assertDistributionScaleHandoff(program, {
227
+ owned,
228
+ oldXScale: candidate.previous.x.scale,
229
+ oldYScale: candidate.previous.y.scale,
230
+ newXScale: candidate.xScale.id,
231
+ newYScale: candidate.yScale.id
232
+ });
233
+ let next = program;
234
+ for (const id of owned) next = clearCartesianPositions(next, id);
235
+ for (const scale of [candidate.xScale, candidate.yScale]) {
236
+ if (scale.create) next = next.createScale(scale.definition);
237
+ }
238
+ const category = candidate.orientation === "vertical" ? candidate.x : candidate.y;
239
+ const measure = candidate.orientation === "vertical" ? candidate.y : candidate.x;
240
+ const categoryChannel = candidate.orientation === "vertical" ? "x" : "y";
241
+ const measureChannel = candidate.orientation === "vertical" ? "y" : "x";
242
+ next = setCartesianPosition(next, owner.id, categoryChannel, {
243
+ field: category.field,
244
+ fieldType: category.fieldType,
245
+ scale: category.scale
246
+ });
247
+ next = setCartesianRange(
248
+ next,
249
+ owner.id,
250
+ measureChannel,
251
+ BOX_FIELDS.q1,
252
+ BOX_FIELDS.q3,
253
+ measure.scale,
254
+ candidate.measure
255
+ );
256
+ next = next._withMarkConfig(current.whiskerId, {
257
+ ...whiskerConfig,
258
+ errorBar: {
259
+ ...whiskerConfig.errorBar,
260
+ data: findLayer(next, current.whiskerId).data,
261
+ orientation: candidate.orientation,
262
+ positionField: candidate.category,
263
+ positionFieldType: candidate.categoryType,
264
+ coordinate: owner.coordinate,
265
+ positionScale: category.scale,
266
+ intervalScale: measure.scale
267
+ }
268
+ });
269
+ next = setCartesianPosition(next, current.whiskerId, categoryChannel, {
270
+ field: category.field,
271
+ fieldType: category.fieldType,
272
+ scale: category.scale
273
+ });
274
+ next = setCartesianRange(
275
+ next,
276
+ current.whiskerId,
277
+ measureChannel,
278
+ BOX_FIELDS.lowerWhisker,
279
+ BOX_FIELDS.upperWhisker,
280
+ measure.scale,
281
+ candidate.measure
282
+ );
283
+ for (const [index, capId] of capIds.entries()) {
284
+ const field = index === 0 ? BOX_FIELDS.lowerWhisker : BOX_FIELDS.upperWhisker;
285
+ next = setCartesianPosition(next, capId, categoryChannel, {
286
+ field: category.field,
287
+ fieldType: category.fieldType,
288
+ scale: category.scale
289
+ });
290
+ next = setCartesianPosition(next, capId, measureChannel, {
291
+ field,
292
+ fieldType: "quantitative",
293
+ scale: measure.scale
294
+ })._withMarkConfig(capId, {
295
+ ...next.markConfigs[capId],
296
+ fixedSpan: {
297
+ ...next.markConfigs[capId].fixedSpan,
298
+ orientation: candidate.orientation === "vertical"
299
+ ? "horizontal"
300
+ : "vertical"
301
+ }
302
+ });
303
+ }
304
+ next = setCartesianPosition(next, current.medianId, categoryChannel, {
305
+ field: category.field,
306
+ fieldType: category.fieldType,
307
+ scale: category.scale
308
+ });
309
+ next = setCartesianPosition(next, current.medianId, measureChannel, {
310
+ field: BOX_FIELDS.median,
311
+ fieldType: "quantitative",
312
+ scale: measure.scale
313
+ });
314
+ if (hasOutliers && findLayer(next, current.outlierId) !== undefined) {
315
+ next = setCartesianPosition(next, current.outlierId, categoryChannel, {
316
+ field: category.field,
317
+ fieldType: category.fieldType,
318
+ scale: category.scale
319
+ });
320
+ next = setCartesianPosition(next, current.outlierId, measureChannel, {
321
+ field: candidate.measure,
322
+ fieldType: "quantitative",
323
+ scale: measure.scale
324
+ });
325
+ }
326
+ for (const id of new Set([candidate.xScale.id, candidate.yScale.id])) {
327
+ next = next.rematerializeScale({ id, marks: false, guides: false });
328
+ }
329
+ next = rebindDistributionGuides(next, {
330
+ oldXScale: candidate.previous.x.scale,
331
+ oldYScale: candidate.previous.y.scale,
332
+ newXScale: candidate.xScale.id,
333
+ newYScale: candidate.yScale.id,
334
+ oldXTitle: candidate.previous.x.field,
335
+ oldYTitle: candidate.previous.y.field,
336
+ newXTitle: candidate.x.field,
337
+ newYTitle: candidate.y.field,
338
+ oldMeasureChannel: current.orientation === "vertical" ? "y" : "x",
339
+ newMeasureChannel: candidate.orientation === "vertical" ? "y" : "x"
340
+ });
341
+ for (const scale of [candidate.xScale, candidate.yScale]) {
342
+ if (scale.edit !== undefined) next = next.editScale(scale.edit);
343
+ }
344
+ next = next.rematerializeBarMark({ id: owner.id })
345
+ .rematerializeErrorBar({ id: current.whiskerId });
346
+ for (const capId of capIds) {
347
+ next = next.materializeRuleSpan({
348
+ id: capId,
349
+ orientation: candidate.orientation === "vertical" ? "horizontal" : "vertical",
350
+ size: whiskerConfig.errorBar.capSize
351
+ });
352
+ }
353
+ next = next.rematerializeRuleMark({ id: current.medianId });
354
+ if (hasOutliers && findLayer(next, current.outlierId) !== undefined) {
355
+ next = next.rematerializePointMark({ id: current.outlierId });
356
+ }
357
+ return next;
70
358
  }
71
359
 
72
360
  export const editBoxPlot = action(
@@ -116,6 +404,170 @@ export const editBoxPlot = action(
116
404
  );
117
405
  }
118
406
 
407
+ const candidate = roleCandidate(this, owner, current, args);
408
+ const sourceDataset = findDataset(this, candidate.source);
409
+ if (sourceDataset === undefined) {
410
+ throw new Error(`Unknown box-plot data "${candidate.source}".`);
411
+ }
412
+ const roleRequested = ["data", "x", "y"].some(
413
+ key => Object.hasOwn(args, key)
414
+ );
415
+ const changesRoles = !sameRoleCandidate(current, candidate);
416
+ if (changesRoles) {
417
+ const derived = deriveBoxData(
418
+ sourceDataset.values,
419
+ normalizeBoxTransform({
420
+ type: "boxSummary",
421
+ category: candidate.category,
422
+ field: candidate.measure,
423
+ whisker: whisker.type,
424
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
425
+ })
426
+ );
427
+ const hasOutliers = outliers && whisker.type === "tukey" &&
428
+ derived.outliers.length > 0;
429
+ const whiskerConfig = this.markConfigs[current.whiskerId];
430
+ const capIds = [
431
+ whiskerConfig.errorBar.lowerCapId,
432
+ whiskerConfig.errorBar.upperCapId
433
+ ].filter(Boolean);
434
+ const summaryRevision = planDerivedDataRevision(this, {
435
+ owner: owner.id,
436
+ role: "SummaryData",
437
+ previous: current.summaryId,
438
+ consumers: [owner.id, current.whiskerId, ...capIds, current.medianId]
439
+ });
440
+ const hadOutlierLayer = findLayer(this, current.outlierId) !== undefined;
441
+ const outlierRevision = hasOutliers
442
+ ? planDerivedDataRevision(this, {
443
+ owner: owner.id,
444
+ role: "OutlierData",
445
+ ...(current.outlierDataId === undefined
446
+ ? {}
447
+ : { previous: current.outlierDataId }),
448
+ consumers: hadOutlierLayer ? [current.outlierId] : []
449
+ })
450
+ : undefined;
451
+ const applyEdit = program => {
452
+ let next = program.createBoxSummaryData({
453
+ id: summaryRevision.id,
454
+ source: candidate.source,
455
+ category: candidate.category,
456
+ field: candidate.measure,
457
+ whisker: whisker.type,
458
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
459
+ });
460
+ if (outlierRevision !== undefined) {
461
+ next = next.createBoxOutlierData({
462
+ id: outlierRevision.id,
463
+ source: candidate.source,
464
+ category: candidate.category,
465
+ field: candidate.measure,
466
+ whisker: whisker.type,
467
+ factor: whisker.factor
468
+ });
469
+ for (const rebind of outlierRevision.rebinds) {
470
+ next = next.rebindLayerData(rebind);
471
+ }
472
+ }
473
+ for (const rebind of summaryRevision.rebinds) {
474
+ next = next.rebindLayerData(rebind);
475
+ }
476
+ next = next._withMarkConfig(current.whiskerId, {
477
+ ...next.markConfigs[current.whiskerId],
478
+ errorBar: {
479
+ ...next.markConfigs[current.whiskerId].errorBar,
480
+ data: summaryRevision.id
481
+ }
482
+ });
483
+ if (hadOutlierLayer && !hasOutliers) {
484
+ next = removeOwnedMark(next, current.outlierId);
485
+ }
486
+ next = next._withMarkConfig(owner.id, {
487
+ ...next.markConfigs[owner.id],
488
+ boxPlot: {
489
+ ...current,
490
+ whisker,
491
+ width,
492
+ outliers,
493
+ box,
494
+ median,
495
+ outlier,
496
+ source: candidate.source,
497
+ orientation: candidate.orientation,
498
+ category: candidate.category,
499
+ measure: candidate.measure,
500
+ summaryId: summaryRevision.id,
501
+ outlierDataId: outlierRevision?.id
502
+ },
503
+ barWidth: { band: width },
504
+ fill: box.fill,
505
+ opacity: box.opacity,
506
+ stroke: box.stroke,
507
+ strokeWidth: box.strokeWidth
508
+ });
509
+ next = updateBoxPositions(next, owner, current, candidate, {
510
+ outlierDataId: outlierRevision?.id,
511
+ hasOutliers
512
+ });
513
+ if (!hadOutlierLayer && hasOutliers) {
514
+ const category = candidate.orientation === "vertical"
515
+ ? candidate.x
516
+ : candidate.y;
517
+ const measure = candidate.orientation === "vertical"
518
+ ? candidate.y
519
+ : candidate.x;
520
+ next = next.createBoxOutliers({
521
+ id: current.outlierId,
522
+ data: outlierRevision.id,
523
+ category: candidate.category,
524
+ categoryType: candidate.categoryType,
525
+ measure: candidate.measure,
526
+ orientation: candidate.orientation,
527
+ coordinate: owner.coordinate,
528
+ categoryScale: category.scale,
529
+ measureScale: measure.scale,
530
+ shape: outlier.shape,
531
+ radius: outlier.radius,
532
+ opacity: outlier.opacity
533
+ });
534
+ }
535
+ next = next
536
+ .encodeStroke({ target: current.medianId, value: median.stroke })
537
+ .encodeStrokeWidth({
538
+ target: current.medianId,
539
+ value: median.strokeWidth
540
+ });
541
+ if (findLayer(next, current.outlierId) !== undefined) {
542
+ next = next
543
+ .editPointMark({
544
+ target: current.outlierId,
545
+ shape: outlier.shape,
546
+ opacity: outlier.opacity
547
+ })
548
+ .encodeRadius({ target: current.outlierId, value: outlier.radius });
549
+ }
550
+ next = next.releaseDerivedData(summaryRevision.release);
551
+ if (current.outlierDataId !== undefined) {
552
+ next = next.releaseDerivedData({ id: current.outlierDataId });
553
+ }
554
+ return next._withContext({
555
+ currentMark: owner.id,
556
+ currentData: candidate.source
557
+ });
558
+ };
559
+ applyEdit(this);
560
+ return applyEdit(this);
561
+ }
562
+ if (roleRequested && !OPTIONS.slice(4).some(
563
+ key => Object.hasOwn(args, key)
564
+ )) {
565
+ return this._withContext({
566
+ currentMark: owner.id,
567
+ currentData: current.source
568
+ });
569
+ }
570
+
119
571
  const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
120
572
  outliers !== current.outliers;
121
573
  const changesBox = Object.hasOwn(args, "box") ||
@@ -37,12 +37,12 @@ function plainOptions(value, keys, label, operation = "createBoxPlot") {
37
37
  return value;
38
38
  }
39
39
 
40
- export function resolveBoxPosition(value, label) {
40
+ export function resolveBoxPosition(value, label, operation = "createBoxPlot") {
41
41
  if (value === undefined) return undefined;
42
42
  if (!isPlainObject(value)) {
43
- throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
43
+ throw new TypeError(`${operation} ${label} must be a plain object.`);
44
44
  }
45
- validateKeys(value, ["field", "fieldType", "scale"], `createBoxPlot ${label}`);
45
+ validateKeys(value, ["field", "fieldType", "scale"], `${operation} ${label}`);
46
46
  return value;
47
47
  }
48
48
 
@@ -16,7 +16,9 @@ import { materializeFacetGraphics } from "../../materialization/facets.js";
16
16
  const CONCAT_OPTIONS = Object.freeze([
17
17
  "id", "programs", "gap", "align", "padding"
18
18
  ]);
19
- const LAYOUT_EDIT_OPTIONS = Object.freeze(["gap", "align", "padding"]);
19
+ const LAYOUT_EDIT_OPTIONS = Object.freeze([
20
+ "columns", "gap", "align", "padding"
21
+ ]);
20
22
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
21
23
 
22
24
  function normalizeChildEntry(entry, index) {
@@ -149,7 +151,7 @@ export const vconcatAction = concatAction("vertical", "vconcat");
149
151
  const editCompositionLayout = action(
150
152
  {
151
153
  op: "editCompositionLayout",
152
- description: "Edit composition spacing and alignment.",
154
+ description: "Edit composition layout.",
153
155
  scope: "composition"
154
156
  },
155
157
  function (args = {}) {
@@ -164,7 +166,9 @@ const editCompositionLayout = action(
164
166
  ...childDescriptor({ id, program: this.children[id] }),
165
167
  value: current.facet.values[index]
166
168
  })),
167
- columns: current.columns,
169
+ columns: Object.hasOwn(args, "columns")
170
+ ? args.columns
171
+ : current.columns,
168
172
  gap: Object.hasOwn(args, "gap") ? args.gap : current.gap,
169
173
  align: Object.hasOwn(args, "align") ? args.align : current.align,
170
174
  padding: Object.hasOwn(args, "padding")
@@ -176,12 +180,18 @@ const editCompositionLayout = action(
176
180
  children: this.children,
177
181
  compositionSpec: {
178
182
  ...current,
183
+ columns: layout.columns,
179
184
  gap: layout.gap,
180
185
  align: layout.align,
181
186
  padding: layout.padding
182
187
  }
183
188
  }).materializeComposition();
184
189
  }
190
+ if (Object.hasOwn(args, "columns")) {
191
+ throw new Error(
192
+ "editCompositionLayout columns is available only on a facet composition."
193
+ );
194
+ }
185
195
  const padding = Object.hasOwn(args, "padding")
186
196
  ? normalizeCompositionPadding(args.padding, current.padding)
187
197
  : current.padding;