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
@@ -1,3 +1,4 @@
1
+ import { withGuideLayoutValidation } from "../../materialization/guides/layout.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { noOptions } from "../../core/validation.js";
3
4
  import {
@@ -92,7 +93,7 @@ function titleTextAction(kind, create) {
92
93
  return action({
93
94
  op,
94
95
  description: `${create ? "Create" : "Rematerialize"} chart ${kind} text.`
95
- }, function (args = {}) {
96
+ }, withGuideLayoutValidation(function (args = {}) {
96
97
  noOptions(args, op);
97
98
  const config = requireTitleConfig(this);
98
99
  if (create && this.graphicSpec.objects[id] !== undefined) {
@@ -111,7 +112,7 @@ function titleTextAction(kind, create) {
111
112
  ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
112
113
  ...resolveCanvasGraphicPlacement(this)
113
114
  })[`edit${suffix}`]();
114
- });
115
+ }));
115
116
  }
116
117
 
117
118
  export const editTitleText = titleTextAction("title", false);
@@ -121,7 +122,7 @@ export const createSubtitleText = titleTextAction("subtitle", true);
121
122
 
122
123
  export const rematerializeTitle = action(
123
124
  { op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
124
- function (args = {}) {
125
+ withGuideLayoutValidation(function (args = {}) {
125
126
  noOptions(args, "rematerializeTitle");
126
127
  requireTitleConfig(this);
127
128
  if (this.graphicSpec.objects.chartTitle?.type !== "text") {
@@ -138,7 +139,7 @@ export const rematerializeTitle = action(
138
139
  next = next.editGraphics({ target: "chartSubtitle", remove: true });
139
140
  }
140
141
  return next;
141
- }
142
+ })
142
143
  );
143
144
 
144
145
  export const createTitle = action(
@@ -147,7 +148,7 @@ export const createTitle = action(
147
148
  description: "Create a chart title and optional subtitle.",
148
149
  scope: "any"
149
150
  },
150
- function (args = {}) {
151
+ withGuideLayoutValidation(function (args = {}) {
151
152
  assertTitleScope(this, "createTitle");
152
153
  const options = normalizeTitleOptions(args);
153
154
  if (Object.keys(this.semanticSpec.title).length > 0) {
@@ -172,7 +173,7 @@ export const createTitle = action(
172
173
  next = next.createTitleText();
173
174
  if (subtitle !== undefined) next = next.createSubtitleText();
174
175
  return next;
175
- }
176
+ })
176
177
  );
177
178
 
178
179
  export const editTitle = action(
@@ -181,7 +182,7 @@ export const editTitle = action(
181
182
  description: "Edit one stable chart title resource.",
182
183
  scope: "any"
183
184
  },
184
- function (args = {}) {
185
+ withGuideLayoutValidation(function (args = {}) {
185
186
  assertTitleScope(this, "editTitle");
186
187
  if (this.semanticSpec.title.text === undefined) {
187
188
  throw new Error("editTitle requires an existing chart title.");
@@ -210,7 +211,7 @@ export const editTitle = action(
210
211
  }
211
212
  resolveTitleLayout(next, normalized.config);
212
213
  return next.rematerializeTitle();
213
- }
214
+ })
214
215
  );
215
216
 
216
217
  export const removeTitle = action(
@@ -219,7 +220,7 @@ export const removeTitle = action(
219
220
  description: "Remove the complete chart title resource.",
220
221
  scope: "any"
221
222
  },
222
- function (args = {}) {
223
+ withGuideLayoutValidation(function (args = {}) {
223
224
  assertTitleScope(this, "removeTitle");
224
225
  noOptions(args, "removeTitle");
225
226
  if (
@@ -246,5 +247,5 @@ export const removeTitle = action(
246
247
  return next.compositionSpec?.type === "facet"
247
248
  ? next.materializeComposition()
248
249
  : next;
249
- }
250
+ })
250
251
  );
@@ -7,12 +7,10 @@ import {
7
7
  validatePositiveFinite
8
8
  } from "../../core/validation.js";
9
9
  import { resolveGraphicBounds } from "../../layout/canvas.js";
10
- import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
11
10
  import {
12
11
  alignedTextAnchor,
13
12
  alignedTitleAnchor,
14
13
  buildTitleReadingBlock,
15
- layoutBoundsIntersect,
16
14
  resolveTitleComponentBounds,
17
15
  unionTitleBounds
18
16
  } from "../../layout/title.js";
@@ -176,35 +174,7 @@ export function requireTitleConfig(program) {
176
174
  return program.titleConfig;
177
175
  }
178
176
 
179
- function reservedGraphicIds(program, position) {
180
- const ids = [];
181
- const axisChannel = ["top", "bottom"].includes(position) ? "x" : "y";
182
- const axis = program.guideConfigs.axis?.[axisChannel];
183
- if (axis && Object.values(axis).some(config => config?.position === position)) {
184
- const prefix = axisChannel === "x" ? "xAxis" : "yAxis";
185
- ids.push(`${prefix}Line`, `${prefix}Ticks`, `${prefix}Labels`, `${prefix}Title`);
186
- }
187
- const prefixes = {
188
- series: "seriesLegend",
189
- color: "colorLegend",
190
- gradient: "colorGradient",
191
- opacity: "opacityLegend"
192
- };
193
- for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
194
- if (config?.position !== position || prefixes[kind] === undefined) continue;
195
- ids.push(...Object.keys(program.graphicSpec.objects).filter(
196
- id => id.startsWith(prefixes[kind])
197
- ));
198
- if (kind === "series" && program.guideConfigs.legend?.size !== undefined) {
199
- ids.push(...Object.keys(program.graphicSpec.objects).filter(
200
- id => id.startsWith("sizeLegend")
201
- ));
202
- }
203
- }
204
- return [...new Set(ids)];
205
- }
206
-
207
- function validateLayout(program, position, titleBounds, plot, canvas) {
177
+ function validateLayout(position, titleBounds, plot, canvas) {
208
178
  if (
209
179
  titleBounds.left < 0 || titleBounds.top < 0 ||
210
180
  titleBounds.right > canvas.width || titleBounds.bottom > canvas.height
@@ -220,14 +190,6 @@ function validateLayout(program, position, titleBounds, plot, canvas) {
220
190
  if (!outsidePlot) {
221
191
  throw new Error(`Chart title requires more ${position}-margin space.`);
222
192
  }
223
- for (const id of reservedGraphicIds(program, position)) {
224
- const bounds = program.graphicSpec.objects[id] === undefined
225
- ? undefined
226
- : resolveConcreteGraphicBounds(program.graphicSpec, id);
227
- if (bounds !== undefined && layoutBoundsIntersect(titleBounds, bounds)) {
228
- throw new Error(`Chart title and ${position} guides require more margin space.`);
229
- }
230
- }
231
193
  }
232
194
 
233
195
  export function resolveTitleLayout(program, config) {
@@ -293,7 +255,6 @@ export function resolveTitleLayout(program, config) {
293
255
  : [resolveTitleComponentBounds(subtitle, config.subtitleStyle)])
294
256
  ]);
295
257
  validateLayout(
296
- program,
297
258
  config.position,
298
259
  titleBounds,
299
260
  plot,
@@ -1,6 +1,7 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateOptionObject } from "../../core/validation.js";
3
3
  import { findDataset } from "../../selectors/datasets.js";
4
+ import { findLayer } from "../../selectors/layers.js";
4
5
  import {
5
6
  applyFacadeGuides,
6
7
  normalizeAppearance,
@@ -19,7 +20,7 @@ const OPTIONS = Object.freeze([
19
20
  ]);
20
21
  const POSITION_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
21
22
  const DENSITY_OPTIONS = Object.freeze([
22
- "bandwidth", "extent", "steps", "kernel", "normalization", "width"
23
+ "bandwidth", "extent", "steps", "kernel", "normalization", "width", "side"
23
24
  ]);
24
25
  const WIDTH_OPTIONS = Object.freeze(["band", "resolve"]);
25
26
  const SPLIT_OPTIONS = Object.freeze(["field", "domain"]);
@@ -28,7 +29,6 @@ const AREA_OPTIONS = Object.freeze([
28
29
  ]);
29
30
  const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
30
31
  const OPERATION = "createViolinPlot";
31
- const ROLE_ERROR = `${OPERATION} requires one categorical axis and one quantitative axis.`;
32
32
 
33
33
  function inferFieldType(dataset, encoding, label) {
34
34
  if (encoding.fieldType !== undefined) return encoding.fieldType;
@@ -44,7 +44,7 @@ function inferFieldType(dataset, encoding, label) {
44
44
  return values.every(Number.isFinite) ? "quantitative" : "nominal";
45
45
  }
46
46
 
47
- function normalizePosition(dataset, value, label) {
47
+ export function normalizeViolinPosition(dataset, value, label) {
48
48
  const encoding = normalizeFieldEncoding(value, label);
49
49
  validateOptionObject(encoding, POSITION_OPTIONS, label);
50
50
  return {
@@ -53,6 +53,54 @@ function normalizePosition(dataset, value, label) {
53
53
  };
54
54
  }
55
55
 
56
+ export function resolveViolinRoles(dataset, xValue, yValue, operation) {
57
+ const x = normalizeViolinPosition(dataset, xValue, `${operation} x`);
58
+ const y = normalizeViolinPosition(dataset, yValue, `${operation} y`);
59
+ const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
60
+ const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
61
+ const roleError = `${operation} requires one categorical axis and one quantitative axis.`;
62
+ if (xCategorical === yCategorical || (
63
+ x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
64
+ )) {
65
+ throw new Error(roleError);
66
+ }
67
+ const category = xCategorical ? x : y;
68
+ const value = xCategorical ? y : x;
69
+ if (value.fieldType !== "quantitative") throw new Error(roleError);
70
+ return {
71
+ x,
72
+ y,
73
+ category,
74
+ value,
75
+ orientation: xCategorical ? "vertical" : "horizontal",
76
+ densityChannel: xCategorical ? "x" : "y"
77
+ };
78
+ }
79
+
80
+ export function resolveViolinSplit(value, category, operation, { removable = false } = {}) {
81
+ if (removable && value === false) return undefined;
82
+ if (value === undefined) return undefined;
83
+ validateOptionObject(value, SPLIT_OPTIONS, `${operation} split`);
84
+ if (typeof value.field !== "string" || value.field.length === 0) {
85
+ throw new TypeError(`${operation} split.field must be a non-empty string.`);
86
+ }
87
+ const split = { ...value };
88
+ if (split.field === category.field) {
89
+ throw new Error(`${operation} split field must differ from its category field.`);
90
+ }
91
+ return split;
92
+ }
93
+
94
+ export function resolveViolinDensity(value, current = {}, operation = OPERATION) {
95
+ if (value !== undefined) {
96
+ validateOptionObject(value, DENSITY_OPTIONS, `${operation} density`);
97
+ if (value.width !== undefined) {
98
+ validateOptionObject(value.width, WIDTH_OPTIONS, `${operation} density.width`);
99
+ }
100
+ }
101
+ return { ...current, ...(value ?? {}) };
102
+ }
103
+
56
104
  function axisGuideOptions(value, field) {
57
105
  if (value === false) return false;
58
106
  return {
@@ -95,31 +143,9 @@ export const createViolinPlot = action(
95
143
  });
96
144
  const data = resolveFacadeData(this, args.data, OPERATION);
97
145
  const dataset = findDataset(this, data);
98
- const x = normalizePosition(dataset, args.x, `${OPERATION} x`);
99
- const y = normalizePosition(dataset, args.y, `${OPERATION} y`);
100
- const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
101
- const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
102
- if (xCategorical === yCategorical || (
103
- x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
104
- )) {
105
- throw new Error(ROLE_ERROR);
106
- }
107
- const category = xCategorical ? x : y;
108
- const value = xCategorical ? y : x;
109
- if (value.fieldType !== "quantitative") {
110
- throw new Error(ROLE_ERROR);
111
- }
112
- let split = args.split;
113
- if (split !== undefined) {
114
- validateOptionObject(split, SPLIT_OPTIONS, `${OPERATION} split`);
115
- if (typeof split.field !== "string" || split.field.length === 0) {
116
- throw new TypeError(`${OPERATION} split.field must be a non-empty string.`);
117
- }
118
- split = { ...split };
119
- }
120
- if (split?.field === category.field) {
121
- throw new Error(`${OPERATION} split field must differ from its category field.`);
122
- }
146
+ const roles = resolveViolinRoles(dataset, args.x, args.y, OPERATION);
147
+ const { x, y, category, value } = roles;
148
+ const split = resolveViolinSplit(args.split, category, OPERATION);
123
149
  const color = normalizeEncoding(args.color, `${OPERATION} color`);
124
150
  if (
125
151
  color !== undefined &&
@@ -129,18 +155,8 @@ export const createViolinPlot = action(
129
155
  `${OPERATION} color must encode its category or split field.`
130
156
  );
131
157
  }
132
- const density = args.density === undefined ? {} : args.density;
133
- if (args.density !== undefined) {
134
- validateOptionObject(density, DENSITY_OPTIONS, `${OPERATION} density`);
135
- if (density.width !== undefined) {
136
- validateOptionObject(
137
- density.width,
138
- WIDTH_OPTIONS,
139
- `${OPERATION} density.width`
140
- );
141
- }
142
- }
143
- const { width: densityWidth, ...densityOptions } = density;
158
+ const density = resolveViolinDensity(args.density);
159
+ const { width: densityWidth, side: densitySide, ...densityOptions } = density;
144
160
  const area = normalizeAppearance(
145
161
  args.area,
146
162
  AREA_OPTIONS,
@@ -174,18 +190,46 @@ export const createViolinPlot = action(
174
190
  source: data,
175
191
  field: value.field,
176
192
  groupBy: category.field,
177
- densityChannel: xCategorical ? "x" : "y",
193
+ densityChannel: roles.densityChannel,
178
194
  ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
179
195
  ...(value.scale === undefined ? {} : { valueScale: value.scale }),
180
196
  ...densityOptions,
181
197
  placement: {
182
198
  type: "category",
199
+ ...(densitySide === undefined ? {} : { side: densitySide }),
183
200
  ...(densityWidth === undefined ? {} : { width: densityWidth }),
184
201
  ...(split === undefined ? {} : { split }),
185
202
  ...(category.scale === undefined ? {} : { scale: category.scale })
186
203
  }
187
204
  });
188
205
  if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
189
- return applyFacadeGuides(next, guides);
206
+ next = applyFacadeGuides(next, guides, id, args.guides ?? {});
207
+ const transform = findDataset(next, findLayer(next, id).data).transform[0];
208
+ return next._withMarkConfig(id, {
209
+ ...next.markConfigs[id],
210
+ violinPlot: {
211
+ materialized: true,
212
+ source: data,
213
+ orientation: roles.orientation,
214
+ category: category.field,
215
+ categoryType: category.fieldType,
216
+ value: value.field,
217
+ split: transform.placement.split,
218
+ density: {
219
+ bandwidth: transform.bandwidth,
220
+ extent: transform.extent,
221
+ steps: transform.steps,
222
+ kernel: transform.kernel,
223
+ normalization: transform.normalization,
224
+ width: transform.placement.width,
225
+ side: transform.placement.side
226
+ },
227
+ colorRole: color === undefined
228
+ ? undefined
229
+ : color.field === category.field
230
+ ? "category"
231
+ : color.field === split?.field ? "split" : undefined
232
+ }
233
+ });
190
234
  }
191
235
  );
@@ -0,0 +1,191 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { findDataset } from "../../selectors/datasets.js";
5
+ import { findLayer } from "../../selectors/layers.js";
6
+ import { rebindDistributionGuides } from "../distributions/revision.js";
7
+ import {
8
+ resolveViolinDensity,
9
+ resolveViolinRoles,
10
+ resolveViolinSplit
11
+ } from "./create.js";
12
+
13
+ const OPERATION = "editViolinPlot";
14
+ const OPTIONS = Object.freeze([
15
+ "target", "data", "x", "y", "split", "density"
16
+ ]);
17
+
18
+ function resolveOwner(program, requested) {
19
+ const eligible = program.semanticSpec.layers.filter(
20
+ layer => program.markConfigs[layer.id]?.violinPlot?.materialized === true
21
+ );
22
+ if (requested !== undefined) {
23
+ const id = validateUserId(requested, "Violin-plot owner id");
24
+ const layer = findLayer(program, id);
25
+ if (layer === undefined || !eligible.includes(layer)) {
26
+ throw new Error(`Unknown violin-plot owner "${id}".`);
27
+ }
28
+ return layer;
29
+ }
30
+ const current = findLayer(program, program.context.currentMark);
31
+ if (current !== undefined && eligible.includes(current)) return current;
32
+ if (eligible.length === 1) return eligible[0];
33
+ if (eligible.length === 0) throw new Error(`${OPERATION} requires a violin plot.`);
34
+ throw new Error(`${OPERATION} target is ambiguous; provide target.`);
35
+ }
36
+
37
+ function currentPosition(owner, current, channel) {
38
+ const categorical = current.orientation === "vertical"
39
+ ? channel === "x"
40
+ : channel === "y";
41
+ return {
42
+ field: categorical ? current.category : current.value,
43
+ fieldType: categorical ? current.categoryType : "quantitative",
44
+ scale: { id: owner.encoding[channel].scale }
45
+ };
46
+ }
47
+
48
+ function requestedScale(args, channel, expectedId) {
49
+ if (!Object.hasOwn(args, channel) || typeof args[channel] !== "object") {
50
+ return undefined;
51
+ }
52
+ const scale = args[channel].scale;
53
+ if (scale === undefined) return undefined;
54
+ if (scale.id !== undefined && scale.id !== expectedId) {
55
+ throw new Error(`${OPERATION} ${channel} scale cannot change its id.`);
56
+ }
57
+ const { id: _id, ...definition } = scale;
58
+ void _id;
59
+ return definition;
60
+ }
61
+
62
+ function assertSplitSelectionCompatibility(program, owner, changesSplit) {
63
+ if (!changesSplit) return;
64
+ const referenced = Object.entries(
65
+ program.materializationConfigs.selections ?? {}
66
+ ).find(([, selection]) =>
67
+ selection.target === owner.id && selection.selector?.channel === "color"
68
+ );
69
+ if (referenced !== undefined) {
70
+ throw new Error(
71
+ `${OPERATION} cannot change split while selection "${referenced[0]}" ` +
72
+ "references color."
73
+ );
74
+ }
75
+ }
76
+
77
+ export const editViolinPlot = action(
78
+ {
79
+ op: OPERATION,
80
+ description: "Revise one violin plot's data and statistical roles."
81
+ },
82
+ function (args = {}) {
83
+ validateKeys(args, OPTIONS, OPERATION);
84
+ if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
85
+ throw new Error(`${OPERATION} requires at least one violin-plot option.`);
86
+ }
87
+ const owner = resolveOwner(this, args.target);
88
+ const current = this.markConfigs[owner.id].violinPlot;
89
+ const source = Object.hasOwn(args, "data")
90
+ ? validateUserId(args.data, "Violin-plot data id")
91
+ : current.source;
92
+ const dataset = findDataset(this, source);
93
+ if (dataset === undefined) {
94
+ throw new Error(`Unknown violin-plot data "${source}".`);
95
+ }
96
+ const roles = resolveViolinRoles(
97
+ dataset,
98
+ Object.hasOwn(args, "x") ? args.x : currentPosition(owner, current, "x"),
99
+ Object.hasOwn(args, "y") ? args.y : currentPosition(owner, current, "y"),
100
+ OPERATION
101
+ );
102
+ const split = Object.hasOwn(args, "split")
103
+ ? resolveViolinSplit(args.split, roles.category, OPERATION, {
104
+ removable: true
105
+ })
106
+ : current.split;
107
+ if (split?.field === roles.category.field) {
108
+ throw new Error(
109
+ `${OPERATION} split field must differ from its category field.`
110
+ );
111
+ }
112
+ const density = resolveViolinDensity(args.density, current.density, OPERATION);
113
+ const changesSplit = split?.field !== current.split?.field;
114
+ assertSplitSelectionCompatibility(this, owner, changesSplit);
115
+ if (current.colorRole === "split" && split === undefined) {
116
+ throw new Error(
117
+ `${OPERATION} cannot remove split while color encodes the split field.`
118
+ );
119
+ }
120
+
121
+ const categoryChannel = roles.orientation === "vertical" ? "x" : "y";
122
+ const valueChannel = roles.orientation === "vertical" ? "y" : "x";
123
+ const categoryScale = requestedScale(
124
+ args, categoryChannel, owner.encoding[categoryChannel].scale
125
+ );
126
+ const valueScale = requestedScale(
127
+ args, valueChannel, owner.encoding[valueChannel].scale
128
+ );
129
+ const { width, side, ...densityOptions } = density;
130
+ let next = this;
131
+ if (current.colorRole === "split" && changesSplit) {
132
+ next = next.editSemantic({
133
+ property: `layer[${owner.id}].encoding.color.field`,
134
+ value: split.field
135
+ });
136
+ }
137
+ next = next.editDensity({
138
+ target: owner.id,
139
+ source,
140
+ field: roles.value.field,
141
+ groupBy: roles.category.field,
142
+ densityChannel: roles.densityChannel,
143
+ ...densityOptions,
144
+ ...(valueScale === undefined ? {} : { valueScale }),
145
+ placement: {
146
+ type: "category",
147
+ ...(side === undefined ? {} : { side }),
148
+ width,
149
+ ...(split === undefined ? {} : { split }),
150
+ ...(categoryScale === undefined ? {} : { scale: categoryScale })
151
+ }
152
+ });
153
+ next = rebindDistributionGuides(next, {
154
+ oldXScale: owner.encoding.x.scale,
155
+ oldYScale: owner.encoding.y.scale,
156
+ newXScale: owner.encoding.x.scale,
157
+ newYScale: owner.encoding.y.scale,
158
+ oldXTitle: current.orientation === "vertical" ? current.category : current.value,
159
+ oldYTitle: current.orientation === "vertical" ? current.value : current.category,
160
+ newXTitle: roles.x.field,
161
+ newYTitle: roles.y.field,
162
+ oldMeasureChannel: current.orientation === "vertical" ? "y" : "x",
163
+ newMeasureChannel: valueChannel
164
+ });
165
+ const revisedLayer = findLayer(next, owner.id);
166
+ const transform = findDataset(next, revisedLayer.data).transform[0];
167
+ next = next._withMarkConfig(owner.id, {
168
+ ...next.markConfigs[owner.id],
169
+ violinPlot: {
170
+ ...current,
171
+ materialized: true,
172
+ source,
173
+ orientation: roles.orientation,
174
+ category: roles.category.field,
175
+ categoryType: roles.category.fieldType,
176
+ value: roles.value.field,
177
+ split: transform.placement.split,
178
+ density: {
179
+ bandwidth: transform.bandwidth,
180
+ extent: transform.extent,
181
+ steps: transform.steps,
182
+ kernel: transform.kernel,
183
+ normalization: transform.normalization,
184
+ width: transform.placement.width,
185
+ side: transform.placement.side
186
+ }
187
+ }
188
+ });
189
+ return next._withContext({ currentMark: owner.id, currentData: source });
190
+ }
191
+ );
@@ -1,5 +1,7 @@
1
1
  import { createViolinPlot } from "./create.js";
2
+ import { editViolinPlot } from "./edit.js";
2
3
 
3
4
  export function registerViolinPlotActions(ProgramClass) {
4
5
  ProgramClass.prototype.createViolinPlot = createViolinPlot;
6
+ ProgramClass.prototype.editViolinPlot = editViolinPlot;
5
7
  }
@@ -173,6 +173,13 @@ export class ChartProgram {
173
173
  });
174
174
  }
175
175
 
176
+ _withoutResolvedScale(id) {
177
+ if (!Object.hasOwn(this.resolvedScales, id)) return this;
178
+ const { [id]: unused, ...remaining } = this.resolvedScales;
179
+ void unused;
180
+ return this._clone({ resolvedScales: freezeOwned(remaining) });
181
+ }
182
+
176
183
  _withMarkConfig(id, config) {
177
184
  if (typeof id !== "string" || id.length === 0) {
178
185
  throw new TypeError("Mark config id must be a non-empty string.");
@@ -1,6 +1,14 @@
1
1
  import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
2
2
 
3
3
  const metadataByWrappedAction = new WeakMap();
4
+ let actionCompletionHook;
5
+
6
+ export function setActionCompletionHook(hook) {
7
+ if (hook !== undefined && typeof hook !== "function") {
8
+ throw new TypeError("Action completion hook must be a function.");
9
+ }
10
+ actionCompletionHook = hook;
11
+ }
4
12
 
5
13
  export function getWrappedActionMetadata(value) {
6
14
  return metadataByWrappedAction.get(value);
@@ -127,16 +135,28 @@ export function action(metadata, implementation) {
127
135
  if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
128
136
  if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
129
137
 
138
+ const summarizedArgs = summarizeArgs(args);
130
139
  const entered = this._enterAction({
131
140
  ...ownedMetadata,
132
- args: summarizeArgs(args)
141
+ args: summarizedArgs
133
142
  });
134
- const result = implementation.call(entered, args);
143
+ let result = implementation.call(entered, args);
135
144
 
136
145
  if (!(result instanceof this.constructor)) {
137
146
  throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
138
147
  }
139
148
 
149
+ if (this.actionStack.length === 0 && actionCompletionHook !== undefined) {
150
+ result = actionCompletionHook(result, {
151
+ source: this,
152
+ metadata: ownedMetadata,
153
+ args: summarizedArgs
154
+ });
155
+ if (!(result instanceof this.constructor)) {
156
+ throw new TypeError("Action completion hook must return a ChartProgram.");
157
+ }
158
+ }
159
+
140
160
  return result._exitAction();
141
161
  };
142
162