ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,3 +1,4 @@
1
+ import { applyFacadeGuides } from "../charts/shared.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import {
@@ -190,7 +191,7 @@ export const materializeBoxPlot = action(
190
191
  }
191
192
  });
192
193
  if (config.guides !== false) {
193
- next = next.createGuides(config.guides);
194
+ next = applyFacadeGuides(next, config.guides, ownerId);
194
195
  }
195
196
  return next._withContext({ currentMark: ownerId, currentData: source });
196
197
  }
@@ -161,7 +161,9 @@ export const createBasicCanvas = action(
161
161
  background: Object.hasOwn(args, "background")
162
162
  ? args.background
163
163
  : DEFAULT_CANVAS.background,
164
- margin: normalizeMargin(args.margin, DEFAULT_MARGIN),
164
+ margin: Object.hasOwn(args, "margin")
165
+ ? normalizeMargin(args.margin, DEFAULT_MARGIN)
166
+ : DEFAULT_MARGIN,
165
167
  size: {
166
168
  width: Object.hasOwn(args, "width") ? "explicit" : "auto",
167
169
  height: Object.hasOwn(args, "height") ? "explicit" : "auto"
@@ -0,0 +1,211 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ validateNonNegativeFinite,
4
+ validatePositiveFinite,
5
+ validateOptionObject
6
+ } from "../../core/validation.js";
7
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
8
+ import { findCanvasGraphic } from
9
+ "../../materialization/graphicHierarchy.js";
10
+
11
+ const OPTIONS = Object.freeze([
12
+ "padding",
13
+ "minPlotWidth",
14
+ "minPlotHeight",
15
+ "iterationLimit",
16
+ "overflow"
17
+ ]);
18
+ const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
19
+ const DEFAULTS = Object.freeze({
20
+ padding: 0,
21
+ minPlotWidth: 160,
22
+ minPlotHeight: 120,
23
+ iterationLimit: 32,
24
+ overflow: "error"
25
+ });
26
+ const UNITS_PER_PIXEL = 4;
27
+
28
+ function normalizeOptions(args) {
29
+ validateOptionObject(args, OPTIONS, "fitCanvas", { allowEmpty: true });
30
+ const policy = { ...DEFAULTS, ...args };
31
+ validateNonNegativeFinite(policy.padding, "fitCanvas padding");
32
+ validatePositiveFinite(policy.minPlotWidth, "fitCanvas minPlotWidth");
33
+ validatePositiveFinite(policy.minPlotHeight, "fitCanvas minPlotHeight");
34
+ if (!Number.isInteger(policy.iterationLimit) ||
35
+ policy.iterationLimit <= 0 || policy.iterationLimit > 64) {
36
+ throw new RangeError(
37
+ "fitCanvas iterationLimit must be an integer from 1 through 64."
38
+ );
39
+ }
40
+ if (!["error", "report"].includes(policy.overflow)) {
41
+ throw new Error(`Unsupported fitCanvas overflow policy "${policy.overflow}".`);
42
+ }
43
+ return policy;
44
+ }
45
+
46
+ function probe(program, margin) {
47
+ try {
48
+ return { ok: true, program: program.editCanvas({ margin }) };
49
+ } catch (error) {
50
+ return {
51
+ ok: false,
52
+ message: error instanceof Error ? error.message : String(error)
53
+ };
54
+ }
55
+ }
56
+
57
+ function marginWith(margin, side, value) {
58
+ return { ...margin, [side]: value };
59
+ }
60
+
61
+ function minimizeSide(program, margin, side, policy) {
62
+ const current = margin[side];
63
+ const floor = Math.ceil(policy.padding * UNITS_PER_PIXEL) /
64
+ UNITS_PER_PIXEL;
65
+ if (floor > current) {
66
+ const expanded = marginWith(margin, side, floor);
67
+ const result = probe(program, expanded);
68
+ return result.ok
69
+ ? { program: result.program, margin: expanded, iterations: 1, issues: [] }
70
+ : {
71
+ program,
72
+ margin,
73
+ iterations: 1,
74
+ issues: [`${side} padding: ${result.message}`]
75
+ };
76
+ }
77
+
78
+ let low = Math.ceil(policy.padding * UNITS_PER_PIXEL);
79
+ let high = Math.floor(current * UNITS_PER_PIXEL);
80
+ if (low > high) {
81
+ return { program, margin, iterations: 0, issues: [] };
82
+ }
83
+
84
+ const cache = new Map();
85
+ const test = units => {
86
+ if (cache.has(units)) return cache.get(units);
87
+ const result = probe(
88
+ program,
89
+ marginWith(margin, side, units / UNITS_PER_PIXEL)
90
+ );
91
+ cache.set(units, result);
92
+ return result;
93
+ };
94
+ let iterations = 0;
95
+ const upper = test(high);
96
+ iterations += 1;
97
+ if (!upper.ok) {
98
+ return { program, margin, iterations, issues: [] };
99
+ }
100
+
101
+ while (low < high && iterations < policy.iterationLimit) {
102
+ const middle = Math.floor((low + high) / 2);
103
+ const result = test(middle);
104
+ iterations += 1;
105
+ if (result.ok) high = middle;
106
+ else low = middle + 1;
107
+ }
108
+ const best = test(high);
109
+ const fittedMargin = marginWith(
110
+ margin,
111
+ side,
112
+ high / UNITS_PER_PIXEL
113
+ );
114
+ return {
115
+ program: best.program,
116
+ margin: fittedMargin,
117
+ iterations,
118
+ issues: low === high
119
+ ? []
120
+ : [`${side} fitting reached iterationLimit ${policy.iterationLimit}`]
121
+ };
122
+ }
123
+
124
+ function fitMargins(program, policy) {
125
+ let probeProgram = program;
126
+ let margin = { ...program.materializationConfigs.canvas.margin };
127
+ let iterations = 0;
128
+ const issues = [];
129
+ for (const side of SIDES) {
130
+ const result = minimizeSide(probeProgram, margin, side, policy);
131
+ probeProgram = result.program;
132
+ margin = result.margin;
133
+ iterations += result.iterations;
134
+ issues.push(...result.issues);
135
+ }
136
+ return { margin, iterations, issues };
137
+ }
138
+
139
+ function sameMargin(first, second) {
140
+ return SIDES.every(side => first[side] === second[side]);
141
+ }
142
+
143
+ function samePolicy(first, second) {
144
+ return OPTIONS.every(option => first?.[option] === second[option]);
145
+ }
146
+
147
+ function layoutSignature(program) {
148
+ const source = JSON.stringify({
149
+ graphicSpec: program.graphicSpec,
150
+ canvas: program.materializationConfigs.canvas,
151
+ guides: program.materializationConfigs.guides,
152
+ title: program.materializationConfigs.title
153
+ });
154
+ let hash = 0xcbf29ce484222325n;
155
+ for (let index = 0; index < source.length; index += 1) {
156
+ hash ^= BigInt(source.charCodeAt(index));
157
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
158
+ }
159
+ return hash.toString(16).padStart(16, "0");
160
+ }
161
+
162
+ export const fitCanvas = action(
163
+ {
164
+ op: "fitCanvas",
165
+ description: "Fit Canvas margins around existing layout resources."
166
+ },
167
+ function (args = {}) {
168
+ const policy = normalizeOptions(args);
169
+ const canvas = findCanvasGraphic(this);
170
+ if (canvas?.type !== "canvas" ||
171
+ this.materializationConfigs.canvas?.margin === undefined) {
172
+ throw new Error("fitCanvas requires an existing Canvas.");
173
+ }
174
+ const previous = this.materializationConfigs.fitting;
175
+ if (samePolicy(previous?.policy, policy) &&
176
+ previous?.result?.signature === layoutSignature(this)) {
177
+ return this;
178
+ }
179
+ const sourceMargin = this.materializationConfigs.canvas.margin;
180
+ const fitted = fitMargins(this, policy);
181
+ let next = sameMargin(sourceMargin, fitted.margin)
182
+ ? this
183
+ : this.editCanvas({ margin: fitted.margin });
184
+ const plot = resolveGraphicBounds(next);
185
+ if (plot.width < policy.minPlotWidth) {
186
+ fitted.issues.push(
187
+ `plot width ${plot.width} is smaller than minPlotWidth ${policy.minPlotWidth}`
188
+ );
189
+ }
190
+ if (plot.height < policy.minPlotHeight) {
191
+ fitted.issues.push(
192
+ `plot height ${plot.height} is smaller than minPlotHeight ${policy.minPlotHeight}`
193
+ );
194
+ }
195
+ if (fitted.issues.length > 0 && policy.overflow === "error") {
196
+ throw new Error(`fitCanvas overflow: ${fitted.issues.join("; ")}.`);
197
+ }
198
+ next = next._withMaterializationConfig(["fitting"], {
199
+ policy,
200
+ result: {
201
+ status: fitted.issues.length === 0 ? "fit" : "overflow",
202
+ margin: fitted.margin,
203
+ plot: { width: plot.width, height: plot.height },
204
+ iterations: fitted.iterations,
205
+ issues: fitted.issues,
206
+ signature: layoutSignature(next)
207
+ }
208
+ });
209
+ return next;
210
+ }
211
+ );
@@ -1,8 +1,10 @@
1
1
  import { createBasicCanvas, createCanvas, editCanvas } from "./actions.js";
2
+ import { fitCanvas } from "./fitting.js";
2
3
 
3
4
  export function registerCanvasActions(ProgramClass) {
4
5
  ProgramClass.prototype.editCanvas = editCanvas;
5
6
  ProgramClass.prototype.createCanvas = createCanvas;
7
+ ProgramClass.prototype.fitCanvas = fitCanvas;
6
8
  }
7
9
 
8
10
  export function registerBasicCanvasActions(ProgramClass) {
@@ -11,6 +11,7 @@ import {
11
11
  import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
12
12
  import { buildMaterializationPlan } from "../../materialization/planner.js";
13
13
  import {
14
+ CATEGORY_ORDER_CHANNELS,
14
15
  normalizeCategoryOrder,
15
16
  resolveCategoryOrder
16
17
  } from "../../grammar/categoryOrder.js";
@@ -19,8 +20,8 @@ const ORDER_OPTIONS = Object.freeze(["target", "channel", "values", "by", "direc
19
20
  const REMOVE_OPTIONS = Object.freeze(["target", "channel"]);
20
21
 
21
22
  function resolveAssignment(program, args, operation, { activeOnly = false } = {}) {
22
- if (!["x", "y"].includes(args.channel)) {
23
- throw new Error(`${operation} channel must be x or y.`);
23
+ if (!CATEGORY_ORDER_CHANNELS.includes(args.channel)) {
24
+ throw new Error(`${operation} channel must be x, y, or theta.`);
24
25
  }
25
26
  const requested = args.target === undefined
26
27
  ? undefined
@@ -0,0 +1,47 @@
1
+ import { action } from "../../core/action.js";
2
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
3
+ import { normalizeAreaBound, validateAreaEndpointPair } from "../../grammar/areaEndpoints.js";
4
+ import {
5
+ applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
6
+ normalizeFieldEncoding, omitUndefinedOptions, positionArgs, resolveFacadeData, resolveFacadeId,
7
+ validateFacadeOptions
8
+ } from "./shared.js";
9
+
10
+ const OPERATION = "createAreaPlot";
11
+ const OPTIONS = ["id", "data", "coordinate", "x", "y", "valueChannel", "baseline", "groupBy", "layout", "missing", "color", "area", "guides"];
12
+
13
+ export const createAreaPlot = action({ op: OPERATION, description: "Create a simple, ranged, or stacked Cartesian area plot." }, function (args = {}) {
14
+ validateFacadeOptions(args, OPTIONS, OPERATION);
15
+ for (const [key, value] of Object.entries(args)) if (value === null) throw new TypeError(`${OPERATION} ${key} cannot be null.`);
16
+ const id = resolveFacadeId(this, args.id, { defaultId: "areaPlot", operation: OPERATION });
17
+ const data = resolveFacadeData(this, args.data, OPERATION);
18
+ const valueChannel = args.valueChannel ?? "y";
19
+ if (!["x", "y"].includes(valueChannel)) throw new Error("createAreaPlot valueChannel must be x or y.");
20
+ const independentChannel = valueChannel === "y" ? "x" : "y";
21
+ const independent = omitUndefinedOptions(normalizeFieldEncoding(args[independentChannel], `${OPERATION} ${independentChannel}`));
22
+ validateFacadeOptions(independent, ["field", "fieldType", "temporalUnit", "scale"], `${OPERATION} independent position`);
23
+ if (independent.fieldType !== undefined && !["quantitative", "temporal"].includes(independent.fieldType)) {
24
+ throw new Error("createAreaPlot independent position must be quantitative or temporal.");
25
+ }
26
+ const measurement = omitUndefinedOptions(normalizeFieldEncoding(args[valueChannel], `${OPERATION} ${valueChannel}`));
27
+ const ranged = Object.hasOwn(measurement, "lower") || Object.hasOwn(measurement, "upper");
28
+ validateFacadeOptions(measurement, ranged ? ["lower", "upper", "scale"] : ["field", "scale"], `${OPERATION} measurement`);
29
+ if (ranged && args.baseline !== undefined) throw new Error("createAreaPlot baseline cannot be combined with a range.");
30
+ const lower = ranged ? measurement.lower : measurement.field;
31
+ const upper = ranged ? measurement.upper : { datum: args.baseline ?? 0 };
32
+ validateAreaEndpointPair(normalizeAreaBound(lower), normalizeAreaBound(upper));
33
+ const groupBy = args.groupBy === undefined ? undefined : normalizeGroupFields(args.groupBy);
34
+ const area = omitUndefinedOptions(normalizeAppearance(args.area, ["fill", "opacity", "stroke", "strokeWidth", "curve"], `${OPERATION} area`));
35
+ const color = args.color === undefined ? undefined : normalizeCategoricalColor(args.color, `${OPERATION} color`);
36
+ if (color !== undefined && area.fill !== undefined) throw new Error("createAreaPlot area.fill cannot be combined with color.");
37
+ const guides = normalizeCategoricalGuides(args.guides, OPERATION, color);
38
+ let next = this.createAreaMark({ id, data, ...area, missing: args.missing ?? "error" });
39
+ if (groupBy !== undefined) next = next.encodeGroup({ target: id, fields: groupBy });
40
+ next = next[independentChannel === "x" ? "encodeX" : "encodeY"](positionArgs(independent, { target: id, coordinate: args.coordinate }))
41
+ [valueChannel === "y" ? "encodeYRange" : "encodeXRange"]({ target: id, lower, upper,
42
+ ...(measurement.scale === undefined ? {} : { scale: measurement.scale }),
43
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }) })
44
+ .layoutSeries({ target: id, mode: args.layout ?? "overlay" });
45
+ if (color !== undefined) next = next.encodeColor({ target: id, ...color });
46
+ return applyFacadeGuides(next, guides, id, args.guides ?? {});
47
+ });
@@ -1,4 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
+ import { isBarCategoryEncoding } from "../../grammar/bars/policy.js";
2
3
  import {
3
4
  applyFacadeGuides,
4
5
  inferFacadeFieldType,
@@ -42,12 +43,17 @@ export const createBarPlot = action(
42
43
  const width = normalizeTargetOptions(args.width, "createBarPlot width");
43
44
  const guides = normalizeGuides(args.guides, "createBarPlot");
44
45
 
45
- let next = this
46
- .createBarMark({ id, data, ...bar })
47
- .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
48
- .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
46
+ const positions = isBarCategoryEncoding(y)
47
+ ? [["encodeY", y], ["encodeX", x]]
48
+ : [["encodeX", x], ["encodeY", y]];
49
+ let next = this.createBarMark({ id, data, ...bar });
50
+ for (const [operation, encoding] of positions) {
51
+ next = next[operation](positionArgs(encoding, {
52
+ target: id, coordinate: args.coordinate
53
+ }));
54
+ }
49
55
  if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
50
56
  if (width !== undefined) next = next.encodeBarWidth(targetArgs(width, id));
51
- return applyFacadeGuides(next, guides);
57
+ return applyFacadeGuides(next, guides, id);
52
58
  }
53
59
  );
@@ -0,0 +1,70 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
4
+ import { findLayer } from "../../selectors/layers.js";
5
+ import { resolveFacadeId, validateFacadeOptions } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze([
8
+ "id", "data", "coordinate", "x", "y", "color", "size", "shape",
9
+ "point", "packing", "guides"
10
+ ]);
11
+ const PACKING_OPTIONS = Object.freeze(["maxOffset", "padding", "key", "overflow"]);
12
+
13
+ function ownerScale(value, id, label) {
14
+ if (typeof value === "string") return { field: value, scale: { id } };
15
+ if (!isPlainObject(value)) return value;
16
+ if (value.scale !== undefined && !isPlainObject(value.scale)) {
17
+ throw new TypeError(`${label}.scale must be a plain object.`);
18
+ }
19
+ return {
20
+ ...value,
21
+ scale: { ...(value.scale ?? {}), id: value.scale?.id ?? id }
22
+ };
23
+ }
24
+
25
+ function normalizePacking(value, operation) {
26
+ if (value === false) return false;
27
+ const packing = value ?? {};
28
+ validateOptionObject(packing, PACKING_OPTIONS, `${operation} packing`);
29
+ return { ...packing };
30
+ }
31
+
32
+ export const createBeeswarmPlot = action(
33
+ {
34
+ op: "createBeeswarmPlot",
35
+ description: "Create a Point plot packed within categorical slots."
36
+ },
37
+ function (args = {}) {
38
+ const operation = "createBeeswarmPlot";
39
+ validateFacadeOptions(args, OPTIONS, operation);
40
+ if (args.x === undefined || args.y === undefined) {
41
+ throw new Error(`${operation} requires x and y.`);
42
+ }
43
+ const id = resolveFacadeId(this, args.id, {
44
+ defaultId: "beeswarmPlot",
45
+ operation
46
+ });
47
+ const packing = normalizePacking(args.packing, operation);
48
+ let next = this.createStripPlot({
49
+ id,
50
+ ...(args.data === undefined ? {} : { data: args.data }),
51
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
52
+ x: ownerScale(args.x, `${id}X`, `${operation} x`),
53
+ y: ownerScale(args.y, `${id}Y`, `${operation} y`),
54
+ ...(args.color === undefined ? {} : { color: args.color }),
55
+ ...(args.size === undefined ? {} : { size: args.size }),
56
+ ...(args.shape === undefined ? {} : { shape: args.shape }),
57
+ ...(args.point === undefined ? {} : { point: args.point }),
58
+ ...(args.guides === undefined ? {} : { guides: args.guides })
59
+ });
60
+ if (packing === false) return next;
61
+ const layer = findLayer(next, id);
62
+ const categoryChannel = ["x", "y"].find(channel =>
63
+ ["nominal", "ordinal"].includes(layer?.encoding?.[channel]?.fieldType)
64
+ );
65
+ if (categoryChannel === undefined) {
66
+ throw new Error(`${operation} requires exactly one categorical position.`);
67
+ }
68
+ return next.packPoints({ target: id, channel: categoryChannel, ...packing });
69
+ }
70
+ );
@@ -0,0 +1,44 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
3
+ import {
4
+ applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
5
+ omitUndefinedOptions, resolveFacadeData, resolveFacadeId, validateFacadeOptions
6
+ } from "./shared.js";
7
+
8
+ const OPERATION = "createDensityPlot";
9
+ const OPTIONS = ["id", "data", "coordinate", "field", "groupBy", "bandwidth", "extent", "steps",
10
+ "kernel", "normalization", "as", "densityChannel", "valueScale", "densityScale", "color", "area", "guides"];
11
+ const AREA_OPTIONS = ["fill", "opacity", "stroke", "strokeWidth", "curve"];
12
+
13
+ export const createDensityPlot = action({
14
+ op: OPERATION, description: "Create a baseline kernel-density area plot with optional explicit groups."
15
+ }, function (args = {}) {
16
+ validateFacadeOptions(args, OPTIONS, OPERATION);
17
+ const id = resolveFacadeId(this, args.id, { defaultId: "densityPlot", operation: OPERATION });
18
+ const data = resolveFacadeData(this, args.data, OPERATION);
19
+ validateNonEmptyString(args.field, `${OPERATION} field`);
20
+ const { id: requestedId, data: requestedData, color: requestedColor, area: requestedArea,
21
+ guides: requestedGuides, ...density } = omitUndefinedOptions(args);
22
+ const area = omitUndefinedOptions(normalizeAppearance(requestedArea, AREA_OPTIONS, `${OPERATION} area`));
23
+ for (const [key, value] of Object.entries({ ...density, ...area })) {
24
+ if (value === null) throw new TypeError(`${OPERATION} ${key} cannot be null.`);
25
+ }
26
+ for (const key of ["valueScale", "densityScale"]) {
27
+ if (density[key] === undefined) continue;
28
+ validateOptionObject(density[key], undefined, `${OPERATION} ${key}`);
29
+ density[key] = omitUndefinedOptions(density[key]);
30
+ }
31
+ const color = requestedColor === undefined ? undefined
32
+ : normalizeCategoricalColor(requestedColor, `${OPERATION} color`, ["field", "fieldType", "scale", "palette", "layout"]);
33
+ if (color !== undefined && (color.field !== density.groupBy ||
34
+ color.layout !== undefined && color.layout !== "overlay")) {
35
+ throw new Error(`${OPERATION} color requires the same explicit groupBy field and overlay layout.`);
36
+ }
37
+ if (color !== undefined && area.fill !== undefined) {
38
+ throw new Error(`${OPERATION} area.fill cannot be combined with color.`);
39
+ }
40
+ const guides = normalizeCategoricalGuides(requestedGuides, OPERATION, color);
41
+ let next = this.createAreaMark({ id, data, ...area }).encodeDensity({ ...density, target: id });
42
+ if (color !== undefined) next = next.encodeColor({ ...color, target: id });
43
+ return applyFacadeGuides(next, guides, id, requestedGuides ?? {});
44
+ });
@@ -0,0 +1,183 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
4
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
5
+ import {
6
+ applyFacadeGuides,
7
+ normalizeAppearance,
8
+ normalizeCategoricalColor,
9
+ normalizeGuides,
10
+ resolveFacadeData,
11
+ resolveFacadeId,
12
+ targetArgs,
13
+ validateFacadeOptions
14
+ } from "./shared.js";
15
+
16
+ const OPTIONS = Object.freeze([
17
+ "id", "data", "coordinate", "field", "groupBy", "weight", "missing",
18
+ "as", "color", "line", "labels", "guides"
19
+ ]);
20
+ const EDIT_OPTIONS = Object.freeze([
21
+ "target", "data", "coordinate", "field", "groupBy", "weight", "missing", "as", "color"
22
+ ]);
23
+ const LINE_OPTIONS = Object.freeze(["strokeWidth", "stroke", "opacity"]);
24
+ const LABEL_OPTIONS = Object.freeze([
25
+ "field", "value", "content", "normalizeBy", "format", "layout", "fill",
26
+ "opacity", "fontSize", "fontFamily", "fontWeight", "align", "baseline",
27
+ "rotation", "dx", "dy"
28
+ ]);
29
+
30
+ function normalizeGrouping(value) {
31
+ return value === undefined ? [] : normalizeGroupFields(value);
32
+ }
33
+
34
+ function normalizeLabels(value, operation) {
35
+ if (value === undefined || value === false) return false;
36
+ validateOptionObject(value, LABEL_OPTIONS, `${operation} labels`);
37
+ return { ...value };
38
+ }
39
+
40
+ function normalizeOutputs(value, id, operation) {
41
+ if (value === undefined) {
42
+ return {
43
+ value: `__${id}ECDFData_value`,
44
+ cumulative: `__${id}ECDFData_cumulative`,
45
+ probability: `__${id}ECDFData_probability`
46
+ };
47
+ }
48
+ validateOptionObject(value, ["value", "cumulative", "probability"], `${operation} as`);
49
+ for (const field of ["value", "cumulative", "probability"]) {
50
+ validateNonEmptyString(value[field], `${operation} as.${field}`);
51
+ }
52
+ return { ...value };
53
+ }
54
+
55
+ function resolveOwner(program, requested) {
56
+ const eligible = program.semanticSpec.layers.filter(
57
+ layer => program.markConfigs[layer.id]?.ecdfPlot !== undefined
58
+ );
59
+ if (requested !== undefined) {
60
+ const id = validateUserId(requested, "ECDF-plot owner id");
61
+ const owner = eligible.find(layer => layer.id === id);
62
+ if (owner === undefined) throw new Error(`Unknown ECDF-plot owner "${id}".`);
63
+ return owner;
64
+ }
65
+ const current = eligible.find(layer => layer.id === program.context.currentMark);
66
+ if (current !== undefined) return current;
67
+ if (eligible.length === 1) return eligible[0];
68
+ if (eligible.length === 0) throw new Error("editECDFPlot requires an ECDF plot.");
69
+ throw new Error("editECDFPlot target is ambiguous; provide target.");
70
+ }
71
+
72
+ export const createECDFPlot = action(
73
+ {
74
+ op: "createECDFPlot",
75
+ description: "Create a right-continuous empirical cumulative distribution plot."
76
+ },
77
+ function (args = {}) {
78
+ const operation = "createECDFPlot";
79
+ validateFacadeOptions(args, OPTIONS, operation);
80
+ const id = resolveFacadeId(this, args.id, { defaultId: "ecdfPlot", operation });
81
+ const source = resolveFacadeData(this, args.data, operation);
82
+ validateNonEmptyString(args.field, `${operation} field`);
83
+ if (args.weight !== undefined) validateNonEmptyString(args.weight, `${operation} weight`);
84
+ const groupBy = normalizeGrouping(args.groupBy);
85
+ const as = normalizeOutputs(args.as, id, operation);
86
+ const color = args.color === undefined
87
+ ? undefined
88
+ : normalizeCategoricalColor(args.color, `${operation} color`);
89
+ if (color !== undefined && !groupBy.includes(color.field)) {
90
+ throw new Error(`${operation} color field must be included in groupBy.`);
91
+ }
92
+ const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
93
+ const labels = normalizeLabels(args.labels, operation);
94
+ const guides = normalizeGuides(args.guides, operation);
95
+ const dataId = `${id}ECDFData`;
96
+
97
+ let next = this
98
+ .createECDFData({
99
+ id: dataId,
100
+ source,
101
+ field: args.field,
102
+ ...(groupBy.length === 0 ? {} : { groupBy }),
103
+ ...(args.weight === undefined ? {} : { weight: args.weight }),
104
+ ...(args.missing === undefined ? {} : { missing: args.missing }),
105
+ as
106
+ })
107
+ .createLineMark({ id, data: dataId, curve: "step-after", ...line })
108
+ .encodeX({
109
+ target: id,
110
+ field: as.value,
111
+ fieldType: "quantitative",
112
+ scale: { id: `${id}Value`, zero: false, nice: false },
113
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
114
+ })
115
+ .encodeY({
116
+ target: id,
117
+ field: as.probability,
118
+ fieldType: "quantitative",
119
+ scale: { id: `${id}Probability`, domain: [0, 1], zero: true, nice: false },
120
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
121
+ });
122
+ if (groupBy.length > 0) next = next.encodeGroup({ target: id, fields: groupBy });
123
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
124
+ if (labels !== false) {
125
+ const hasContent = ["field", "value", "content"].some(key => Object.hasOwn(labels, key));
126
+ next = next.createMarkLabels({
127
+ id: `${id}Label`,
128
+ source: id,
129
+ ...labels,
130
+ ...(hasContent ? {} : { field: as.probability })
131
+ });
132
+ }
133
+ next = applyFacadeGuides(next, guides, id, guides);
134
+ return next._withMarkConfig(id, {
135
+ ...next.markConfigs[id],
136
+ ecdfPlot: {
137
+ source,
138
+ data: dataId,
139
+ options: {
140
+ id,
141
+ data: source,
142
+ coordinate: args.coordinate,
143
+ field: args.field,
144
+ ...(groupBy.length === 0 ? {} : { groupBy }),
145
+ ...(args.weight === undefined ? {} : { weight: args.weight }),
146
+ missing: args.missing ?? "drop",
147
+ as,
148
+ ...(color === undefined ? {} : { color }),
149
+ line,
150
+ labels,
151
+ guides
152
+ }
153
+ }
154
+ });
155
+ }
156
+ );
157
+
158
+ export const editECDFPlot = action(
159
+ {
160
+ op: "editECDFPlot",
161
+ description: "Atomically revise an ECDF plot's source or statistical roles."
162
+ },
163
+ function (args = {}) {
164
+ validateFacadeOptions(args, EDIT_OPTIONS, "editECDFPlot");
165
+ const changes = Object.keys(args).filter(key => key !== "target");
166
+ if (changes.length === 0) {
167
+ throw new Error("editECDFPlot requires at least one ECDF option.");
168
+ }
169
+ const owner = resolveOwner(this, args.target);
170
+ const current = this.markConfigs[owner.id].ecdfPlot;
171
+ const revised = { ...current.options };
172
+ for (const key of changes) {
173
+ if (key === "groupBy" && args[key] === false) {
174
+ delete revised.groupBy;
175
+ if (!Object.hasOwn(args, "color")) delete revised.color;
176
+ }
177
+ else if (key === "weight" && args[key] === false) delete revised.weight;
178
+ else if (key === "color" && args[key] === false) delete revised.color;
179
+ else revised[key] = args[key];
180
+ }
181
+ return this.removeMark({ target: owner.id }).createECDFPlot(revised);
182
+ }
183
+ );