ggaction 0.0.11 → 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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -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 +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  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 +79 -24
  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 +153 -11
  83. package/src/actions/errorBars/edit.js +365 -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 +138 -14
  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 +60 -4
  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/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -0,0 +1,279 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateNonEmptyString, validateNonNegativeFinite, validateOptionObject } from
4
+ "../../core/validation.js";
5
+ import {
6
+ applyFacadeGuides,
7
+ normalizeAppearance,
8
+ normalizeEncoding,
9
+ normalizeFieldEncoding,
10
+ normalizeGuides,
11
+ omitUndefinedOptions,
12
+ positionArgs,
13
+ resolveFacadeData,
14
+ resolveFacadeId,
15
+ targetArgs,
16
+ inferFacadeFieldType,
17
+ validateFacadeOptions
18
+ } from "./shared.js";
19
+
20
+ const RUG_OPTIONS = Object.freeze(["id", "data", "coordinate", "x", "y", "edge", "tick", "guides"]);
21
+ const MEASURE_OPTIONS = Object.freeze(["field", "fieldType", "temporalUnit", "scale"]);
22
+ const TICK_OPTIONS = Object.freeze(["length", "stroke", "strokeWidth", "opacity"]);
23
+ const STRIP_OPTIONS = Object.freeze([
24
+ "id", "data", "coordinate", "x", "y", "color", "size", "shape", "point", "jitter", "guides"
25
+ ]);
26
+ const POINT_OPTIONS = Object.freeze([
27
+ "radius", "shape", "fill", "opacity", "stroke", "strokeWidth"
28
+ ]);
29
+ const JITTER_OPTIONS = Object.freeze(["maxOffset", "seed", "key"]);
30
+
31
+ function normalizeMeasure(value, operation, channel) {
32
+ const result = normalizeFieldEncoding(value, `${operation} ${channel}`);
33
+ validateOptionObject(result, MEASURE_OPTIONS, `${operation} ${channel}`);
34
+ validateNonEmptyString(result.field, `${operation} ${channel} field`);
35
+ const fieldType = result.fieldType ?? "quantitative";
36
+ if (!["quantitative", "temporal"].includes(fieldType)) {
37
+ throw new Error(`${operation} ${channel} must be quantitative or temporal.`);
38
+ }
39
+ return { ...result, fieldType };
40
+ }
41
+
42
+ function normalizeRugGuides(value, measure, operation) {
43
+ const guides = normalizeGuides(value, operation);
44
+ if (guides === false) return false;
45
+ validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
46
+ const anchor = measure === "x" ? "y" : "x";
47
+ if (guides.legend !== undefined && guides.legend !== false) {
48
+ throw new Error(`${operation} does not have a legend encoding.`);
49
+ }
50
+ let axes = guides.axes;
51
+ if (axes !== undefined && axes !== false) {
52
+ if (!isPlainObject(axes)) {
53
+ throw new TypeError(`${operation} guides.axes must be false or a plain object.`);
54
+ }
55
+ if (axes[anchor] !== undefined && axes[anchor] !== false) {
56
+ throw new Error(`${operation} cannot create an axis for its constant anchor.`);
57
+ }
58
+ axes = { ...axes, [anchor]: false };
59
+ } else if (axes === undefined) {
60
+ axes = { [measure]: {}, [anchor]: false };
61
+ }
62
+ let grid = guides.grid;
63
+ const anchorGrid = measure === "x" ? "horizontal" : "vertical";
64
+ if (grid !== undefined && grid !== false) {
65
+ if (!isPlainObject(grid)) {
66
+ throw new TypeError(`${operation} guides.grid must be false or a plain object.`);
67
+ }
68
+ if (grid[anchorGrid] !== undefined && grid[anchorGrid] !== false) {
69
+ throw new Error(`${operation} cannot create a grid for its constant anchor.`);
70
+ }
71
+ grid = { ...grid, [anchorGrid]: false };
72
+ } else if (grid === undefined) {
73
+ grid = false;
74
+ }
75
+ return { axes, grid, legend: false };
76
+ }
77
+
78
+ export const createRugPlot = action(
79
+ {
80
+ op: "createRugPlot",
81
+ description: "Create a one-dimensional Rug plot at an explicit plot edge."
82
+ },
83
+ function (args = {}) {
84
+ const operation = "createRugPlot";
85
+ validateFacadeOptions(args, RUG_OPTIONS, operation);
86
+ const hasX = args.x !== undefined;
87
+ const hasY = args.y !== undefined;
88
+ if (hasX === hasY) {
89
+ throw new Error(`${operation} requires exactly one of x or y.`);
90
+ }
91
+ const measure = hasX ? "x" : "y";
92
+ const edge = args.edge;
93
+ const supportedEdges = measure === "x" ? ["top", "bottom"] : ["left", "right"];
94
+ if (!supportedEdges.includes(edge)) {
95
+ throw new Error(`${operation} ${measure} requires edge ${supportedEdges.join(" or ")}.`);
96
+ }
97
+ const id = resolveFacadeId(this, args.id, {
98
+ defaultId: "rugPlot",
99
+ operation
100
+ });
101
+ const data = resolveFacadeData(this, args.data, operation);
102
+ const encoding = normalizeMeasure(args[measure], operation, measure);
103
+ const tick = normalizeAppearance(args.tick, TICK_OPTIONS, `${operation} tick`);
104
+ const guides = normalizeRugGuides(args.guides, measure, operation);
105
+ const anchor = measure === "x" ? "y" : "x";
106
+ const highEdge = edge === "top" || edge === "right";
107
+
108
+ let next = this.createTickMark({ id, data, ...tick });
109
+ if (args.coordinate !== undefined) {
110
+ next = next.createCoordinate({ id: args.coordinate, type: "cartesian", layers: [id] });
111
+ }
112
+ next = next[measure === "x" ? "encodeX" : "encodeY"](
113
+ positionArgs(encoding, { target: id })
114
+ );
115
+ next = next[anchor === "x" ? "encodeX" : "encodeY"]({
116
+ target: id,
117
+ datum: highEdge ? 1 : 0,
118
+ fieldType: "quantitative",
119
+ scale: { id: `${id}Anchor`, domain: [0, 1], zero: false, nice: false }
120
+ });
121
+ next = next.encodeAngle({ target: id, value: measure === "x" ? 0 : 90 });
122
+ return applyFacadeGuides(next, guides, id, guides);
123
+ }
124
+ );
125
+
126
+ function normalizeStripPosition(program, data, value, operation, channel) {
127
+ const result = normalizeFieldEncoding(value, `${operation} ${channel}`);
128
+ validateOptionObject(result, MEASURE_OPTIONS, `${operation} ${channel}`);
129
+ validateNonEmptyString(result.field, `${operation} ${channel} field`);
130
+ const fieldType = inferFacadeFieldType(
131
+ program,
132
+ data,
133
+ result,
134
+ `${operation} ${channel}`
135
+ );
136
+ if (!["quantitative", "temporal", "nominal", "ordinal"].includes(fieldType)) {
137
+ throw new Error(`${operation} ${channel} has an unsupported field type.`);
138
+ }
139
+ return { ...omitUndefinedOptions(result), fieldType };
140
+ }
141
+
142
+ function normalizeStripJitter(value, channel, slot, operation) {
143
+ if (value === undefined || value === false) return undefined;
144
+ if (!isPlainObject(value)) {
145
+ throw new TypeError(`${operation} jitter must be false or a plain object.`);
146
+ }
147
+ validateOptionObject(value, JITTER_OPTIONS, `${operation} jitter`);
148
+ if (!isPlainObject(value.maxOffset)) {
149
+ throw new TypeError(`${operation} jitter.maxOffset must be a plain object.`);
150
+ }
151
+ const keys = Object.keys(value.maxOffset);
152
+ const expected = slot === "category" ? "band" : "pixels";
153
+ if (keys.length !== 1 || keys[0] !== expected) {
154
+ throw new Error(
155
+ `${operation} ${slot} slot jitter requires maxOffset.${expected}.`
156
+ );
157
+ }
158
+ return { ...value, channel };
159
+ }
160
+
161
+ function normalizeStripGuides(value, actualChannels, hasLegend, operation) {
162
+ const guides = normalizeGuides(value, operation);
163
+ if (guides === false) return false;
164
+ validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
165
+ const anchor = actualChannels.length === 1
166
+ ? (actualChannels[0] === "x" ? "y" : "x")
167
+ : undefined;
168
+ let axes = guides.axes;
169
+ if (axes !== undefined && axes !== false) {
170
+ if (!isPlainObject(axes)) {
171
+ throw new TypeError(`${operation} guides.axes must be false or a plain object.`);
172
+ }
173
+ if (anchor !== undefined && axes[anchor] !== undefined && axes[anchor] !== false) {
174
+ throw new Error(`${operation} cannot create an axis for its constant slot.`);
175
+ }
176
+ axes = anchor === undefined ? { ...axes } : { ...axes, [anchor]: false };
177
+ } else if (axes === undefined) {
178
+ axes = {
179
+ x: actualChannels.includes("x") ? {} : false,
180
+ y: actualChannels.includes("y") ? {} : false
181
+ };
182
+ }
183
+ let grid = guides.grid;
184
+ if (grid === undefined) grid = false;
185
+ else if (grid !== false) {
186
+ if (!isPlainObject(grid)) {
187
+ throw new TypeError(`${operation} guides.grid must be false or a plain object.`);
188
+ }
189
+ const anchorGrid = anchor === "x" ? "vertical" : anchor === "y" ? "horizontal" : undefined;
190
+ if (
191
+ anchorGrid !== undefined && grid[anchorGrid] !== undefined &&
192
+ grid[anchorGrid] !== false
193
+ ) {
194
+ throw new Error(`${operation} cannot create a grid for its constant slot.`);
195
+ }
196
+ grid = anchorGrid === undefined ? { ...grid } : { ...grid, [anchorGrid]: false };
197
+ }
198
+ if (!hasLegend && guides.legend !== undefined && guides.legend !== false) {
199
+ throw new Error(`${operation} legend requires color, size, or shape.`);
200
+ }
201
+ const legend = guides.legend === undefined ? (hasLegend ? {} : false) : guides.legend;
202
+ return { axes, grid, legend };
203
+ }
204
+
205
+ export const createStripPlot = action(
206
+ {
207
+ op: "createStripPlot",
208
+ description: "Create a one-dimensional Strip plot with an optional categorical slot and jitter."
209
+ },
210
+ function (args = {}) {
211
+ const operation = "createStripPlot";
212
+ validateFacadeOptions(args, STRIP_OPTIONS, operation);
213
+ if (args.x === undefined) throw new Error(`${operation} requires x.`);
214
+ const id = resolveFacadeId(this, args.id, {
215
+ defaultId: "stripPlot",
216
+ operation
217
+ });
218
+ const data = resolveFacadeData(this, args.data, operation);
219
+ const x = normalizeStripPosition(this, data, args.x, operation, "x");
220
+ const y = args.y === undefined
221
+ ? undefined
222
+ : normalizeStripPosition(this, data, args.y, operation, "y");
223
+ const isMeasure = encoding => ["quantitative", "temporal"].includes(encoding.fieldType);
224
+ if (!isMeasure(x) && y === undefined) {
225
+ throw new Error(`${operation} x must be a quantitative or temporal measure when y is omitted.`);
226
+ }
227
+ if (y !== undefined && isMeasure(x) === isMeasure(y)) {
228
+ throw new Error(`${operation} requires exactly one measure and one categorical slot.`);
229
+ }
230
+ const color = normalizeEncoding(args.color, `${operation} color`);
231
+ const size = normalizeEncoding(args.size, `${operation} size`);
232
+ const shape = normalizeEncoding(args.shape, `${operation} shape`);
233
+ const { radius, ...point } = normalizeAppearance(
234
+ args.point,
235
+ POINT_OPTIONS,
236
+ `${operation} point`
237
+ );
238
+ if (radius !== undefined) {
239
+ validateNonNegativeFinite(radius, `${operation} point radius`);
240
+ }
241
+ if (radius !== undefined && size !== undefined) {
242
+ throw new Error(`${operation} point radius conflicts with size.`);
243
+ }
244
+ const categoryChannel = y === undefined ? undefined : (isMeasure(x) ? "y" : "x");
245
+ const jitterChannel = categoryChannel ?? "y";
246
+ const jitter = normalizeStripJitter(
247
+ args.jitter,
248
+ jitterChannel,
249
+ categoryChannel === undefined ? "constant" : "category",
250
+ operation
251
+ );
252
+ const guides = normalizeStripGuides(
253
+ args.guides,
254
+ y === undefined ? ["x"] : ["x", "y"],
255
+ color !== undefined || size !== undefined || shape !== undefined,
256
+ operation
257
+ );
258
+
259
+ let next = this.createPointMark({ id, data, ...point });
260
+ if (args.coordinate !== undefined) {
261
+ next = next.createCoordinate({ id: args.coordinate, type: "cartesian", layers: [id] });
262
+ }
263
+ next = next.encodeX(positionArgs(x, { target: id }));
264
+ next = y === undefined
265
+ ? next.encodeY({
266
+ target: id,
267
+ datum: 0.5,
268
+ fieldType: "quantitative",
269
+ scale: { id: `${id}Anchor`, domain: [0, 1], zero: false, nice: false }
270
+ })
271
+ : next.encodeY(positionArgs(y, { target: id }));
272
+ if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
273
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
274
+ if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
275
+ if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
276
+ if (jitter !== undefined) next = next.jitterPoints({ target: id, ...jitter });
277
+ return applyFacadeGuides(next, guides, id, guides);
278
+ }
279
+ );
@@ -1,4 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
+ import { validateNonNegativeFinite } from "../../core/validation.js";
2
3
  import {
3
4
  applyFacadeGuides,
4
5
  normalizeAppearance,
@@ -17,7 +18,7 @@ const OPTIONS = Object.freeze([
17
18
  "point", "guides"
18
19
  ]);
19
20
  const POINT_OPTIONS = Object.freeze([
20
- "shape", "fill", "opacity", "stroke", "strokeWidth"
21
+ "shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
21
22
  ]);
22
23
 
23
24
  export const createScatterPlot = action(
@@ -32,7 +33,7 @@ export const createScatterPlot = action(
32
33
  operation: "createScatterPlot"
33
34
  });
34
35
  const data = resolveFacadeData(this, args.data, "createScatterPlot");
35
- const point = normalizeAppearance(
36
+ const { radius, ...point } = normalizeAppearance(
36
37
  args.point,
37
38
  POINT_OPTIONS,
38
39
  "createScatterPlot point"
@@ -43,14 +44,19 @@ export const createScatterPlot = action(
43
44
  const size = normalizeEncoding(args.size, "createScatterPlot size");
44
45
  const shape = normalizeEncoding(args.shape, "createScatterPlot shape");
45
46
  const guides = normalizeGuides(args.guides, "createScatterPlot");
47
+ if (Object.hasOwn(args.point ?? {}, "radius")) {
48
+ validateNonNegativeFinite(radius, "createScatterPlot point radius");
49
+ if (size !== undefined) throw new Error("createScatterPlot point radius conflicts with size.");
50
+ }
46
51
 
47
52
  let next = this
48
53
  .createPointMark({ id, data, ...point })
49
54
  .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
50
55
  .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
56
+ if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
51
57
  if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
52
58
  if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
53
59
  if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
54
- return applyFacadeGuides(next, guides);
60
+ return applyFacadeGuides(next, guides, id);
55
61
  }
56
62
  );
@@ -1,9 +1,10 @@
1
1
  import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
- import { validateOptionObject } from "../../core/validation.js";
4
- import { findDataset } from "../../selectors/datasets.js";
3
+ import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
4
+ import { findDataset, requireMaterializedDataset } from "../../selectors/datasets.js";
5
5
  import { isNominalValue } from "../../grammar/scales/fields.js";
6
6
  import { hasLayer } from "../../selectors/layers.js";
7
+ import { fulfillFacadeGuides } from "../guides/facade.js";
7
8
 
8
9
  export function validateFacadeOptions(args, supported, operation) {
9
10
  return validateOptionObject(args, supported, operation);
@@ -12,18 +13,17 @@ export function validateFacadeOptions(args, supported, operation) {
12
13
  export function resolveFacadeData(program, requested, operation) {
13
14
  if (requested !== undefined) {
14
15
  const id = validateUserId(requested, `${operation} dataset id`);
15
- if (findDataset(program, id) === undefined) {
16
- throw new Error(`Unknown dataset "${id}".`);
17
- }
18
- return id;
16
+ return requireMaterializedDataset(program, id).id;
19
17
  }
20
18
 
21
19
  const current = program.context.currentData;
22
20
  if (current !== undefined && findDataset(program, current) !== undefined) {
23
- return current;
21
+ return requireMaterializedDataset(program, current).id;
24
22
  }
25
23
  const datasets = program.semanticSpec.datasets;
26
- if (datasets.length === 1) return datasets[0].id;
24
+ if (datasets.length === 1) {
25
+ return requireMaterializedDataset(program, datasets[0].id).id;
26
+ }
27
27
  if (datasets.length > 1) {
28
28
  throw new Error(
29
29
  `${operation} requires data when multiple datasets are available.`
@@ -104,6 +104,39 @@ export function normalizeAppearance(value, supported, label) {
104
104
  return { ...value };
105
105
  }
106
106
 
107
+ export function omitUndefinedOptions(value) {
108
+ return Object.fromEntries(Object.entries(value).filter(([, option]) => option !== undefined));
109
+ }
110
+
111
+ export function normalizeCategoricalColor(value, label, supported = ["field", "fieldType", "scale", "palette"]) {
112
+ const color = normalizeFieldEncoding(value, label);
113
+ validateOptionObject(color, supported, label);
114
+ validateNonEmptyString(color.field, `${label} field`);
115
+ if (color.fieldType !== undefined && !["nominal", "ordinal"].includes(color.fieldType)) {
116
+ throw new TypeError(`${label} must be nominal or ordinal.`);
117
+ }
118
+ if (color.scale !== undefined) validateOptionObject(color.scale, undefined, `${label}.scale`);
119
+ return { ...omitUndefinedOptions(color),
120
+ ...(color.scale === undefined ? {} : { scale: omitUndefinedOptions(color.scale) }) };
121
+ }
122
+
123
+ export function normalizeCategoricalGuides(value, operation, color) {
124
+ const guides = normalizeGuides(value, operation);
125
+ if (guides === false) return false;
126
+ validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
127
+ const legend = guides.legend;
128
+ if (legend !== undefined && legend !== false) {
129
+ validateOptionObject(legend, undefined, `${operation} guides.legend`);
130
+ if (color === undefined) throw new Error(`${operation} legend requires color.`);
131
+ if (Object.hasOwn(legend, "gradient") || Object.hasOwn(legend, "count") ||
132
+ legend.channels !== undefined && (!Array.isArray(legend.channels) ||
133
+ legend.channels.length !== 1 || legend.channels[0] !== "color")) {
134
+ throw new Error(`${operation} only supports a categorical color legend.`);
135
+ }
136
+ }
137
+ return guides;
138
+ }
139
+
107
140
  export function normalizeTargetOptions(value, label) {
108
141
  if (value === undefined) return undefined;
109
142
  if (!isPlainObject(value)) {
@@ -136,6 +169,32 @@ export function targetArgs(encoding, target) {
136
169
  return { ...encoding, target };
137
170
  }
138
171
 
139
- export function applyFacadeGuides(program, guides) {
140
- return guides === false ? program : program.createGuides(guides);
172
+ export function applyFacadeGuides(program, guides, target, explicitGuides = guides) {
173
+ return fulfillFacadeGuides(program, guides, target, explicitGuides);
174
+ }
175
+
176
+ export function normalizeArcCategory(value, operation) {
177
+ const label = `${operation} category`;
178
+ const category = normalizeFieldEncoding(value, label);
179
+ validateOptionObject(category, ["field", "fieldType", "scale"], label);
180
+ validateNonEmptyString(category.field, `${label} field`);
181
+ const fieldType = category.fieldType === undefined ? "nominal" : category.fieldType;
182
+ if (!["nominal", "ordinal"].includes(fieldType)) throw new TypeError(`${label} must be nominal or ordinal.`);
183
+ if (category.scale !== undefined) {
184
+ validateOptionObject(category.scale, ["id", "type", "domain", "range", "reverse"], `${label}.scale`);
185
+ if (category.scale.type !== undefined && category.scale.type !== "band") {
186
+ throw new TypeError(`${label}.scale.type must be band.`);
187
+ }
188
+ }
189
+ return { ...omitUndefinedOptions(category), fieldType,
190
+ ...(category.scale === undefined ? {} : { scale: omitUndefinedOptions(category.scale) }) };
191
+ }
192
+
193
+ export function normalizeCategoryAggregate(args, operation) {
194
+ const aggregate = args.aggregate === undefined ? "count" : args.aggregate;
195
+ if (!["count", "sum"].includes(aggregate) || (aggregate === "sum") !== (args.value !== undefined)) {
196
+ throw new Error(`${operation} requires count without value or explicit sum with value.`);
197
+ }
198
+ if (args.value !== undefined) validateNonEmptyString(args.value, `${operation} value`);
199
+ return aggregate;
141
200
  }
@@ -20,6 +20,9 @@ const LAYOUT_EDIT_OPTIONS = Object.freeze([
20
20
  "columns", "gap", "align", "padding"
21
21
  ]);
22
22
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
23
+ const INSERTION_OPTIONS = Object.freeze(["id", "program", "before", "after"]);
24
+ const REMOVAL_OPTIONS = Object.freeze(["target"]);
25
+ const REORDER_OPTIONS = Object.freeze(["order"]);
23
26
 
24
27
  function option(args, name, fallback) {
25
28
  return Object.hasOwn(args, name) ? args[name] : fallback;
@@ -180,10 +183,17 @@ const editCompositionLayout = action(
180
183
  : current.padding;
181
184
  let layout;
182
185
  if (current.type === "facet") {
186
+ const gridCells = new Map(
187
+ (current.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
188
+ );
183
189
  layout = resolveFacetLayout({
184
190
  children: current.children.map((id, index) => ({
185
191
  ...childDescriptor({ id, program: this.children[id] }),
186
- value: current.facet.values[index]
192
+ value: current.facet.values[index],
193
+ ...(gridCells.has(id) ? {
194
+ row: gridCells.get(id).row,
195
+ column: gridCells.get(id).column
196
+ } : {})
187
197
  })),
188
198
  columns: option(args, "columns", current.columns),
189
199
  gap: option(args, "gap", current.gap),
@@ -224,6 +234,7 @@ const replaceCompositionChild = action(
224
234
  },
225
235
  function (args = {}) {
226
236
  validateOptionObject(args, REPLACEMENT_OPTIONS, "replaceCompositionChild");
237
+ this._assertCompositionProgram("replaceCompositionChild");
227
238
  if (this.compositionSpec.type === "facet") {
228
239
  throw new Error("replaceCompositionChild is not available on a facet composition.");
229
240
  }
@@ -246,9 +257,115 @@ const replaceCompositionChild = action(
246
257
  }
247
258
  );
248
259
 
260
+ function requireConcat(program, operation) {
261
+ program._assertCompositionProgram(operation);
262
+ if (program.compositionSpec.type === "facet") {
263
+ throw new Error(`${operation} is not available on a facet composition.`);
264
+ }
265
+ }
266
+
267
+ const insertCompositionChild = action(
268
+ {
269
+ op: "insertCompositionChild",
270
+ description: "Insert one named child into a concat composition.",
271
+ scope: "composition"
272
+ },
273
+ function (args = {}) {
274
+ validateOptionObject(args, INSERTION_OPTIONS, "insertCompositionChild");
275
+ requireConcat(this, "insertCompositionChild");
276
+ const id = validateUserId(args.id, "Composition child ID");
277
+ if (Object.hasOwn(this.children, id)) {
278
+ throw new Error(`Composition child "${id}" already exists.`);
279
+ }
280
+ if (!(args.program instanceof CoreChartProgram)) {
281
+ throw new TypeError("insertCompositionChild program must be a ChartProgram.");
282
+ }
283
+ if (args.before !== undefined && args.after !== undefined) {
284
+ throw new Error("insertCompositionChild before and after are mutually exclusive.");
285
+ }
286
+ childDescriptor({ id, program: args.program });
287
+ const order = [...this.compositionSpec.children];
288
+ if (args.before !== undefined || args.after !== undefined) {
289
+ const property = args.before !== undefined ? "before" : "after";
290
+ const anchor = validateUserId(args[property], `Composition child ${property}`);
291
+ const index = order.indexOf(anchor);
292
+ if (index < 0) throw new Error(`Unknown composition child "${anchor}".`);
293
+ order.splice(index + (property === "after" ? 1 : 0), 0, id);
294
+ } else {
295
+ order.push(id);
296
+ }
297
+ return applyCompositionState(this, {
298
+ children: freezeOwned({ ...this.children, [id]: args.program }),
299
+ compositionSpec: { ...this.compositionSpec, children: order }
300
+ }, [id]);
301
+ }
302
+ );
303
+
304
+ const removeCompositionChild = action(
305
+ {
306
+ op: "removeCompositionChild",
307
+ description: "Remove one named child from a concat composition.",
308
+ scope: "composition"
309
+ },
310
+ function (args = {}) {
311
+ validateOptionObject(args, REMOVAL_OPTIONS, "removeCompositionChild");
312
+ requireConcat(this, "removeCompositionChild");
313
+ const target = validateUserId(args.target, "Composition child target");
314
+ if (!Object.hasOwn(this.children, target)) {
315
+ throw new Error(`Unknown composition child "${target}".`);
316
+ }
317
+ if (this.compositionSpec.children.length === 1) {
318
+ throw new Error("removeCompositionChild must leave at least one child.");
319
+ }
320
+ const { [target]: _removed, ...remaining } = this.children;
321
+ void _removed;
322
+ return applyCompositionState(this, {
323
+ children: freezeOwned(remaining),
324
+ compositionSpec: {
325
+ ...this.compositionSpec,
326
+ children: this.compositionSpec.children.filter(id => id !== target)
327
+ }
328
+ });
329
+ }
330
+ );
331
+
332
+ const reorderCompositionChildren = action(
333
+ {
334
+ op: "reorderCompositionChildren",
335
+ description: "Reorder every named child in a concat composition.",
336
+ scope: "composition"
337
+ },
338
+ function (args = {}) {
339
+ validateOptionObject(args, REORDER_OPTIONS, "reorderCompositionChildren");
340
+ requireConcat(this, "reorderCompositionChildren");
341
+ if (!Array.isArray(args.order) || args.order.length === 0 ||
342
+ args.order.some(id => typeof id !== "string" || id.length === 0)) {
343
+ throw new TypeError("reorderCompositionChildren order must be a non-empty child ID array.");
344
+ }
345
+ if (new Set(args.order).size !== args.order.length) {
346
+ throw new Error("reorderCompositionChildren order must not contain duplicates.");
347
+ }
348
+ const current = this.compositionSpec.children;
349
+ if (args.order.length !== current.length ||
350
+ args.order.some(id => !Object.hasOwn(this.children, id))) {
351
+ throw new Error("reorderCompositionChildren order must contain every current child exactly once.");
352
+ }
353
+ if (args.order.every((id, index) => id === current[index])) {
354
+ throw new Error("reorderCompositionChildren requires an actual order change.");
355
+ }
356
+ return applyCompositionState(this, {
357
+ children: this.children,
358
+ compositionSpec: { ...this.compositionSpec, children: [...args.order] }
359
+ });
360
+ }
361
+ );
362
+
249
363
  export function registerCompositionActions(ProgramClass) {
250
364
  ProgramClass.prototype.useProgram = useProgram;
251
365
  ProgramClass.prototype.materializeComposition = materializeComposition;
252
366
  ProgramClass.prototype.editCompositionLayout = editCompositionLayout;
253
367
  ProgramClass.prototype.replaceCompositionChild = replaceCompositionChild;
368
+ ProgramClass.prototype.insertCompositionChild = insertCompositionChild;
369
+ ProgramClass.prototype.removeCompositionChild = removeCompositionChild;
370
+ ProgramClass.prototype.reorderCompositionChildren = reorderCompositionChildren;
254
371
  }
@@ -0,0 +1,29 @@
1
+ import {
2
+ deriveBinRows,
3
+ normalizeBinTransform,
4
+ resolveBinTransform
5
+ } from "../../grammar/bin.js";
6
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
7
+
8
+ const OPTIONS = Object.freeze([
9
+ "id", "source", "field", "maxBins", "step", "boundaries", "extent",
10
+ "nice", "zero", "includeEmpty", "members", "as"
11
+ ]);
12
+
13
+ export const materializeBinData = derivedMaterializer(
14
+ "materializeBinData",
15
+ "Materialize one immutable one-dimensional bin dataset.",
16
+ "bin",
17
+ deriveBinRows,
18
+ resolveBinTransform
19
+ );
20
+
21
+ export const createBinData = derivedCreator(
22
+ "createBinData",
23
+ "Create reusable one-dimensional bin bounds, counts, and members.",
24
+ OPTIONS,
25
+ "Bin dataset id",
26
+ "Bin source dataset id",
27
+ normalizeBinTransform,
28
+ "materializeBinData"
29
+ );
@@ -0,0 +1,24 @@
1
+ import {
2
+ deriveComputedRows,
3
+ normalizeComputedTransform
4
+ } from "../../grammar/computed.js";
5
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze(["id", "source", "as", "expression"]);
8
+
9
+ export const materializeComputedData = derivedMaterializer(
10
+ "materializeComputedData",
11
+ "Materialize one immutable row-level arithmetic dataset.",
12
+ "computed",
13
+ deriveComputedRows
14
+ );
15
+
16
+ export const createComputedData = derivedCreator(
17
+ "createComputedData",
18
+ "Create a finite computed field from a closed arithmetic expression.",
19
+ OPTIONS,
20
+ "Computed dataset id",
21
+ "Computed source dataset id",
22
+ normalizeComputedTransform,
23
+ "materializeComputedData"
24
+ );