ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to `ggaction` are recorded in this file.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## [0.0.3] - 2026-07-19
8
+
9
+ ### Added
10
+
11
+ - Added complete Polar point, line/radar, arc/donut/rose/radial-bar authoring with theta/radius guides, selection,
12
+ highlighting, and Canvas/PNG rendering.
13
+ - Added immutable horizontal and vertical program composition, nested child snapshots, layout editing, stable child
14
+ replacement, and Cartesian facets with derived-data replay, scale resolution, outer axes, and shared legends.
15
+ - Added text and rect marks, directional offsets, horizontal grouped bars, and compatible shared temporal bar/line
16
+ position inference.
17
+
18
+ ### Changed
19
+
20
+ - Nested compositions now preserve their intrinsic layout and honor outer cross-axis alignment instead of stretching
21
+ internal cells or leaving unequal snapshots pinned to the start edge.
22
+
23
+ ### Fixed
24
+
25
+ - Corrected zero-baseline and signed geometry for aggregate bars, and materialized complete ranged or aggregate bars
26
+ with their documented default width.
27
+ - Made horizontal error bands compose with color and explicit boundaries in the same supported cases as vertical bands.
28
+ - Added standalone point-size legends and interval-aware temporal axis labels without duplicate automatic tick text.
29
+ - Published the exact `createDerivedData` transform-array contract and stable `ChartProgram` state-inspection paths in
30
+ TypeScript and user documentation.
31
+
5
32
  ## [0.0.2] - 2026-07-17
6
33
 
7
34
  ### Fixed
@@ -31,5 +58,6 @@ All notable changes to `ggaction` are recorded in this file.
31
58
  - Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
32
59
  not imply complete polar rendering.
33
60
 
61
+ [0.0.3]: https://github.com/hj-n/ggaction/releases/tag/v0.0.3
34
62
  [0.0.2]: https://github.com/hj-n/ggaction/releases/tag/v0.0.2
35
63
  [0.0.1]: https://github.com/hj-n/ggaction/releases/tag/v0.0.1
package/README.md CHANGED
@@ -37,7 +37,7 @@ render(program, document.querySelector("#chart").getContext("2d"));
37
37
  The renderer reads only fully materialized, backend-neutral graphics. Semantic
38
38
  state is never automatically compiled during rendering.
39
39
 
40
- > **Status:** `0.0.2` is the current experimental public release. APIs may change
40
+ > **Status:** `0.0.3` is the current experimental public release. APIs may change
41
41
  > before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
42
42
 
43
43
  ## Install
@@ -96,6 +96,17 @@ Tukey/min–max ranges, configurable factor and component appearance, and
96
96
  optional outlier rendering from categorical and quantitative field pairs.
97
97
  The [mark-selection example](https://github.com/hj-n/ggaction/tree/main/examples/mark-selection/) compares grouped point,
98
98
  complete stacked-bar, and line-series highlighting through one selector grammar.
99
+ The [program-composition example](https://github.com/hj-n/ggaction/tree/main/examples/program-composition/) combines
100
+ complete child programs, edits their inferred layout, and replaces one stable slot.
101
+ The [facet example](https://github.com/hj-n/ggaction/tree/main/examples/cars-origin-scatterplot-facet/)
102
+ repeats one chart with `.facet({ field: "Origin" })`, channel-level shared or
103
+ independent scales, per-cell or occupied-outer axes, and an optional compatible
104
+ parent-owned legend. Regression, density, interval, and box-derived data are
105
+ recomputed independently inside each cell.
106
+ Complete Cartesian and Polar programs can be nested with `hconcat` and
107
+ `vconcat`; the [cross-feature dashboard](https://github.com/hj-n/ggaction/tree/main/examples/cross-feature-dashboard/)
108
+ shows a Polar child replacement beside a Cartesian facet. Polar sources cannot
109
+ currently be faceted and fail before partial child state is created.
99
110
  Completed programs can also be exported through
100
111
  [`ggaction/png`](https://hyeonword.com/ggaction/api/rendering/#png-output).
101
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ggaction",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Build charts through immutable, traceable graphical actions.",
5
5
  "license": "MIT",
6
6
  "author": "Hyeon Jeon",
@@ -59,6 +59,11 @@
59
59
  "artifacts:gallery:test": "node scripts/test-roadmap-gallery.js",
60
60
  "docs:images": "node scripts/generate-doc-images.js",
61
61
  "docs:llms": "node scripts/generate-llm-docs.js",
62
+ "docs:signatures": "node scripts/generate-doc-signatures.js",
63
+ "docs:signatures:check": "node scripts/generate-doc-signatures.js --check",
64
+ "docs:generate": "npm run docs:signatures && npm run docs:images && npm run docs:llms",
65
+ "docs:build": "bundle exec jekyll build --source docs --destination _site --baseurl /ggaction",
66
+ "docs:verify": "npm run docs:generate && npm run test:docs && npm run docs:build && npm run test:docs:built && npm run test:docs:browser",
62
67
  "contracts:catalog": "node scripts/generate-action-catalog.js",
63
68
  "contracts:catalog:check": "node scripts/generate-action-catalog.js --check",
64
69
  "package:check": "node scripts/package-artifact.js --check",
@@ -64,7 +64,8 @@ export const createBoxOutliers = action(
64
64
  let next = this.createPointMark({
65
65
  id: args.id,
66
66
  data: args.data,
67
- shape: args.shape
67
+ shape: args.shape,
68
+ fill: "#111111"
68
69
  });
69
70
  next = next[categoryAction]({
70
71
  target: args.id,
@@ -82,7 +83,6 @@ export const createBoxOutliers = action(
82
83
  });
83
84
  return next
84
85
  .encodeRadius({ target: args.id, value: args.radius })
85
- .encodeOpacity({ target: args.id, value: args.opacity })
86
- .editGraphics({ target: args.id, property: "fill", value: "#111111" });
86
+ .encodeOpacity({ target: args.id, value: args.opacity });
87
87
  }
88
88
  );
@@ -0,0 +1,277 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from
6
+ "../../grammar/boxPlot.js";
7
+ import { hasDataset } from "../../selectors/index.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findLayer } from "../../selectors/layers.js";
10
+ import {
11
+ resolveBoxAppearance,
12
+ resolveBoxMedianAppearance,
13
+ resolveBoxOutlierAppearance,
14
+ resolveBoxWhisker,
15
+ resolveBoxWidth
16
+ } from "./options.js";
17
+
18
+ const OPTIONS = Object.freeze([
19
+ "target", "whisker", "width", "outliers", "box", "median", "outlier"
20
+ ]);
21
+
22
+ function resolveBoxOwner(program, requested) {
23
+ const eligible = program.semanticSpec.layers.filter(
24
+ layer => program.markConfigs[layer.id]?.boxPlot?.materialized === true
25
+ );
26
+ if (requested !== undefined) {
27
+ const id = validateUserId(requested, "Box-plot owner id");
28
+ const layer = findLayer(program, id);
29
+ if (layer === undefined || !eligible.includes(layer)) {
30
+ throw new Error(`Unknown box-plot owner "${id}".`);
31
+ }
32
+ return layer;
33
+ }
34
+ const current = findLayer(program, program.context.currentMark);
35
+ if (current !== undefined && eligible.includes(current)) return current;
36
+ if (eligible.length === 1) return eligible[0];
37
+ if (eligible.length === 0) throw new Error("No box-plot owner is available.");
38
+ throw new Error("Box-plot owner is ambiguous; provide target.");
39
+ }
40
+
41
+ function requirePatch(value, label) {
42
+ if (!isPlainObject(value)) {
43
+ throw new TypeError(`editBoxPlot ${label} must be a plain object.`);
44
+ }
45
+ return value;
46
+ }
47
+
48
+ function resolveEditedWhisker(current, value) {
49
+ if (value === undefined) return current;
50
+ const patch = requirePatch(value, "whisker");
51
+ validateKeys(patch, ["type", "factor"], "editBoxPlot whisker");
52
+ const type = patch.type ?? current.type;
53
+ const candidate = type === "minmax"
54
+ ? { type, ...(Object.hasOwn(patch, "factor") ? { factor: patch.factor } : {}) }
55
+ : {
56
+ type,
57
+ factor: Object.hasOwn(patch, "factor")
58
+ ? patch.factor
59
+ : current.type === "tukey" ? current.factor : 1.5
60
+ };
61
+ return resolveBoxWhisker(candidate, "editBoxPlot");
62
+ }
63
+
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
+ function removeOwnedMark(program, id) {
73
+ return program
74
+ .editSemantic({ property: `layer[${id}]`, remove: true })
75
+ .editGraphics({ target: id, remove: true })
76
+ ._withoutMaterializationConfig(["marks", id]);
77
+ }
78
+
79
+ function rebindRuleData(program, id, data) {
80
+ return program.editSemantic({ property: `layer[${id}].data`, value: data });
81
+ }
82
+
83
+ export const editBoxPlot = action(
84
+ {
85
+ op: "editBoxPlot",
86
+ description: "Revise one box plot and its owned components."
87
+ },
88
+ function (args = {}) {
89
+ validateKeys(args, OPTIONS, "editBoxPlot");
90
+ if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
91
+ throw new Error("editBoxPlot requires at least one box-plot option.");
92
+ }
93
+ const owner = resolveBoxOwner(this, args.target);
94
+ const current = this.markConfigs[owner.id].boxPlot;
95
+ const whisker = resolveEditedWhisker(current.whisker, args.whisker);
96
+ const width = Object.hasOwn(args, "width")
97
+ ? resolveBoxWidth(args.width, "editBoxPlot")
98
+ : current.width;
99
+ if (Object.hasOwn(args, "outliers") && typeof args.outliers !== "boolean") {
100
+ throw new TypeError("editBoxPlot outliers must be a boolean.");
101
+ }
102
+ const outliers = args.outliers ?? current.outliers;
103
+ const boxPatch = Object.hasOwn(args, "box")
104
+ ? requirePatch(args.box, "box")
105
+ : {};
106
+ const medianPatch = Object.hasOwn(args, "median")
107
+ ? requirePatch(args.median, "median")
108
+ : {};
109
+ const outlierPatch = Object.hasOwn(args, "outlier")
110
+ ? requirePatch(args.outlier, "outlier")
111
+ : {};
112
+ const box = resolveBoxAppearance(
113
+ { ...current.box, ...boxPatch },
114
+ "editBoxPlot"
115
+ );
116
+ const median = resolveBoxMedianAppearance(
117
+ { ...current.median, ...medianPatch },
118
+ "editBoxPlot"
119
+ );
120
+ const outlier = resolveBoxOutlierAppearance(
121
+ { ...current.outlier, ...outlierPatch },
122
+ "editBoxPlot"
123
+ );
124
+ if (Object.hasOwn(boxPatch, "fill") && owner.encoding?.color !== undefined) {
125
+ throw new Error(
126
+ "editBoxPlot box.fill cannot be combined with a color encoding."
127
+ );
128
+ }
129
+
130
+ const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
131
+ outliers !== current.outliers;
132
+ const changesBox = Object.hasOwn(args, "box") ||
133
+ Object.hasOwn(args, "width") || revisesData;
134
+ const changesMedian = Object.hasOwn(args, "median") ||
135
+ Object.hasOwn(args, "width") || revisesData;
136
+ const changesOutlier = Object.hasOwn(args, "outlier") || revisesData;
137
+ const categoryEncoding = current.orientation === "vertical"
138
+ ? owner.encoding.x
139
+ : owner.encoding.y;
140
+ const measureEncoding = current.orientation === "vertical"
141
+ ? owner.encoding.y
142
+ : owner.encoding.x;
143
+ let summaryId = current.summaryId;
144
+ let outlierDataId = current.outlierDataId;
145
+ let next = this;
146
+
147
+ const sourceRows = findDataset(this, current.source).values;
148
+ const derived = deriveBoxData(sourceRows, normalizeBoxTransform({
149
+ type: "boxSummary",
150
+ category: current.category,
151
+ field: current.measure,
152
+ whisker: whisker.type,
153
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
154
+ }));
155
+ const hasOutliers = outliers && whisker.type === "tukey" &&
156
+ derived.outliers.length > 0;
157
+
158
+ if (revisesData) {
159
+ summaryId = nextRevisionId(this, owner.id, "SummaryData");
160
+ next = next.createBoxSummaryData({
161
+ id: summaryId,
162
+ source: current.source,
163
+ category: current.category,
164
+ field: current.measure,
165
+ whisker: whisker.type,
166
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
167
+ });
168
+ if (hasOutliers) {
169
+ outlierDataId = nextRevisionId(this, owner.id, "OutlierData");
170
+ next = next.createBoxOutlierData({
171
+ id: outlierDataId,
172
+ source: current.source,
173
+ category: current.category,
174
+ field: current.measure,
175
+ whisker: whisker.type,
176
+ factor: whisker.factor
177
+ });
178
+ } else {
179
+ outlierDataId = undefined;
180
+ }
181
+
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, {
193
+ ...whiskerConfig,
194
+ errorBar: { ...whiskerConfig.errorBar, data: summaryId }
195
+ });
196
+ for (const capId of capIds) {
197
+ next = rebindRuleData(next, capId, summaryId);
198
+ }
199
+ next = rebindRuleData(next, current.medianId, summaryId);
200
+
201
+ const hadOutlierLayer = findLayer(next, current.outlierId) !== undefined;
202
+ if (hadOutlierLayer && !hasOutliers) {
203
+ 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
+ } else if (!hadOutlierLayer && hasOutliers) {
210
+ next = next.createBoxOutliers({
211
+ id: current.outlierId,
212
+ data: outlierDataId,
213
+ category: current.category,
214
+ categoryType: categoryEncoding.fieldType,
215
+ measure: current.measure,
216
+ orientation: current.orientation,
217
+ coordinate: owner.coordinate,
218
+ categoryScale: categoryEncoding.scale,
219
+ measureScale: measureEncoding.scale,
220
+ shape: outlier.shape,
221
+ radius: outlier.radius,
222
+ opacity: outlier.opacity
223
+ });
224
+ }
225
+ }
226
+
227
+ next = next._withMarkConfig(owner.id, {
228
+ ...next.markConfigs[owner.id],
229
+ boxPlot: {
230
+ ...current,
231
+ whisker,
232
+ width,
233
+ outliers,
234
+ box,
235
+ median,
236
+ outlier,
237
+ summaryId,
238
+ outlierDataId
239
+ },
240
+ barWidth: { band: width },
241
+ fill: box.fill,
242
+ opacity: box.opacity,
243
+ stroke: box.stroke,
244
+ strokeWidth: box.strokeWidth
245
+ });
246
+
247
+ if (changesBox) next = next.rematerializeBarMark({ id: owner.id });
248
+ if (revisesData) {
249
+ next = next.rematerializeErrorBar({ id: current.whiskerId });
250
+ }
251
+ if (changesMedian) {
252
+ next = next
253
+ .encodeStroke({ target: current.medianId, value: median.stroke })
254
+ .encodeStrokeWidth({
255
+ target: current.medianId,
256
+ value: median.strokeWidth
257
+ });
258
+ }
259
+ if (findLayer(next, current.outlierId) !== undefined && changesOutlier) {
260
+ next = next
261
+ .editPointMark({
262
+ target: current.outlierId,
263
+ shape: outlier.shape,
264
+ opacity: outlier.opacity
265
+ })
266
+ .encodeRadius({ target: current.outlierId, value: outlier.radius });
267
+ }
268
+
269
+ if (revisesData) {
270
+ next = next.releaseDerivedData({ id: current.summaryId });
271
+ if (current.outlierDataId !== undefined) {
272
+ next = next.releaseDerivedData({ id: current.outlierDataId });
273
+ }
274
+ }
275
+ return next._withContext({ currentMark: owner.id, currentData: current.source });
276
+ }
277
+ );
@@ -1,9 +1,11 @@
1
1
  import { createBoxMedian, createBoxOutliers } from "./components.js";
2
2
  import { createBoxPlot } from "./create.js";
3
+ import { editBoxPlot } from "./edit.js";
3
4
  import { materializeBoxPlot } from "./materialize.js";
4
5
 
5
6
  export function registerBoxPlotActions(ProgramClass) {
6
7
  ProgramClass.prototype.createBoxPlot = createBoxPlot;
8
+ ProgramClass.prototype.editBoxPlot = editBoxPlot;
7
9
  ProgramClass.prototype.materializeBoxPlot = materializeBoxPlot;
8
10
  ProgramClass.prototype.createBoxMedian = createBoxMedian;
9
11
  ProgramClass.prototype.createBoxOutliers = createBoxOutliers;
@@ -50,7 +50,11 @@ export const materializeBoxPlot = action(
50
50
  source,
51
51
  orientation,
52
52
  category: category.field,
53
- measure: measure.field
53
+ measure: measure.field,
54
+ summaryId,
55
+ whiskerId,
56
+ medianId,
57
+ outlierId
54
58
  },
55
59
  barWidth: { band: config.width },
56
60
  fill: config.box.fill,
@@ -178,6 +182,13 @@ export const materializeBoxPlot = action(
178
182
  opacity: config.outlier.opacity
179
183
  });
180
184
  }
185
+ next = next._withMarkConfig(ownerId, {
186
+ ...next.markConfigs[ownerId],
187
+ boxPlot: {
188
+ ...next.markConfigs[ownerId].boxPlot,
189
+ outlierDataId: hasOutliers ? outlierDataId : undefined
190
+ }
191
+ });
181
192
  return next._withContext({ currentMark: ownerId, currentData: source });
182
193
  }
183
194
  );
@@ -27,12 +27,12 @@ const DEFAULT_OUTLIER = Object.freeze({
27
27
  opacity: 0.75
28
28
  });
29
29
 
30
- function plainOptions(value, keys, label) {
30
+ function plainOptions(value, keys, label, operation = "createBoxPlot") {
31
31
  if (value === undefined) return {};
32
32
  if (!isPlainObject(value)) {
33
- throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
33
+ throw new TypeError(`${operation} ${label} must be a plain object.`);
34
34
  }
35
- validateKeys(value, keys, `createBoxPlot ${label}`);
35
+ validateKeys(value, keys, `${operation} ${label}`);
36
36
  return value;
37
37
  }
38
38
 
@@ -52,12 +52,12 @@ export function boxEncodingArgs(value) {
52
52
  };
53
53
  }
54
54
 
55
- export function resolveBoxWhisker(value) {
55
+ export function resolveBoxWhisker(value, operation = "createBoxPlot") {
56
56
  if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
57
57
  if (!isPlainObject(value)) {
58
- throw new TypeError("createBoxPlot whisker must be a plain object.");
58
+ throw new TypeError(`${operation} whisker must be a plain object.`);
59
59
  }
60
- validateKeys(value, ["type", "factor"], "createBoxPlot whisker");
60
+ validateKeys(value, ["type", "factor"], `${operation} whisker`);
61
61
  const type = value.type ?? "tukey";
62
62
  if (!["tukey", "minmax"].includes(type)) {
63
63
  throw new Error(`Unsupported createBoxPlot whisker type "${type}".`);
@@ -77,8 +77,8 @@ export function resolveBoxWhisker(value) {
77
77
  return Object.freeze({ type, factor });
78
78
  }
79
79
 
80
- export function resolveBoxWidth(value) {
81
- const options = plainOptions(value, ["band"], "width");
80
+ export function resolveBoxWidth(value, operation = "createBoxPlot") {
81
+ const options = plainOptions(value, ["band"], "width", operation);
82
82
  const band = options.band ?? 0.7;
83
83
  if (!Number.isFinite(band) || band <= 0 || band >= 1) {
84
84
  throw new RangeError(
@@ -88,11 +88,12 @@ export function resolveBoxWidth(value) {
88
88
  return band;
89
89
  }
90
90
 
91
- export function resolveBoxAppearance(value) {
91
+ export function resolveBoxAppearance(value, operation = "createBoxPlot") {
92
92
  const options = plainOptions(
93
93
  value,
94
94
  ["fill", "opacity", "stroke", "strokeWidth"],
95
- "box"
95
+ "box",
96
+ operation
96
97
  );
97
98
  return Object.freeze({
98
99
  fill: options.fill === undefined
@@ -113,8 +114,13 @@ export function resolveBoxAppearance(value) {
113
114
  });
114
115
  }
115
116
 
116
- export function resolveBoxMedianAppearance(value) {
117
- const options = plainOptions(value, ["stroke", "strokeWidth"], "median");
117
+ export function resolveBoxMedianAppearance(value, operation = "createBoxPlot") {
118
+ const options = plainOptions(
119
+ value,
120
+ ["stroke", "strokeWidth"],
121
+ "median",
122
+ operation
123
+ );
118
124
  return Object.freeze({
119
125
  stroke: options.stroke === undefined
120
126
  ? DEFAULT_MEDIAN.stroke
@@ -128,8 +134,13 @@ export function resolveBoxMedianAppearance(value) {
128
134
  });
129
135
  }
130
136
 
131
- export function resolveBoxOutlierAppearance(value) {
132
- const options = plainOptions(value, ["shape", "radius", "opacity"], "outlier");
137
+ export function resolveBoxOutlierAppearance(value, operation = "createBoxPlot") {
138
+ const options = plainOptions(
139
+ value,
140
+ ["shape", "radius", "opacity"],
141
+ "outlier",
142
+ operation
143
+ );
133
144
  return Object.freeze({
134
145
  shape: options.shape === undefined
135
146
  ? DEFAULT_OUTLIER.shape
@@ -6,6 +6,7 @@ import {
6
6
  validateCanvasState
7
7
  } from "../../layout/canvas.js";
8
8
  import { cloneAndFreeze } from "../../core/immutable.js";
9
+ import { validateOptionObject } from "../../core/validation.js";
9
10
  import {
10
11
  applyMaterializationPlan,
11
12
  planCanvasRematerialization
@@ -13,6 +14,7 @@ import {
13
14
  import {
14
15
  assertCanvasHierarchyAvailable,
15
16
  CANVAS_GRAPHIC_ID,
17
+ findCanvasGraphic,
16
18
  PLOT_GRAPHIC_ID
17
19
  } from "../../materialization/graphicHierarchy.js";
18
20
 
@@ -24,21 +26,11 @@ const CANVAS_OPTIONS = Object.freeze([
24
26
  ]);
25
27
 
26
28
  function validateOptions(args, operation, { allowEmpty = false } = {}) {
27
- const keys = Object.keys(args);
28
-
29
- if (!allowEmpty && keys.length === 0) {
30
- throw new TypeError(`${operation} requires at least one option.`);
31
- }
32
-
33
- for (const key of keys) {
34
- if (!CANVAS_OPTIONS.includes(key)) {
35
- throw new Error(`Unknown ${operation} option "${key}".`);
36
- }
37
- }
29
+ validateOptionObject(args, CANVAS_OPTIONS, operation, { allowEmpty });
38
30
  }
39
31
 
40
32
  function requireCanvas(program) {
41
- const canvas = program.graphicSpec.objects.canvas;
33
+ const canvas = findCanvasGraphic(program);
42
34
 
43
35
  if (canvas?.type !== "canvas") {
44
36
  throw new Error("editCanvas requires an existing canvas.");
@@ -49,8 +41,9 @@ function requireCanvas(program) {
49
41
 
50
42
  function resolveCanvasState(program, args) {
51
43
  const canvas = requireCanvas(program);
52
- const baseMargin =
53
- program.materializationConfigs.canvas?.margin ?? DEFAULT_MARGIN;
44
+ const canvasConfig = program.materializationConfigs.canvas;
45
+ const baseMargin = canvasConfig?.margin ?? DEFAULT_MARGIN;
46
+ const baseSize = canvasConfig?.size ?? { width: "explicit", height: "explicit" };
54
47
  const state = {
55
48
  width: Object.hasOwn(args, "width")
56
49
  ? args.width
@@ -63,7 +56,11 @@ function resolveCanvasState(program, args) {
63
56
  : canvas.properties.background,
64
57
  margin: Object.hasOwn(args, "margin")
65
58
  ? normalizeMargin(args.margin, baseMargin)
66
- : baseMargin
59
+ : baseMargin,
60
+ size: {
61
+ width: Object.hasOwn(args, "width") ? "explicit" : baseSize.width,
62
+ height: Object.hasOwn(args, "height") ? "explicit" : baseSize.height
63
+ }
67
64
  };
68
65
 
69
66
  validateCanvasState(state);
@@ -90,7 +87,7 @@ export const editCanvas = action(
90
87
  }
91
88
  }
92
89
 
93
- next = next._withCanvasConfig({ margin: state.margin });
90
+ next = next._withCanvasConfig({ margin: state.margin, size: state.size });
94
91
 
95
92
  if (
96
93
  Object.hasOwn(args, "width") ||
@@ -132,7 +129,7 @@ export const createCanvas = action(
132
129
  : DEFAULT_CANVAS.margin
133
130
  };
134
131
 
135
- return this
132
+ const next = this
136
133
  .createGraphics({ id: CANVAS_GRAPHIC_ID, type: "canvas" })
137
134
  .createGraphics({
138
135
  id: PLOT_GRAPHIC_ID,
@@ -140,5 +137,12 @@ export const createCanvas = action(
140
137
  parent: CANVAS_GRAPHIC_ID
141
138
  })
142
139
  .editCanvas(options);
140
+ return next._withCanvasConfig({
141
+ ...next.materializationConfigs.canvas,
142
+ size: {
143
+ width: Object.hasOwn(args, "width") ? "explicit" : "auto",
144
+ height: Object.hasOwn(args, "height") ? "explicit" : "auto"
145
+ }
146
+ });
143
147
  }
144
148
  );