ggaction 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -4,19 +4,31 @@ import { validateUserId } from "../../core/identifiers.js";
4
4
  import { validateKeys } from "../../core/validation.js";
5
5
  import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from
6
6
  "../../grammar/boxPlot.js";
7
- import { hasDataset } from "../../selectors/index.js";
7
+ import { planDerivedDataRevision } from
8
+ "../../materialization/dataProvenance.js";
8
9
  import { findDataset } from "../../selectors/datasets.js";
9
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";
10
19
  import {
11
20
  resolveBoxAppearance,
12
21
  resolveBoxMedianAppearance,
13
22
  resolveBoxOutlierAppearance,
23
+ resolveBoxPosition,
14
24
  resolveBoxWhisker,
15
25
  resolveBoxWidth
16
26
  } from "./options.js";
27
+ import { resolveBoxOrientation } from "./resolve.js";
17
28
 
18
29
  const OPTIONS = Object.freeze([
19
- "target", "whisker", "width", "outliers", "box", "median", "outlier"
30
+ "target", "data", "x", "y", "whisker", "width", "outliers", "box",
31
+ "median", "outlier"
20
32
  ]);
21
33
 
22
34
  function resolveBoxOwner(program, requested) {
@@ -61,23 +73,288 @@ function resolveEditedWhisker(current, value) {
61
73
  return resolveBoxWhisker(candidate, "editBoxPlot");
62
74
  }
63
75
 
64
- function nextRevisionId(program, ownerId, kind) {
65
- let revision = 1;
66
- while (hasDataset(program, `${ownerId}${kind}Revision${revision}`)) {
67
- revision += 1;
68
- }
69
- return `${ownerId}${kind}Revision${revision}`;
70
- }
71
-
72
76
  function removeOwnedMark(program, id) {
73
- 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
74
92
  .editSemantic({ property: `layer[${id}]`, remove: true })
75
93
  .editGraphics({ target: id, remove: true })
76
- ._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
+ };
77
201
  }
78
202
 
79
- function rebindRuleData(program, id, data) {
80
- return program.editSemantic({ property: `layer[${id}].data`, value: data });
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;
81
358
  }
82
359
 
83
360
  export const editBoxPlot = action(
@@ -127,6 +404,170 @@ export const editBoxPlot = action(
127
404
  );
128
405
  }
129
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
+
130
571
  const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
131
572
  outliers !== current.outliers;
132
573
  const changesBox = Object.hasOwn(args, "box") ||
@@ -156,7 +597,18 @@ export const editBoxPlot = action(
156
597
  derived.outliers.length > 0;
157
598
 
158
599
  if (revisesData) {
159
- summaryId = nextRevisionId(this, owner.id, "SummaryData");
600
+ const whiskerConfig = next.markConfigs[current.whiskerId];
601
+ const capIds = [
602
+ whiskerConfig.errorBar.lowerCapId,
603
+ whiskerConfig.errorBar.upperCapId
604
+ ].filter(id => id !== undefined);
605
+ const summaryRevision = planDerivedDataRevision(this, {
606
+ owner: owner.id,
607
+ role: "SummaryData",
608
+ previous: current.summaryId,
609
+ consumers: [owner.id, current.whiskerId, ...capIds, current.medianId]
610
+ });
611
+ summaryId = summaryRevision.id;
160
612
  next = next.createBoxSummaryData({
161
613
  id: summaryId,
162
614
  source: current.source,
@@ -166,7 +618,16 @@ export const editBoxPlot = action(
166
618
  ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
167
619
  });
168
620
  if (hasOutliers) {
169
- outlierDataId = nextRevisionId(this, owner.id, "OutlierData");
621
+ const hadOutlierLayer = findLayer(next, current.outlierId) !== undefined;
622
+ const outlierRevision = planDerivedDataRevision(this, {
623
+ owner: owner.id,
624
+ role: "OutlierData",
625
+ ...(current.outlierDataId === undefined
626
+ ? {}
627
+ : { previous: current.outlierDataId }),
628
+ consumers: hadOutlierLayer ? [current.outlierId] : []
629
+ });
630
+ outlierDataId = outlierRevision.id;
170
631
  next = next.createBoxOutlierData({
171
632
  id: outlierDataId,
172
633
  source: current.source,
@@ -175,37 +636,24 @@ export const editBoxPlot = action(
175
636
  whisker: whisker.type,
176
637
  factor: whisker.factor
177
638
  });
639
+ for (const rebind of outlierRevision.rebinds) {
640
+ next = next.rebindLayerData(rebind);
641
+ }
178
642
  } else {
179
643
  outlierDataId = undefined;
180
644
  }
181
645
 
182
- next = next.editSemantic({
183
- property: `layer[${owner.id}].data`,
184
- value: summaryId
185
- });
186
- const whiskerConfig = next.markConfigs[current.whiskerId];
187
- const capIds = [
188
- whiskerConfig.errorBar.lowerCapId,
189
- whiskerConfig.errorBar.upperCapId
190
- ].filter(id => id !== undefined);
191
- next = rebindRuleData(next, current.whiskerId, summaryId)
192
- ._withMarkConfig(current.whiskerId, {
646
+ for (const rebind of summaryRevision.rebinds) {
647
+ next = next.rebindLayerData(rebind);
648
+ }
649
+ next = next._withMarkConfig(current.whiskerId, {
193
650
  ...whiskerConfig,
194
651
  errorBar: { ...whiskerConfig.errorBar, data: summaryId }
195
652
  });
196
- for (const capId of capIds) {
197
- next = rebindRuleData(next, capId, summaryId);
198
- }
199
- next = rebindRuleData(next, current.medianId, summaryId);
200
653
 
201
654
  const hadOutlierLayer = findLayer(next, current.outlierId) !== undefined;
202
655
  if (hadOutlierLayer && !hasOutliers) {
203
656
  next = removeOwnedMark(next, current.outlierId);
204
- } else if (hadOutlierLayer && hasOutliers) {
205
- next = next.editSemantic({
206
- property: `layer[${current.outlierId}].data`,
207
- value: outlierDataId
208
- });
209
657
  } else if (!hadOutlierLayer && hasOutliers) {
210
658
  next = next.createBoxOutliers({
211
659
  id: current.outlierId,
@@ -189,6 +189,9 @@ export const materializeBoxPlot = action(
189
189
  outlierDataId: hasOutliers ? outlierDataId : undefined
190
190
  }
191
191
  });
192
+ if (config.guides !== false) {
193
+ next = next.createGuides(config.guides);
194
+ }
192
195
  return next._withContext({ currentMark: ownerId, currentData: source });
193
196
  }
194
197
  );
@@ -8,10 +8,11 @@ import {
8
8
  } from "../../core/validation.js";
9
9
  import { validatePointShape } from "../../grammar/pointShapes.js";
10
10
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
11
+ import { normalizeGuides } from "../charts/shared.js";
11
12
 
12
13
  export const BOX_PLOT_OPTIONS = Object.freeze([
13
14
  "id", "target", "data", "x", "y", "coordinate", "whisker",
14
- "width", "outliers", "box", "median", "outlier"
15
+ "width", "outliers", "box", "median", "outlier", "guides"
15
16
  ]);
16
17
 
17
18
  const DEFAULT_BOX = Object.freeze({
@@ -36,12 +37,12 @@ function plainOptions(value, keys, label, operation = "createBoxPlot") {
36
37
  return value;
37
38
  }
38
39
 
39
- export function resolveBoxPosition(value, label) {
40
+ export function resolveBoxPosition(value, label, operation = "createBoxPlot") {
40
41
  if (value === undefined) return undefined;
41
42
  if (!isPlainObject(value)) {
42
- throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
43
+ throw new TypeError(`${operation} ${label} must be a plain object.`);
43
44
  }
44
- validateKeys(value, ["field", "fieldType", "scale"], `createBoxPlot ${label}`);
45
+ validateKeys(value, ["field", "fieldType", "scale"], `${operation} ${label}`);
45
46
  return value;
46
47
  }
47
48
 
@@ -52,6 +53,12 @@ export function boxEncodingArgs(value) {
52
53
  };
53
54
  }
54
55
 
56
+ export function resolveBoxGuides(value) {
57
+ return value === undefined
58
+ ? false
59
+ : normalizeGuides(value, "createBoxPlot");
60
+ }
61
+
55
62
  export function resolveBoxWhisker(value, operation = "createBoxPlot") {
56
63
  if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
57
64
  if (!isPlainObject(value)) {
@@ -11,7 +11,9 @@ export function resolveBoxOrientation(x, y) {
11
11
  return undefined;
12
12
  }
13
13
 
14
- export function resolveBoxSourceLayer(program, target) {
14
+ export function resolveBoxSourceLayer(program, target, {
15
+ requiresInference = true
16
+ } = {}) {
15
17
  if (target !== undefined) {
16
18
  const layer = findLayer(program, validateUserId(target, "Box source layer id"));
17
19
  if (layer === undefined) throw new Error(`Unknown box source layer "${target}".`);
@@ -24,7 +26,13 @@ export function resolveBoxSourceLayer(program, target) {
24
26
  const eligible = program.semanticSpec.layers.filter(
25
27
  layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
26
28
  );
27
- return eligible.length === 1 ? eligible[0] : undefined;
29
+ if (eligible.length === 1) return eligible[0];
30
+ if (eligible.length > 1 && requiresInference) {
31
+ throw new Error(
32
+ "createBoxPlot target is ambiguous; provide target or explicit x and y."
33
+ );
34
+ }
35
+ return undefined;
28
36
  }
29
37
 
30
38
  export function resolveBoxPlotId(program, requested) {