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
@@ -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
 
@@ -146,3 +146,45 @@ export const createCanvas = action(
146
146
  });
147
147
  }
148
148
  );
149
+
150
+ export const createBasicCanvas = action(
151
+ {
152
+ op: "createCanvas",
153
+ description: "Create and configure the chart canvas."
154
+ },
155
+ function (args = {}) {
156
+ validateOptions(args, "createCanvas", { allowEmpty: true });
157
+ assertCanvasHierarchyAvailable(this);
158
+ const state = {
159
+ width: Object.hasOwn(args, "width") ? args.width : DEFAULT_CANVAS.width,
160
+ height: Object.hasOwn(args, "height") ? args.height : DEFAULT_CANVAS.height,
161
+ background: Object.hasOwn(args, "background")
162
+ ? args.background
163
+ : DEFAULT_CANVAS.background,
164
+ margin: normalizeMargin(args.margin, DEFAULT_MARGIN),
165
+ size: {
166
+ width: Object.hasOwn(args, "width") ? "explicit" : "auto",
167
+ height: Object.hasOwn(args, "height") ? "explicit" : "auto"
168
+ }
169
+ };
170
+ validateCanvasState(state);
171
+ let next = this
172
+ .createGraphics({
173
+ id: CANVAS_GRAPHIC_ID,
174
+ type: "canvas"
175
+ })
176
+ .createGraphics({
177
+ id: PLOT_GRAPHIC_ID,
178
+ type: "collection",
179
+ parent: CANVAS_GRAPHIC_ID
180
+ });
181
+ for (const property of ["width", "height", "background"]) {
182
+ next = next.editGraphics({
183
+ target: CANVAS_GRAPHIC_ID,
184
+ property,
185
+ value: state[property]
186
+ });
187
+ }
188
+ return next._withCanvasConfig({ margin: state.margin, size: state.size });
189
+ }
190
+ );
@@ -1,6 +1,10 @@
1
- import { createCanvas, editCanvas } from "./actions.js";
1
+ import { createBasicCanvas, createCanvas, editCanvas } from "./actions.js";
2
2
 
3
3
  export function registerCanvasActions(ProgramClass) {
4
4
  ProgramClass.prototype.editCanvas = editCanvas;
5
5
  ProgramClass.prototype.createCanvas = createCanvas;
6
6
  }
7
+
8
+ export function registerBasicCanvasActions(ProgramClass) {
9
+ ProgramClass.prototype.createCanvas = createBasicCanvas;
10
+ }
@@ -0,0 +1,13 @@
1
+ import { createBarPlot } from "./bar.js";
2
+ import { createHeatmap } from "./heatmap.js";
3
+ import { createHistogram } from "./histogram.js";
4
+ import { createLinePlot } from "./line.js";
5
+ import { createScatterPlot } from "./scatter.js";
6
+
7
+ export function registerBasicChartActions(ProgramClass) {
8
+ ProgramClass.prototype.createBarPlot = createBarPlot;
9
+ ProgramClass.prototype.createHeatmap = createHeatmap;
10
+ ProgramClass.prototype.createHistogram = createHistogram;
11
+ ProgramClass.prototype.createScatterPlot = createScatterPlot;
12
+ ProgramClass.prototype.createLinePlot = createLinePlot;
13
+ }
@@ -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;
@@ -0,0 +1,11 @@
1
+ import { createData } from "./create.js";
2
+ import { createDerivedData } from "./derived.js";
3
+ import { createBasicBin2DData } from "./basicBin2d.js";
4
+ import { materializeBin2DData } from "./bin2dMaterialize.js";
5
+
6
+ export function registerBasicDataActions(ProgramClass) {
7
+ ProgramClass.prototype.createData = createData;
8
+ ProgramClass.prototype.createDerivedData = createDerivedData;
9
+ ProgramClass.prototype.createBin2DData = createBasicBin2DData;
10
+ ProgramClass.prototype.materializeBin2DData = materializeBin2DData;
11
+ }
@@ -0,0 +1,43 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveBin2DRows,
6
+ normalizeBin2DTransform
7
+ } from "../../grammar/bin2d.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+
10
+ const OPTIONS = Object.freeze([
11
+ "id", "source", "x", "y", "bins", "extent", "includeEmpty", "members", "as"
12
+ ]);
13
+
14
+ export const createBasicBin2DData = action(
15
+ {
16
+ op: "createBin2DData",
17
+ description: "Create immutable rectangular 2D-bin values."
18
+ },
19
+ function (args = {}) {
20
+ validateKeys(args, OPTIONS, "createBin2DData");
21
+ const owner = validateUserId(args.id, "2D bin dataset id");
22
+ if (this.materializationConfigs.data?.bin2d?.[owner] !== undefined) {
23
+ throw new Error(`2D bin owner "${owner}" already exists.`);
24
+ }
25
+ const source = validateUserId(
26
+ args.source ?? this.context.currentData,
27
+ "Source dataset id"
28
+ );
29
+ const dataset = findDataset(this, source);
30
+ if (dataset?.values === undefined) {
31
+ throw new Error(`Source dataset "${source}" has no values.`);
32
+ }
33
+ const transform = normalizeBin2DTransform({ ...args, id: owner });
34
+ deriveBin2DRows(dataset.values, transform);
35
+ return this
36
+ .createDerivedData({ id: owner, source, transform: [transform] })
37
+ .materializeBin2DData({ id: owner })
38
+ ._withMaterializationConfig(
39
+ ["data", "bin2d", owner],
40
+ { current: owner }
41
+ );
42
+ }
43
+ );