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,106 @@
1
+ import { findCoordinate } from "../../selectors/coordinates.js";
2
+ import { polarGuideNames } from "./polar/resolve.js";
3
+ import { polarGridOperations, validatePolarGridOptions } from "./polar/grids.js";
4
+ import { findLayer } from "../../selectors/layers.js";
5
+ import { resolveGuideOptions } from "./guides.js";
6
+ import { resolveGridOptions } from "./grids/grid.js";
7
+ import { gridNames, validateGridCreateArgs } from "./grids/resolve.js";
8
+ import { scopeFacadeAxes, planFacadeAxes } from "./facadeAxes.js";
9
+ import { assertGuideOptions, guideConflict, resolveStoredGuideCoordinate } from "./reuse.js";
10
+ import { planFacadeLegend } from "./facadeLegend.js";
11
+ import { applyLegendCreationPlan } from "./legends/categorical/actions.js";
12
+
13
+ function facadeGridNames(direction) {
14
+ return ["theta", "radial"].includes(direction)
15
+ ? { ...polarGridOperations(direction), channel: polarGuideNames(direction).channel }
16
+ : gridNames(direction);
17
+ }
18
+
19
+ function scopeGrid(program, layer, args) {
20
+ const directions = resolveGridOptions(program, args, [layer]);
21
+ const scoped = {};
22
+ const polar = findCoordinate(program, layer.coordinate)?.type === "polar";
23
+ for (const [direction, option] of Object.entries(directions)) {
24
+ if (option === undefined) continue;
25
+ if (polar !== ["theta", "radial"].includes(direction)) {
26
+ guideConflict("grid coordinate family does not belong to this facade");
27
+ }
28
+ const { channel, create } = facadeGridNames(direction);
29
+ const scale = layer.encoding?.[channel]?.scale;
30
+ if (scale === undefined || option.scale !== undefined && option.scale !== scale ||
31
+ option.coordinate !== undefined && option.coordinate !== layer.coordinate) {
32
+ guideConflict(`${direction} grid does not belong to this facade`);
33
+ }
34
+ scoped[direction] = { ...option, scale, coordinate: layer.coordinate };
35
+ if (polar) validatePolarGridOptions(scoped[direction], create, true);
36
+ else validateGridCreateArgs(scoped[direction], create);
37
+ }
38
+ // An explicit vertical-only request must not re-enable horizontal grid defaults.
39
+ if (polar) {
40
+ for (const direction of ["theta", "radial"]) if (scoped[direction] === undefined) scoped[direction] = false;
41
+ } else if (scoped.horizontal === undefined) scoped.horizontal = false;
42
+ return scoped;
43
+ }
44
+
45
+ function planGrid(program, args) {
46
+ return Object.entries(args).flatMap(([direction, option]) => {
47
+ if (option === false) return [];
48
+ const { channel, create } = facadeGridNames(direction);
49
+ const guide = program.semanticSpec.guides.grid?.[direction];
50
+ const config = program.guideConfigs.grid?.[direction];
51
+ if (guide === undefined && config === undefined) return [{ op: create, args: option }];
52
+ if (guide?.scale !== option.scale || config === undefined || config.scale !== option.scale ||
53
+ config.coordinate !== undefined && config.coordinate !== option.coordinate ||
54
+ resolveStoredGuideCoordinate(program, guide, channel) !== option.coordinate) {
55
+ guideConflict(`${direction} grid uses a different coordinate or scale`);
56
+ }
57
+ const { scale, coordinate, ...appearance } = option;
58
+ assertGuideOptions(appearance, config, `${direction} grid`);
59
+ return [];
60
+ });
61
+ }
62
+
63
+ export function fulfillFacadeGuides(program, guides, target, explicitGuides = guides) {
64
+ if (guides === false) return program;
65
+ const layer = findLayer(program, target);
66
+ if (layer === undefined) throw new Error(`Unknown facade guide target "${target}".`);
67
+ const resolved = resolveGuideOptions(program, guides, [layer]);
68
+ const axisDirections = Object.entries(resolved.axes ?? {}).filter(([key]) => key !== "coordinate");
69
+ const scopedAxes = resolved.axes === undefined || axisDirections.length > 0 && axisDirections.every(
70
+ ([, value]) => value === false
71
+ )
72
+ ? undefined
73
+ : scopeFacadeAxes(program, layer, resolved.axes);
74
+ const axes = scopedAxes;
75
+ const scopedGrid = resolved.grid === undefined || Object.values(resolved.grid).every(
76
+ value => value === false
77
+ )
78
+ ? undefined
79
+ : scopeGrid(program, layer, resolved.grid);
80
+ const grid = scopedGrid;
81
+ const legend = resolved.legend === undefined ? undefined : { ...resolved.legend, target };
82
+ if (resolved.legend?.target !== undefined && resolved.legend.target !== target) {
83
+ guideConflict("legend target does not belong to this facade");
84
+ }
85
+ if (axes === undefined && grid === undefined && legend === undefined) return program;
86
+ const legendPlan = legend === undefined ? undefined
87
+ : planFacadeLegend(program, layer, legend, explicitGuides.legend ?? {});
88
+ const steps = [
89
+ ...(axes === undefined ? [] : planFacadeAxes(program, layer, axes, explicitGuides.axes)),
90
+ ...(grid === undefined ? [] : planGrid(program, grid))
91
+ ];
92
+ const hasExisting = [program.guideConfigs.axis, program.guideConfigs.grid, program.guideConfigs.legend,
93
+ program.semanticSpec.guides.axis, program.semanticSpec.guides.grid, program.semanticSpec.guides.legend]
94
+ .some(config => Object.keys(config ?? {}).length > 0);
95
+ const unambiguousAxes = axes?.coordinate.type !== "parallel" ||
96
+ program.semanticSpec.layers.filter(candidate =>
97
+ candidate.coordinate === layer.coordinate && candidate.encoding?.parallel !== undefined
98
+ ).length === 1;
99
+ if (!hasExisting && unambiguousAxes) {
100
+ return program.createGuides({ axes: axes ?? false, grid: grid ?? false, legend: legendPlan?.args ?? false });
101
+ }
102
+ let next = program;
103
+ for (const step of steps) next = next[step.op](step.args);
104
+ if (legendPlan !== undefined) next = applyLegendCreationPlan(next, legendPlan);
105
+ return next;
106
+ }
@@ -0,0 +1,200 @@
1
+ import { validatePolarAxisArgs } from "./polar/axes/facade.js";
2
+ import { prefix as polarPrefix, resolveAngle } from "./polar/axes/shared.js";
3
+ import { validateAxesArgs } from "./axes/axes.js";
4
+ import { validateAxisArgs } from "./axes/axis.js";
5
+ import { axisComponentDisabled, hasAxisComponent } from "./axes/components.js";
6
+ import { validateAxisTickGroupArgs } from "./axes/tickGroups.js";
7
+ import { findCoordinate } from "../../selectors/coordinates.js";
8
+ import {
9
+ assertGuideOptions, guideConflict, resolveStoredGuideCoordinate, sameGuideValue
10
+ } from "./reuse.js";
11
+
12
+ export function scopeFacadeAxes(program, layer, args) {
13
+ const descriptor = validateAxesArgs(args);
14
+ const coordinate = findCoordinate(program, layer.coordinate);
15
+ if (coordinate === undefined) guideConflict(`layer "${layer.id}" has no coordinate`);
16
+ if (descriptor.id !== undefined && descriptor.id !== coordinate.id) {
17
+ guideConflict("axes.coordinate.id does not belong to this facade");
18
+ }
19
+ if (descriptor.type !== undefined && descriptor.type !== "auto" && descriptor.type !== coordinate.type) {
20
+ guideConflict("axes.coordinate.type does not match this facade");
21
+ }
22
+ if (coordinate.type === "polar") {
23
+ if (args.x !== undefined || args.y !== undefined) guideConflict("Polar facades do not own Cartesian axes");
24
+ const scoped = { coordinate: { id: coordinate.id, type: "polar" } };
25
+ for (const channel of ["theta", "radius"]) {
26
+ const option = args[channel];
27
+ const scale = layer.encoding?.[channel]?.scale;
28
+ if (option === false || option === undefined && scale === undefined) { scoped[channel] = false; continue; }
29
+ if (scale === undefined || option?.scale !== undefined && option.scale !== scale ||
30
+ option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
31
+ guideConflict(`axes.${channel} does not belong to this facade`);
32
+ }
33
+ scoped[channel] = { ...option, scale, coordinate: coordinate.id };
34
+ validatePolarAxisArgs(channel, scoped[channel], `create${polarPrefix(channel)}Axis`);
35
+ resolveAngle(program, channel, scoped[channel]);
36
+ }
37
+ return scoped;
38
+ }
39
+ if (args.theta !== undefined || args.radius !== undefined) {
40
+ guideConflict("Cartesian and Parallel facades do not own Polar axes");
41
+ }
42
+ if (coordinate.type === "parallel") {
43
+ if (args.x !== undefined || args.y !== undefined) {
44
+ throw new Error("createAxes channel options are not supported for Parallel axes.");
45
+ }
46
+ return { coordinate: { id: coordinate.id, type: "parallel" } };
47
+ }
48
+ const scoped = { coordinate: { id: coordinate.id, type: "cartesian" } };
49
+ for (const channel of ["x", "y"]) {
50
+ const option = args[channel];
51
+ if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
52
+ scoped[channel] = false;
53
+ continue;
54
+ }
55
+ const scale = layer.encoding?.[channel]?.scale;
56
+ if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
57
+ if (option?.scale !== undefined && option.scale !== scale) {
58
+ guideConflict(`axes.${channel}.scale does not belong to this facade`);
59
+ }
60
+ if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
61
+ guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
62
+ }
63
+ scoped[channel] = { ...option, scale, coordinate: coordinate.id };
64
+ validateAxisArgs(scoped[channel], `create${channel.toUpperCase()}Axis`);
65
+ if (scoped[channel].ticksAndLabels !== false) {
66
+ validateAxisTickGroupArgs(scoped[channel].ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
67
+ }
68
+ }
69
+ return scoped;
70
+ }
71
+
72
+ function tickMode(config) {
73
+ return config?.mode === "values" ? { values: config.values }
74
+ : config?.mode === "count" ? { count: config.count } : {};
75
+ }
76
+
77
+ function planAxis(program, channel, args, explicit) {
78
+ const prefix = channel.toUpperCase();
79
+ const guide = program.semanticSpec.guides.axis?.[channel];
80
+ const configs = program.guideConfigs.axis?.[channel] ?? {};
81
+ if (guide === undefined && Object.keys(configs).length === 0) {
82
+ return [{ op: `create${prefix}Axis`, args }];
83
+ }
84
+ if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
85
+ guideConflict(`${channel} axis uses a different coordinate or scale`);
86
+ }
87
+ const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
88
+ if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
89
+ const position = explicit.position ?? positions[0];
90
+ const shared = { scale: args.scale, ...(position === undefined ? {} : { position }) };
91
+ const explicitShared = explicit.position === undefined ? {} : { position: explicit.position };
92
+ const steps = guide.coordinate === undefined ? [{
93
+ op: "editSemantic", args: { property: `guide.axis.${channel}.coordinate`, value: args.coordinate }
94
+ }] : [];
95
+ const group = args.ticksAndLabels ?? {};
96
+ const explicitGroup = explicit.ticksAndLabels ?? {};
97
+ const explicitMode = Object.fromEntries(["count", "values"].filter(key =>
98
+ Object.hasOwn(explicitGroup, key)
99
+ ).map(key => [key, explicitGroup[key]]));
100
+ const requested = {
101
+ line: { ...explicitShared, ...(explicit.line ?? {}) },
102
+ ticks: { ...explicitShared, ...explicitMode, ...(explicitGroup.ticks ?? {}) },
103
+ labels: { ...explicitShared, ...explicitMode, ...(explicitGroup.labels ?? {}) },
104
+ title: { ...explicitShared, ...(explicit.title ?? {}) }
105
+ };
106
+ for (const component of ["line", "ticks", "labels", "title"]) {
107
+ const config = configs[component];
108
+ if (axisComponentDisabled(args, component)) {
109
+ if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
110
+ continue;
111
+ }
112
+ if (config === undefined) continue;
113
+ if (config.scale !== undefined && config.scale !== args.scale) {
114
+ guideConflict(`${channel} ${component} uses a different scale`);
115
+ }
116
+ assertGuideOptions(requested[component], component === "title"
117
+ ? { ...config, text: guide.title } : config, `${channel} axis ${component}`);
118
+ }
119
+ if (args.line !== false && configs.line === undefined) {
120
+ steps.push({ op: `create${prefix}AxisLine`, args: { ...shared, ...args.line } });
121
+ }
122
+ if (args.ticksAndLabels !== false) {
123
+ if (configs.ticks === undefined && configs.labels === undefined) {
124
+ steps.push({ op: `create${prefix}AxisTicksAndLabels`, args: { ...shared, ...group } });
125
+ } else {
126
+ if (configs.ticks === undefined) {
127
+ steps.push({ op: `create${prefix}AxisTicks`, args: { ...shared, ...tickMode(configs.labels), ...explicitMode, ...group.ticks } });
128
+ }
129
+ if (configs.labels === undefined) {
130
+ steps.push({ op: `create${prefix}AxisLabels`, args: { ...shared, ...group.labels } });
131
+ }
132
+ }
133
+ }
134
+ if (args.title !== false && configs.title === undefined) {
135
+ steps.push({ op: `create${prefix}AxisTitle`, args: { ...shared, ...args.title } });
136
+ }
137
+ return steps;
138
+ }
139
+
140
+ export function planFacadeAxes(program, layer, scoped, explicit = {}) {
141
+ if (scoped.coordinate.type === "parallel") {
142
+ const existing = program.semanticSpec.guides.axis?.parallel;
143
+ if (existing === undefined) return [{
144
+ op: "createParallelAxes", args: { target: layer.id, coordinate: layer.coordinate }
145
+ }];
146
+ const scales = layer.encoding.parallel.dimensions.map(dimension => dimension.scale);
147
+ if (existing.coordinate !== layer.coordinate || !sameGuideValue(existing.scales, scales)) {
148
+ guideConflict("Parallel axes use a different coordinate or dimension scales");
149
+ }
150
+ return [];
151
+ }
152
+ if (scoped.coordinate.type === "polar") {
153
+ return ["theta", "radius"].flatMap(channel => scoped[channel] === false ? []
154
+ : planPolarAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
155
+ }
156
+ return ["x", "y"].flatMap(channel => scoped[channel] === false ? []
157
+ : planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
158
+ }
159
+
160
+ function planPolarAxis(program, channel, args, explicit) {
161
+ const prefix = polarPrefix(channel);
162
+ const guide = program.semanticSpec.guides.axis?.[channel];
163
+ const configs = program.guideConfigs.axis?.[channel] ?? {};
164
+ if (guide === undefined && Object.keys(configs).length === 0) return [{ op: `create${prefix}Axis`, args }];
165
+ if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
166
+ guideConflict(`${channel} axis uses a different coordinate or scale`);
167
+ }
168
+ const shared = { scale: args.scale, coordinate: args.coordinate,
169
+ ...(channel === "radius" ? { angle: resolveAngle(program, channel, args) } : {}) };
170
+ const group = args.ticksAndLabels ?? {};
171
+ const explicitGroup = explicit.ticksAndLabels ?? {};
172
+ const mode = Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(explicitGroup, key))
173
+ .map(key => [key, explicitGroup[key]]));
174
+ const requested = {
175
+ line: explicit.line ?? {},
176
+ ticks: { ...mode, ...explicitGroup.ticks },
177
+ labels: { ...mode, ...explicitGroup.labels },
178
+ title: explicit.title || {}
179
+ };
180
+ const steps = [];
181
+ for (const component of ["line", "ticks", "labels", "title"]) {
182
+ const config = configs[component];
183
+ if (axisComponentDisabled(args, component)) {
184
+ if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
185
+ continue;
186
+ }
187
+ if (config !== undefined) {
188
+ if (config.scale !== args.scale || config.coordinate !== args.coordinate) guideConflict(`${channel} ${component} uses different resources`);
189
+ assertGuideOptions(requested[component], component === "title" ? { ...config, text: guide.title } : config,
190
+ `${channel} axis ${component}`);
191
+ continue;
192
+ }
193
+ const options = component === "line" ? args.line ?? {} : component === "title" ? args.title ?? {}
194
+ : { ...tickMode(configs[component === "ticks" ? "labels" : "ticks"]),
195
+ ...Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(group, key)).map(key => [key, group[key]])),
196
+ ...group[component] };
197
+ steps.push({ op: `create${prefix}Axis${component[0].toUpperCase() + component.slice(1)}`, args: { ...shared, ...options } });
198
+ }
199
+ return steps;
200
+ }
@@ -0,0 +1,63 @@
1
+ import { resolveLegendCreationPlan } from "./legends/categorical/actions.js";
2
+ import { resolveLegendStepConfig } from "./legends/creation.js";
3
+ import { legendResourcePolicy } from "../../materialization/guides/resources.js";
4
+ import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
5
+
6
+ function compatibleLegend(program, kind, requested, existing, explicit) {
7
+ const expectedScales = requested.scales ?? [requested.scale];
8
+ const actualScales = existing.scales ?? [existing.scale];
9
+ if (!sameGuideValue(expectedScales, actualScales) ||
10
+ !sameGuideValue(requested.channels, existing.channels)) {
11
+ guideConflict(`${kind} legend uses different channels or scales`);
12
+ }
13
+ const domain = config => config.domain ?? program.resolvedScales[config.scale]?.domain;
14
+ if (!sameGuideValue(domain(requested), domain(existing))) {
15
+ guideConflict(`${kind} legend uses a different domain or order`);
16
+ }
17
+ if (requested.symbol?.layers !== undefined && !sameGuideValue(
18
+ requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
19
+ )) guideConflict(`${kind} legend uses a different symbol recipe`);
20
+ const appearance = {};
21
+ for (const [key, value] of Object.entries(explicit)) {
22
+ if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
23
+ if (key === "symbol" && requested.symbol?.layers !== undefined) {
24
+ appearance.symbol = value === "auto" ? requested.symbol
25
+ : value.layers !== undefined ? value
26
+ : { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
27
+ } else if (key === "border" && value === true) {
28
+ if (existing.border === false) guideConflict(`${kind} legend border is disabled`);
29
+ appearance.border = {};
30
+ } else appearance[key] = value;
31
+ }
32
+ assertGuideOptions(appearance, existing, `${kind} legend`);
33
+ }
34
+
35
+ export function planFacadeLegend(program, layer, args, explicit = args) {
36
+ const plan = resolveLegendCreationPlan(program, args, [layer]);
37
+ const steps = [];
38
+ for (const step of plan.steps) {
39
+ const { kind, config } = resolveLegendStepConfig(program, step);
40
+ const stored = program.guideConfigs.legend?.[kind];
41
+ const policy = legendResourcePolicy(kind);
42
+ const occupied = program.semanticSpec.guides.legend?.[policy.semanticKind];
43
+ if (stored === undefined) {
44
+ if (occupied !== undefined ||
45
+ policy.family === "categorical" && (
46
+ program.semanticSpec.guides.legend?.series !== undefined ||
47
+ program.semanticSpec.guides.legend?.color !== undefined
48
+ )) {
49
+ guideConflict(`${kind} legend slot belongs to an incompatible legend family`);
50
+ }
51
+ steps.push(step);
52
+ } else {
53
+ if (occupied === undefined) guideConflict(`${kind} legend has no semantic owner`);
54
+ compatibleLegend(program, kind, config, stored, explicit);
55
+ }
56
+ }
57
+ const first = plan.steps[0];
58
+ const scopedArgs = first?.op === "createCategoricalLegend" && first.args.symbol !== undefined
59
+ ? { ...args, symbol: first.args.symbol } : args;
60
+ // Reused blocks retain their original target. The direct createLegend combined
61
+ // owner check is inapplicable to independently verified shared blocks.
62
+ return { ...plan, combined: false, steps, args: scopedArgs };
63
+ }
@@ -217,45 +217,49 @@ const editGrid = action(
217
217
  }
218
218
  );
219
219
 
220
+ export function resolveGridOptions(program, args = {}, layers = program.semanticSpec.layers) {
221
+ validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
222
+ const automatic = Object.keys(args).length === 0
223
+ ? resolveAutomaticGridOptions(program, layers)
224
+ : undefined;
225
+ const hasExplicitPolar = Object.hasOwn(args, "theta") ||
226
+ Object.hasOwn(args, "radial");
227
+ const polarDefault = automatic !== undefined &&
228
+ (Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
229
+ const horizontal = automatic !== undefined
230
+ ? automatic.horizontal === false ? undefined : automatic.horizontal
231
+ : polarDefault ||
232
+ (hasExplicitPolar && args.horizontal === undefined)
233
+ ? undefined
234
+ : normalizeDirection(args.horizontal, "horizontal");
235
+ const vertical = automatic !== undefined
236
+ ? automatic.vertical === false ? undefined : automatic.vertical
237
+ : args.vertical === undefined
238
+ ? undefined
239
+ : normalizeDirection(args.vertical, "vertical");
240
+ const theta = automatic !== undefined
241
+ ? automatic.theta
242
+ : args.theta === undefined
243
+ ? undefined
244
+ : normalizeDirection(args.theta, "theta");
245
+ const radial = automatic !== undefined
246
+ ? automatic.radial
247
+ : args.radial === undefined
248
+ ? undefined
249
+ : normalizeDirection(args.radial, "radial");
250
+ if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
251
+ throw new Error("createGrid requires at least one selected direction.");
252
+ }
253
+ return { horizontal, vertical, theta, radial };
254
+ }
255
+
220
256
  const createGrid = action(
221
257
  {
222
258
  op: "createGrid",
223
259
  description: "Create selected Cartesian grid directions."
224
260
  },
225
261
  function (args = {}) {
226
- validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
227
- const automatic = Object.keys(args).length === 0
228
- ? resolveAutomaticGridOptions(this)
229
- : undefined;
230
- const hasExplicitPolar = Object.hasOwn(args, "theta") ||
231
- Object.hasOwn(args, "radial");
232
- const polarDefault = automatic !== undefined &&
233
- (Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
234
- const horizontal = automatic !== undefined
235
- ? automatic.horizontal === false ? undefined : automatic.horizontal
236
- : polarDefault ||
237
- (hasExplicitPolar && args.horizontal === undefined)
238
- ? undefined
239
- : normalizeDirection(args.horizontal, "horizontal");
240
- const vertical = automatic !== undefined
241
- ? automatic.vertical === false ? undefined : automatic.vertical
242
- : args.vertical === undefined
243
- ? undefined
244
- : normalizeDirection(args.vertical, "vertical");
245
- const theta = automatic !== undefined
246
- ? automatic.theta
247
- : args.theta === undefined
248
- ? undefined
249
- : normalizeDirection(args.theta, "theta");
250
- const radial = automatic !== undefined
251
- ? automatic.radial
252
- : args.radial === undefined
253
- ? undefined
254
- : normalizeDirection(args.radial, "radial");
255
- if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
256
- throw new Error("createGrid requires at least one selected direction.");
257
- }
258
-
262
+ const { horizontal, vertical, theta, radial } = resolveGridOptions(this, args);
259
263
  let next = this;
260
264
  if (horizontal !== undefined) {
261
265
  next = next.createHorizontalGrid(horizontal);
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { validateUserId } from "../../../core/identifiers.js";
2
3
  import {
3
4
  validateGeneratedItemLimit,
@@ -126,7 +127,7 @@ export function validateGridEditArgs(args, operation) {
126
127
  export function resolveGridResources(program, direction, args) {
127
128
  const { channel } = gridNames(direction);
128
129
  let layers = program.semanticSpec.layers.filter(
129
- layer => layer.encoding?.[channel]?.scale !== undefined
130
+ layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
130
131
  );
131
132
  if (layers.length === 0) throw new Error(`${direction} grid requires a ${channel} encoding.`);
132
133
  const requestedCoordinate = args.coordinate === undefined
@@ -167,7 +168,14 @@ export function resolveGridResources(program, direction, args) {
167
168
  ) {
168
169
  throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
169
170
  }
170
- const related = new Set(layers.map(layer => layer.id));
171
+ const sourceIds = new Set(layers.map(layer => layer.id));
172
+ const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
173
+ isSourceOwnedText(layer) && sourceIds.has(layer.source)
174
+ )];
175
+ const related = new Set(relatedLayers.flatMap(layer => {
176
+ const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
177
+ return leader === undefined ? [layer.id] : [leader, layer.id];
178
+ }));
171
179
  let before;
172
180
  walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
173
181
  if (before === undefined && related.has(id)) before = id;
@@ -25,8 +25,9 @@ function validateOptions(args) {
25
25
  validateGuideOption(args.legend, "createGuides legend");
26
26
  }
27
27
 
28
- function inferAxesOptions(program, applicability) {
29
- const horizontalInterval = program.semanticSpec.layers.some(layer =>
28
+ function inferAxesOptions(program, applicability, layers) {
29
+ const horizontalInterval = layers.some(layer =>
30
+ program.markConfigs[layer.id]?.boxPlot?.orientation === "horizontal" ||
30
31
  layer.mark?.type === "rule" &&
31
32
  layer.encoding?.x?.fieldType === "quantitative" &&
32
33
  layer.encoding?.x2?.fieldType === "quantitative" &&
@@ -36,11 +37,11 @@ function inferAxesOptions(program, applicability) {
36
37
  ? { x: { ticksAndLabels: { count: 7 } } }
37
38
  : {};
38
39
  const directions = applicability.axes.directions;
39
- const hasStoredY = program.semanticSpec.layers.some(
40
+ const hasStoredY = layers.some(
40
41
  layer => layer.encoding?.y !== undefined
41
42
  );
42
43
  if (!directions.y && hasStoredY) inferred.y = false;
43
- const horizonLayers = program.semanticSpec.layers.filter(layer =>
44
+ const horizonLayers = layers.filter(layer =>
44
45
  findUpstreamTransform(
45
46
  program,
46
47
  findDataset(program, layer.data),
@@ -83,39 +84,45 @@ function mergeInferredOptions(inferred, explicit) {
83
84
  ? mergeInferredOptions(inferred[key], value)
84
85
  : value;
85
86
  }
87
+ if (Object.hasOwn(explicit, "count")) delete merged.values;
88
+ if (Object.hasOwn(explicit, "values")) delete merged.count;
86
89
  return merged;
87
90
  }
88
91
 
92
+ export function resolveGuideOptions(program, args = {}, layers = program.semanticSpec.layers) {
93
+ validateOptions(args);
94
+ const applicability = resolveGuideApplicability(program, layers);
95
+ const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
96
+ applicability.axes.parallel;
97
+ const inferredAxes = applicability.axes.cartesian
98
+ ? inferAxesOptions(program, applicability, layers)
99
+ : {};
100
+ const axes = args.axes === undefined && applicability.axes.cartesian
101
+ ? inferredAxes
102
+ : args.axes !== undefined && args.axes !== false && hasAxes
103
+ ? mergeInferredOptions(inferredAxes, args.axes)
104
+ : selectOption(args.axes, hasAxes);
105
+ const grid = args.grid === undefined &&
106
+ (applicability.grid.cartesian || applicability.grid.polar)
107
+ ? resolveAutomaticGridOptions(program, layers)
108
+ : selectOption(
109
+ args.grid,
110
+ applicability.grid.cartesian || applicability.grid.polar
111
+ );
112
+ const legend = selectOption(
113
+ args.legend,
114
+ applicability.legend
115
+ );
116
+ return { axes, grid, legend };
117
+ }
118
+
89
119
  const createGuides = action(
90
120
  {
91
121
  op: "createGuides",
92
122
  description: "Create applicable axes, grid, and legend."
93
123
  },
94
124
  function (args = {}) {
95
- validateOptions(args);
96
- const applicability = resolveGuideApplicability(this);
97
- const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
98
- applicability.axes.parallel;
99
- const inferredAxes = applicability.axes.cartesian
100
- ? inferAxesOptions(this, applicability)
101
- : {};
102
- const axes = args.axes === undefined && applicability.axes.cartesian
103
- ? inferredAxes
104
- : args.axes !== undefined && args.axes !== false && hasAxes
105
- ? mergeInferredOptions(inferredAxes, args.axes)
106
- : selectOption(args.axes, hasAxes);
107
- const grid = args.grid === undefined &&
108
- (applicability.grid.cartesian || applicability.grid.polar)
109
- ? resolveAutomaticGridOptions(this)
110
- : selectOption(
111
- args.grid,
112
- applicability.grid.cartesian || applicability.grid.polar
113
- );
114
- const legend = selectOption(
115
- args.legend,
116
- applicability.legend
117
- );
118
-
125
+ const { axes, grid, legend } = resolveGuideOptions(this, args);
119
126
  if (axes === undefined && grid === undefined && legend === undefined) {
120
127
  throw new Error("createGuides requires at least one selected guide.");
121
128
  }