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
@@ -0,0 +1,42 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ applyFacadeGuides, normalizeAppearance, normalizeArcCategory, normalizeCategoryAggregate,
4
+ normalizeCategoricalColor, normalizeCategoricalGuides, omitUndefinedOptions,
5
+ resolveFacadeData, resolveFacadeId, validateFacadeOptions
6
+ } from "./shared.js";
7
+
8
+ const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "radiusScale", "color", "arc", "guides"];
9
+ const ARC_OPTIONS = ["innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth"];
10
+ const RADIUS_OPTIONS = ["id", "type", "domain", "range", "zero", "nice", "reverse", "clamp"];
11
+
12
+ function createMeasuredPlot(program, args, { operation, defaultId, mapping }) {
13
+ validateFacadeOptions(args, OPTIONS, operation);
14
+ const id = resolveFacadeId(program, args.id, { defaultId, operation });
15
+ const data = resolveFacadeData(program, args.data, operation);
16
+ const category = normalizeArcCategory(args.category, operation);
17
+ const aggregate = normalizeCategoryAggregate(args, operation);
18
+ const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${operation} arc`));
19
+ if (arc.padAngle !== undefined && arc.padAngle !== 0) throw new Error(`${operation} requires padAngle 0.`);
20
+ const radiusScale = args.radiusScale === undefined ? undefined
21
+ : omitUndefinedOptions(normalizeAppearance(args.radiusScale, RADIUS_OPTIONS, `${operation} radiusScale`));
22
+ const color = args.color === false ? undefined
23
+ : normalizeCategoricalColor(args.color === undefined ? category.field : args.color, `${operation} color`);
24
+ if (color !== undefined && arc.fill !== undefined) {
25
+ throw new Error(`${operation} arc.fill cannot be combined with color; use color:false.`);
26
+ }
27
+ const guides = normalizeCategoricalGuides(args.guides, operation, color);
28
+ let next = program.createArcMark({ id, data, ...arc }).encodeTheta({
29
+ ...category, target: id, ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
30
+ }).encodeR({ target: id, aggregate, mapping,
31
+ ...(aggregate === "sum" ? { field: args.value } : {}),
32
+ ...(radiusScale === undefined ? {} : { scale: radiusScale }) });
33
+ if (color !== undefined) next = next.encodeColor({ ...color, target: id });
34
+ return applyFacadeGuides(next, guides, id, args.guides ?? {});
35
+ }
36
+
37
+ export const createRosePlot = action({ op: "createRosePlot", description: "Create equal-angle sectors with area proportional to category count or sum." }, function (args = {}) {
38
+ return createMeasuredPlot(this, args, { operation: "createRosePlot", defaultId: "rosePlot", mapping: "area" });
39
+ });
40
+ export const createRadialBarPlot = action({ op: "createRadialBarPlot", description: "Create equal-angle sectors with radial length proportional to category count or sum." }, function (args = {}) {
41
+ return createMeasuredPlot(this, args, { operation: "createRadialBarPlot", defaultId: "radialBarPlot", mapping: "radius-length" });
42
+ });
@@ -0,0 +1,422 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateOptionObject } from "../../core/validation.js";
5
+ import { findDataset } from "../../selectors/datasets.js";
6
+ import { findLayer } from "../../selectors/layers.js";
7
+ import { findSemanticScale } from "../../selectors/scales.js";
8
+ import { resolveViolinRoles } from "../violinPlots/create.js";
9
+ import {
10
+ applyFacadeGuides,
11
+ normalizeEncoding,
12
+ normalizeGuides,
13
+ resolveFacadeData,
14
+ resolveFacadeId,
15
+ validateFacadeOptions
16
+ } from "./shared.js";
17
+
18
+ const CREATE_OPTIONS = Object.freeze([
19
+ "id", "data", "coordinate", "category", "value", "orientation", "side",
20
+ "density", "summary", "points", "color", "guides"
21
+ ]);
22
+ const EDIT_OPTIONS = Object.freeze([
23
+ "target", "data", "category", "value", "orientation", "side", "density",
24
+ "summary", "points", "color"
25
+ ]);
26
+ const DENSITY_OPTIONS = Object.freeze([
27
+ "bandwidth", "extent", "steps", "kernel", "normalization", "width", "area"
28
+ ]);
29
+ const BOX_OPTIONS = Object.freeze([
30
+ "type", "whisker", "width", "outliers", "box", "median", "outlier"
31
+ ]);
32
+ const INTERVAL_OPTIONS = Object.freeze([
33
+ "type", "center", "extent", "method", "level", "point", "errorBar"
34
+ ]);
35
+ const POINTS_OPTIONS = Object.freeze([
36
+ "type", "size", "shape", "point", "jitter", "packing"
37
+ ]);
38
+ const SLOT_OFFSET_BAND = 0.22;
39
+ const POINT_SPREAD_BAND = 0.12;
40
+
41
+ function normalizeOrientation(value) {
42
+ const orientation = value ?? "vertical";
43
+ if (!["vertical", "horizontal"].includes(orientation)) {
44
+ throw new Error(`Unsupported Raincloud orientation "${orientation}".`);
45
+ }
46
+ return orientation;
47
+ }
48
+
49
+ function normalizeSide(value) {
50
+ const side = value ?? "before";
51
+ if (!["before", "after"].includes(side)) {
52
+ throw new Error(`Unsupported Raincloud side "${side}".`);
53
+ }
54
+ return side;
55
+ }
56
+
57
+ function normalizeDensity(value, current) {
58
+ if (value === false) return false;
59
+ const patch = value ?? {};
60
+ validateOptionObject(patch, DENSITY_OPTIONS, "Raincloud density");
61
+ return { ...(current === false ? {} : current ?? {}), ...patch };
62
+ }
63
+
64
+ function normalizeSummary(value, current) {
65
+ if (value === false) return false;
66
+ const patch = value ?? {};
67
+ if (!isPlainObject(patch)) {
68
+ throw new TypeError("Raincloud summary must be false or a plain object.");
69
+ }
70
+ const type = patch.type ?? (current === false ? undefined : current?.type) ?? "box";
71
+ const keys = type === "box" ? BOX_OPTIONS : type === "interval" ? INTERVAL_OPTIONS : undefined;
72
+ if (keys === undefined) throw new Error(`Unsupported Raincloud summary type "${type}".`);
73
+ validateOptionObject(patch, keys, "Raincloud summary");
74
+ const base = current !== false && current?.type === type ? current : {};
75
+ return { ...base, ...patch, type };
76
+ }
77
+
78
+ function normalizePoints(value, current) {
79
+ if (value === false) return false;
80
+ const patch = value ?? {};
81
+ if (!isPlainObject(patch)) {
82
+ throw new TypeError("Raincloud points must be false or a plain object.");
83
+ }
84
+ const type = patch.type ?? (current === false ? undefined : current?.type) ?? "beeswarm";
85
+ if (!["strip", "beeswarm"].includes(type)) {
86
+ throw new Error(`Unsupported Raincloud points type "${type}".`);
87
+ }
88
+ validateOptionObject(patch, POINTS_OPTIONS, "Raincloud points");
89
+ if (type === "strip" && Object.hasOwn(patch, "packing")) {
90
+ throw new Error("Raincloud strip points do not accept packing.");
91
+ }
92
+ if (type === "beeswarm" && Object.hasOwn(patch, "jitter")) {
93
+ throw new Error("Raincloud beeswarm points do not accept jitter.");
94
+ }
95
+ const base = current !== false && current?.type === type ? current : {};
96
+ return { ...base, ...patch, type };
97
+ }
98
+
99
+ function roleScale(encoding, fallbackId, label, defaults = {}) {
100
+ if (encoding.scale !== undefined && !isPlainObject(encoding.scale)) {
101
+ throw new TypeError(`${label}.scale must be a plain object.`);
102
+ }
103
+ if (defaults.type === "band" && encoding.scale?.type !== undefined && encoding.scale.type !== "band") {
104
+ throw new TypeError(`${label}.scale.type must be band.`);
105
+ }
106
+ return {
107
+ ...encoding,
108
+ scale: { ...defaults, ...(encoding.scale ?? {}), id: encoding.scale?.id ?? fallbackId }
109
+ };
110
+ }
111
+
112
+ function normalizeColor(value, category, id) {
113
+ if (value === undefined || value === false) return undefined;
114
+ const color = normalizeEncoding(value, "Raincloud color");
115
+ if (color.field !== category.field) {
116
+ throw new Error("Raincloud color must encode its category field.");
117
+ }
118
+ if (color.scale !== undefined && !isPlainObject(color.scale)) {
119
+ throw new TypeError("Raincloud color.scale must be a plain object.");
120
+ }
121
+ return {
122
+ ...color,
123
+ fieldType: color.fieldType ?? "nominal",
124
+ scale: { ...(color.scale ?? {}), id: color.scale?.id ?? `${id}Color` }
125
+ };
126
+ }
127
+
128
+ function rolePositions(category, value, orientation) {
129
+ return orientation === "vertical"
130
+ ? { x: category, y: value, categoryChannel: "x" }
131
+ : { x: value, y: category, categoryChannel: "y" };
132
+ }
133
+
134
+ function densitySide(orientation, side) {
135
+ if (orientation === "vertical") return side === "before" ? "left" : "right";
136
+ return side === "before" ? "top" : "bottom";
137
+ }
138
+
139
+ function markRematerializer(layer) {
140
+ return {
141
+ point: "rematerializePointMark",
142
+ bar: "rematerializeBarMark",
143
+ rule: "rematerializeRuleMark"
144
+ }[layer?.mark?.type];
145
+ }
146
+
147
+ function applySlotOffset(program, ids, categoryChannel, side, follow = []) {
148
+ const band = (side === "before" ? 1 : -1) * SLOT_OFFSET_BAND;
149
+ let next = program;
150
+ for (const id of ids) {
151
+ const layer = findLayer(next, id);
152
+ if (layer === undefined) continue;
153
+ next = next._withMarkConfig(id, {
154
+ ...next.markConfigs[id],
155
+ categorySlotOffset: { channel: categoryChannel, band }
156
+ });
157
+ }
158
+ for (const id of [...ids, ...follow]) {
159
+ const layer = findLayer(next, id);
160
+ const operation = markRematerializer(layer);
161
+ if (operation !== undefined) next = next[operation]({ id });
162
+ }
163
+ return next;
164
+ }
165
+
166
+ function boxOffsetTargets(program, id) {
167
+ const box = program.markConfigs[id]?.boxPlot;
168
+ const errorBar = program.markConfigs[box?.whiskerId]?.errorBar;
169
+ return {
170
+ offset: [id, box?.whiskerId, errorBar?.lowerCapId, errorBar?.upperCapId, box?.outlierId]
171
+ .filter(candidate => candidate !== undefined && findLayer(program, candidate) !== undefined),
172
+ follow: [box?.medianId].filter(candidate => findLayer(program, candidate) !== undefined)
173
+ };
174
+ }
175
+
176
+ function intervalOffsetTargets(program, id) {
177
+ const intervalId = `${id}Interval`;
178
+ const errorBar = program.markConfigs[intervalId]?.errorBar;
179
+ return [id, intervalId, errorBar?.lowerCapId, errorBar?.upperCapId]
180
+ .filter(candidate => candidate !== undefined && findLayer(program, candidate) !== undefined);
181
+ }
182
+
183
+ function addSummary(program, config, positions) {
184
+ if (config.summary === false) return program;
185
+ const id = config.childIds.summary;
186
+ const { type, ...options } = config.summary;
187
+ let next;
188
+ if (type === "box") {
189
+ next = program.createBoxPlot({
190
+ id,
191
+ data: config.source,
192
+ coordinate: config.coordinate,
193
+ x: positions.x,
194
+ y: positions.y,
195
+ width: options.width ?? { band: 0.24 },
196
+ ...options,
197
+ guides: false
198
+ });
199
+ if (config.color !== undefined) next = next.encodeColor({ target: id, ...config.color });
200
+ const targets = boxOffsetTargets(next, id);
201
+ return applySlotOffset(next, targets.offset, positions.categoryChannel, config.side, targets.follow);
202
+ }
203
+ const { center, extent, method, level, point, errorBar } = options;
204
+ const interval = {
205
+ field: config.value.field,
206
+ scale: config.value.scale,
207
+ ...(center === undefined ? {} : { center }),
208
+ ...(extent === undefined ? {} : { extent }),
209
+ ...(method === undefined ? {} : { method }),
210
+ ...(level === undefined ? {} : { level })
211
+ };
212
+ next = program.createIntervalPlot({
213
+ id,
214
+ data: config.source,
215
+ coordinate: config.coordinate,
216
+ x: config.orientation === "vertical" ? config.category : interval,
217
+ y: config.orientation === "vertical" ? interval : config.category,
218
+ ...(point === undefined ? {} : { point }),
219
+ ...(errorBar === undefined ? {} : { errorBar }),
220
+ guides: false
221
+ });
222
+ if (config.color !== undefined) next = next.encodeColor({ target: id, ...config.color });
223
+ return applySlotOffset(
224
+ next,
225
+ intervalOffsetTargets(next, id),
226
+ positions.categoryChannel,
227
+ config.side
228
+ );
229
+ }
230
+
231
+ function addPoints(program, config, positions) {
232
+ if (config.points === false) return program;
233
+ const id = config.childIds.points;
234
+ const { type, size, shape, point } = config.points;
235
+ const common = {
236
+ id,
237
+ data: config.source,
238
+ coordinate: config.coordinate,
239
+ x: positions.x,
240
+ y: positions.y,
241
+ ...(config.color === undefined ? {} : { color: config.color }),
242
+ ...(size === undefined ? {} : { size }),
243
+ ...(shape === undefined ? {} : { shape }),
244
+ ...(point === undefined ? {} : { point }),
245
+ guides: false
246
+ };
247
+ let next;
248
+ if (type === "strip") {
249
+ const jitter = Object.hasOwn(config.points, "jitter")
250
+ ? config.points.jitter
251
+ : { maxOffset: { band: POINT_SPREAD_BAND } };
252
+ next = program.createStripPlot({ ...common, jitter });
253
+ } else {
254
+ const requested = config.points.packing;
255
+ const packing = requested === false
256
+ ? false
257
+ : {
258
+ maxOffset: { band: POINT_SPREAD_BAND },
259
+ ...(requested ?? {})
260
+ };
261
+ next = program.createBeeswarmPlot({ ...common, packing });
262
+ }
263
+ return applySlotOffset(next, [id], positions.categoryChannel, config.side);
264
+ }
265
+
266
+ function buildRaincloud(program, config) {
267
+ const positions = rolePositions(config.category, config.value, config.orientation);
268
+ let next = program;
269
+ if (config.density !== false) {
270
+ const { area, ...density } = config.density;
271
+ next = next.createViolinPlot({
272
+ id: config.childIds.cloud,
273
+ data: config.source,
274
+ coordinate: config.coordinate,
275
+ x: positions.x,
276
+ y: positions.y,
277
+ ...(config.color === undefined ? {} : { color: config.color }),
278
+ density: { ...density, side: densitySide(config.orientation, config.side) },
279
+ ...(area === undefined ? {} : { area }),
280
+ guides: false
281
+ });
282
+ }
283
+ next = addSummary(next, config, positions);
284
+ next = addPoints(next, config, positions);
285
+ const ownerId = [config.childIds.cloud, config.childIds.summary, config.childIds.points]
286
+ .find(id => findLayer(next, id) !== undefined);
287
+ if (ownerId === undefined) throw new Error("Raincloud requires at least one enabled component.");
288
+ const guideTarget = [config.childIds.points, config.childIds.cloud, config.childIds.summary]
289
+ .find(id => findLayer(next, id) !== undefined);
290
+ next = applyFacadeGuides(next, config.guides, guideTarget, config.guides);
291
+ const ownedChildIds = [config.childIds.cloud, config.childIds.summary, config.childIds.points]
292
+ .filter(id => findLayer(next, id) !== undefined);
293
+ return next._withMarkConfig(ownerId, {
294
+ ...next.markConfigs[ownerId],
295
+ raincloudPlot: { ...config, ownerId, ownedChildIds, materialized: true }
296
+ })._withContext({ currentMark: ownerId, currentData: config.source });
297
+ }
298
+
299
+ function alignExistingRoleScales(program, config) {
300
+ let next = program;
301
+ for (const encoding of [config.category, config.value, config.color]) {
302
+ const scale = encoding?.scale;
303
+ if (!isPlainObject(scale) || findSemanticScale(next, scale.id) === undefined) continue;
304
+ const patch = Object.fromEntries(Object.entries(scale).filter(
305
+ ([property, value]) => property !== "id" && value !== undefined
306
+ ));
307
+ if (Object.keys(patch).length > 0) next = next.editScale({ id: scale.id, ...patch });
308
+ }
309
+ return next;
310
+ }
311
+
312
+ function normalizeCreate(program, args, operation) {
313
+ const id = resolveFacadeId(program, args.id, { defaultId: "raincloudPlot", operation });
314
+ if (Object.values(program.markConfigs).some(value => value.raincloudPlot?.id === id)) {
315
+ throw new Error(`Raincloud plot "${id}" already exists.`);
316
+ }
317
+ const source = resolveFacadeData(program, args.data, operation);
318
+ const dataset = findDataset(program, source);
319
+ const orientation = normalizeOrientation(args.orientation);
320
+ if (args.category === undefined || args.value === undefined) {
321
+ throw new Error(`${operation} requires category and value.`);
322
+ }
323
+ const raw = orientation === "vertical"
324
+ ? resolveViolinRoles(dataset, args.category, args.value, operation)
325
+ : resolveViolinRoles(dataset, args.value, args.category, operation);
326
+ if (raw.orientation !== orientation) {
327
+ throw new Error(`${operation} category and value do not match orientation ${orientation}.`);
328
+ }
329
+ const category = roleScale(
330
+ raw.category,
331
+ `${id}Category`,
332
+ `${operation} category`,
333
+ { type: "band" }
334
+ );
335
+ const value = roleScale(raw.value, `${id}Value`, `${operation} value`);
336
+ const density = normalizeDensity(args.density);
337
+ const summary = normalizeSummary(args.summary);
338
+ const points = normalizePoints(args.points);
339
+ if ([density, summary, points].every(component => component === false)) {
340
+ throw new Error("Raincloud requires at least one enabled component.");
341
+ }
342
+ return {
343
+ id,
344
+ source,
345
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
346
+ category,
347
+ value,
348
+ orientation,
349
+ side: normalizeSide(args.side),
350
+ density,
351
+ summary,
352
+ points,
353
+ color: normalizeColor(args.color, category, id),
354
+ guides: normalizeGuides(args.guides, operation),
355
+ childIds: {
356
+ cloud: `${id}Cloud`,
357
+ summary: `${id}Summary`,
358
+ points: `${id}Points`
359
+ }
360
+ };
361
+ }
362
+
363
+ function raincloudOwner(program, requested) {
364
+ const owners = Object.entries(program.markConfigs)
365
+ .filter(([, value]) => value.raincloudPlot?.materialized === true)
366
+ .map(([id]) => id);
367
+ if (requested !== undefined) {
368
+ const target = validateUserId(requested, "Raincloud target");
369
+ const owner = owners.find(id => id === target || program.markConfigs[id].raincloudPlot.id === target);
370
+ if (owner === undefined) throw new Error(`Unknown Raincloud plot "${target}".`);
371
+ return owner;
372
+ }
373
+ if (owners.includes(program.context.currentMark)) return program.context.currentMark;
374
+ if (owners.length === 1) return owners[0];
375
+ if (owners.length === 0) throw new Error("editRaincloudPlot requires a Raincloud plot.");
376
+ throw new Error("editRaincloudPlot target is ambiguous; provide target.");
377
+ }
378
+
379
+ export const createRaincloudPlot = action(
380
+ { op: "createRaincloudPlot", description: "Create a half-density, summary, and raw-point distribution plot." },
381
+ function (args = {}) {
382
+ const operation = "createRaincloudPlot";
383
+ validateFacadeOptions(args, CREATE_OPTIONS, operation);
384
+ return buildRaincloud(this, normalizeCreate(this, args, operation));
385
+ }
386
+ );
387
+
388
+ export const editRaincloudPlot = action(
389
+ { op: "editRaincloudPlot", description: "Revise one Raincloud plot's shared source and statistical roles." },
390
+ function (args = {}) {
391
+ const operation = "editRaincloudPlot";
392
+ validateFacadeOptions(args, EDIT_OPTIONS, operation);
393
+ if (!EDIT_OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
394
+ throw new Error(`${operation} requires at least one Raincloud option.`);
395
+ }
396
+ const ownerId = raincloudOwner(this, args.target);
397
+ const current = this.markConfigs[ownerId].raincloudPlot;
398
+ const candidateArgs = {
399
+ id: current.id,
400
+ data: Object.hasOwn(args, "data") ? args.data : current.source,
401
+ ...(current.coordinate === undefined ? {} : { coordinate: current.coordinate }),
402
+ category: Object.hasOwn(args, "category") ? args.category : current.category,
403
+ value: Object.hasOwn(args, "value") ? args.value : current.value,
404
+ orientation: args.orientation ?? current.orientation,
405
+ side: args.side ?? current.side,
406
+ density: Object.hasOwn(args, "density")
407
+ ? normalizeDensity(args.density, current.density)
408
+ : current.density,
409
+ summary: Object.hasOwn(args, "summary")
410
+ ? normalizeSummary(args.summary, current.summary)
411
+ : current.summary,
412
+ points: Object.hasOwn(args, "points")
413
+ ? normalizePoints(args.points, current.points)
414
+ : current.points,
415
+ color: Object.hasOwn(args, "color") ? args.color : current.color,
416
+ guides: current.guides
417
+ };
418
+ const withoutCurrent = this.removeMark({ target: ownerId });
419
+ const revised = normalizeCreate(withoutCurrent, candidateArgs, operation);
420
+ return buildRaincloud(alignExistingRoleScales(withoutCurrent, revised), revised);
421
+ }
422
+ );