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,28 +1,34 @@
1
+ import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
2
+ import { sameGuideValue } from "../../reuse.js";
3
+ import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
1
4
  import { action } from "../../../../core/action.js";
2
5
  import { validateOptionObject } from "../../../../core/validation.js";
3
6
  import { noOptions, resolveLayout, activeConfig } from "./layout.js";
4
7
  import { normalizeOptions } from "./options.js";
8
+ import { normalizeRecipe, resolveLegendSymbol } from "./recipes.js";
9
+ import { validateLegendChannels } from "../target.js";
10
+ import { createCategoricalLegendFromConfig, reconcileCategoricalSymbols } from "../lifecycle.js";
5
11
  import { findLayer } from "../../../../selectors/layers.js";
6
12
  import { findSemanticScale } from "../../../../selectors/scales.js";
7
- import { isSizeLegendPoint } from "../size.js";
13
+ import { isSizeLegendPoint, resolveSizeLegendConfig } from "../size.js";
8
14
  import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
15
+ import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
9
16
  import { legendResourcePolicies } from
10
17
  "../../../../materialization/guides/resources.js";
11
18
  import {
12
- hasMultiHorizontalLegendLane,
13
- hasMultiLegendLane,
19
+ hasHorizontalLegendLane,
20
+ hasLegendLane,
14
21
  hasMultiSideLegendLane
15
22
  } from "../lane.js";
16
23
  import {
17
24
  resolveCurrentDefinition,
18
25
  resolveDefinition,
19
- resolveLegendKind,
20
26
  resolveTarget,
21
27
  sameValues
22
28
  } from "./resolve.js";
23
29
 
24
30
  function finishLegend(program) {
25
- return hasMultiLegendLane(program) ? program.rematerializeLegend() : program;
31
+ return hasLegendLane(program) ? program.rematerializeLegend() : program;
26
32
  }
27
33
 
28
34
  function requestedCandidate(program, target, candidates) {
@@ -33,31 +39,15 @@ function requestedCandidate(program, target, candidates) {
33
39
  return candidates.includes(layer) ? layer : undefined;
34
40
  }
35
41
 
36
- function createStandaloneLegend(program, args, kind) {
37
- const { target, count, position, channels: _channels, ...unsupported } = args;
38
- const label = kind === "size" ? "size" : "stroke-width";
39
- const unsupportedKeys = Object.keys(unsupported);
40
- if (unsupportedKeys.length > 0) {
41
- throw new Error(
42
- `Standalone ${label} legend does not support option "${unsupportedKeys[0]}".`
43
- );
44
- }
45
- if (position !== undefined && position !== "right") {
46
- throw new Error(
47
- `Standalone ${label} legends currently require position "right".`
48
- );
49
- }
50
- return finishLegend(program[kind === "size"
51
- ? "createSizeLegend"
52
- : "createStrokeWidthLegend"]({
53
- ...(target === undefined ? {} : { target }),
54
- ...(count === undefined ? {} : { count })
55
- }));
42
+ function resolveStandaloneLegendStep(args, kind) {
43
+ if (Object.hasOwn(args, "inheritAppearance")) throw new Error("createLegend does not accept inheritAppearance.");
44
+ const { channels: _channels, ...options } = args;
45
+ return { op: kind === "size" ? "createSizeLegend" : "createStrokeWidthLegend", args: options };
56
46
  }
57
47
 
58
48
  export const rematerializeLegend = action(
59
49
  { op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
60
- function (args = {}) {
50
+ withGuideLayoutValidation(function (args = {}) {
61
51
  noOptions(args, "rematerializeLegend");
62
52
  let next = this;
63
53
  const hasCategorical =
@@ -66,7 +56,11 @@ export const rematerializeLegend = action(
66
56
  if (hasCategorical) {
67
57
  const { kind, config } = activeConfig(this);
68
58
  const definition = resolveCurrentDefinition(this, config);
69
- const changed =
59
+ const symbol = config.inferredSymbol
60
+ ? normalizeRecipe(resolveLegendSymbol(this, findLayer(this, config.target), definition.channels), kind)
61
+ : config.symbol;
62
+ const symbolChanged = !sameGuideValue(symbol, config.symbol);
63
+ const changed = symbolChanged ||
70
64
  !sameValues(config.channels, definition.channels) ||
71
65
  !sameValues(config.domain, definition.domain) ||
72
66
  !sameValues(config.scales, definition.scales) ||
@@ -75,6 +69,7 @@ export const rematerializeLegend = action(
75
69
  next = changed
76
70
  ? this._withLegendConfig(kind, {
77
71
  ...config,
72
+ symbol,
78
73
  channels: definition.channels,
79
74
  scales: definition.scales,
80
75
  field: definition.field,
@@ -112,6 +107,10 @@ export const rematerializeLegend = action(
112
107
  });
113
108
  }
114
109
  }
110
+ if (symbolChanged) {
111
+ resolveLayout(next, next.guideConfigs.legend[kind]);
112
+ next = reconcileCategoricalSymbols(next, config, next.guideConfigs.legend[kind]);
113
+ }
115
114
  if (config.border !== false) next = next.rematerializeLegendBackground();
116
115
  next = next
117
116
  .rematerializeLegendSymbols()
@@ -133,251 +132,196 @@ export const rematerializeLegend = action(
133
132
  if (hasMultiSideLegendLane(next)) {
134
133
  next = next.rematerializeSideLegendLane();
135
134
  }
136
- if (hasMultiHorizontalLegendLane(next)) {
135
+ if (hasHorizontalLegendLane(next)) {
137
136
  next = next.rematerializeHorizontalLegendLane();
138
137
  }
139
138
  return next;
140
- }
139
+ })
141
140
  );
142
141
 
142
+ export function resolveCategoricalLegendConfig(program, args = {}) {
143
+ const layer = resolveTarget(program, args.target);
144
+ const definition = resolveDefinition(
145
+ program,
146
+ layer,
147
+ args.channels,
148
+ args.title,
149
+ args.order === undefined ? undefined : normalizeLegendOrder(args.order)
150
+ );
151
+ const options = normalizeOptions({ ...args,
152
+ symbol: resolveLegendSymbol(program, layer, definition.channels, args.symbol)
153
+ }, definition.kind);
154
+ const config = {
155
+ target: layer.id,
156
+ ...definition,
157
+ inferredTitle: !Object.hasOwn(args, "title"),
158
+ inferredSymbol: args.symbol === undefined || args.symbol === "auto",
159
+ position: options.position,
160
+ align: options.align,
161
+ direction: options.direction,
162
+ columns: options.columns,
163
+ offset: options.offset,
164
+ titlePosition: options.titlePosition,
165
+ symbol: options.symbol,
166
+ labels: options.labels,
167
+ titleStyle: options.titleStyle,
168
+ itemGap: options.itemGap,
169
+ layout: options.layout,
170
+ border: options.border,
171
+ titleVisible: true
172
+ };
173
+ return config;
174
+ }
175
+
143
176
  export const createCategoricalLegend = action(
144
177
  { op: "createCategoricalLegend", description: "Create one categorical legend block." },
145
- function (args = {}) {
146
- const layer = resolveTarget(this, args.target);
147
- const kind = resolveLegendKind(layer, args.channels);
148
- const options = normalizeOptions(args, kind);
178
+ withGuideLayoutValidation(function (args = {}) {
179
+ const config = resolveCategoricalLegendConfig(this, args);
149
180
  if (
150
181
  this.semanticSpec.guides.legend?.series !== undefined ||
151
182
  this.semanticSpec.guides.legend?.color !== undefined
152
183
  ) {
153
184
  throw new Error("createCategoricalLegend requires a missing legend.");
154
185
  }
155
- const definition = resolveDefinition(
156
- this,
157
- layer,
158
- options.channels,
159
- options.title
160
- );
161
- const config = {
162
- target: layer.id,
163
- ...definition,
164
- inferredTitle: !Object.hasOwn(args, "title"),
165
- position: options.position,
166
- align: options.align,
167
- direction: options.direction,
168
- columns: options.columns,
169
- offset: options.offset,
170
- titlePosition: options.titlePosition,
171
- symbol: options.symbol,
172
- labels: options.labels,
173
- titleStyle: options.titleStyle,
174
- itemGap: options.itemGap,
175
- bottomGrid: options.bottomGrid,
176
- border: options.border,
177
- titleVisible: true
178
- };
179
186
  resolveLayout(this, config);
180
- let next = this;
181
- if (kind === "series") {
182
- next = next
183
- .editSemantic({
184
- property: "guide.legend.series.channels",
185
- value: definition.channels
186
- })
187
- .editSemantic({
188
- property: "guide.legend.series.scales",
189
- value: definition.scales
190
- })
191
- .editSemantic({
192
- property: "guide.legend.series.title",
193
- value: definition.title
194
- });
195
- } else {
196
- next = next
197
- .editSemantic({
198
- property: "guide.legend.color.scale",
199
- value: definition.scales[0]
200
- })
201
- .editSemantic({
202
- property: "guide.legend.color.title",
203
- value: definition.title
204
- });
205
- }
206
- next = next._withLegendConfig(kind, config);
207
- if (config.border !== false) next = next.createLegendBackground();
208
- return next
209
- .createLegendSymbols()
210
- .createLegendLabels()
211
- .createLegendTitle();
212
- }
187
+ return createCategoricalLegendFromConfig(this, config,
188
+ args.order === undefined ? undefined : normalizeLegendOrder(args.order));
189
+ })
213
190
  );
214
191
 
215
- export const createLegend = action(
216
- { op: "createLegend", description: "Create an inferred legend for selected channels." },
217
- function (args = {}) {
218
- validateOptionObject(args, undefined, "createLegend");
219
- const channels = args.channels;
220
- if (channels !== undefined && !Array.isArray(channels)) {
221
- throw new TypeError("createLegend channels must be an array.");
192
+ export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
193
+ validateOptionObject(args, undefined, "createLegend");
194
+ const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
195
+ const channels = args.channels;
196
+ if (channels !== undefined) validateLegendChannels(channels, "createLegend");
197
+ const standalone = [
198
+ ["size", isSizeLegendPoint, ["color", "shape", "strokeDash", "opacity"]],
199
+ ["strokeWidth", isStrokeWidthLegendLayer, args.target === undefined
200
+ ? ["color", "shape", "strokeDash", "size", "opacity"]
201
+ : ["color", "shape", "strokeDash", "opacity"]],
202
+ ["opacity", isOpacityLegendLayer,
203
+ ["color", "shape", "strokeDash", "size"]]
204
+ ];
205
+ for (const [kind, eligible, otherChannels] of standalone) {
206
+ const explicit = channels?.length === 1 && channels[0] === kind;
207
+ const inferred = channels === undefined && candidates.filter(eligible).length === 1 &&
208
+ !candidates.some(layer => otherChannels.some(channel => layer.encoding?.[channel]?.scale !== undefined));
209
+ if (explicit || inferred) {
210
+ const step = kind === "opacity" ? { op: "createOpacityLegend", args }
211
+ : resolveStandaloneLegendStep(args, kind);
212
+ return { steps: [step], finish: "auto" };
222
213
  }
223
- const explicitSize = channels?.length === 1 && channels[0] === "size";
224
- const explicitStrokeWidth = channels?.length === 1 &&
225
- channels[0] === "strokeWidth";
226
- const requestedSizeLayer = args.target === undefined
227
- ? undefined
228
- : findLayer(this, args.target);
229
- const sizeCandidates = this.semanticSpec.layers.filter(isSizeLegendPoint);
230
- const hasNonSizeLegendEncoding = layer =>
231
- ["color", "shape", "strokeDash", "opacity"].some(channel =>
232
- layer.encoding?.[channel]?.scale !== undefined
233
- );
234
- const hasNonSizeLegendCandidate = this.semanticSpec.layers.some(
235
- hasNonSizeLegendEncoding
236
- );
237
- const inferredSize = channels === undefined && (
238
- (args.target !== undefined && isSizeLegendPoint(requestedSizeLayer) &&
239
- !hasNonSizeLegendEncoding(requestedSizeLayer)) ||
240
- (args.target === undefined && sizeCandidates.length === 1 &&
241
- !hasNonSizeLegendCandidate)
242
- );
243
- if (explicitSize || inferredSize) {
244
- return createStandaloneLegend(this, args, "size");
214
+ }
215
+ const continuousColorCandidates = candidates.filter(layer => {
216
+ const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
217
+ ? layer.encoding?.color
218
+ : undefined;
219
+ const scale = findSemanticScale(program, encoding?.scale);
220
+ return scale?.type === "sequential";
221
+ });
222
+ const continuousColor = requestedCandidate(
223
+ program, args.target, continuousColorCandidates
224
+ );
225
+ if (
226
+ (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
227
+ (channels === undefined && continuousColor)
228
+ ) {
229
+ return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
230
+ }
231
+ const intervalColorCandidates = candidates.filter(layer => {
232
+ const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
233
+ const scale = findSemanticScale(program, encoding?.scale);
234
+ return ["quantize", "quantile", "threshold"].includes(scale?.type);
235
+ });
236
+ if (
237
+ (channels?.length === 1 && channels[0] === "color" &&
238
+ intervalColorCandidates.length > 0) ||
239
+ (channels === undefined && intervalColorCandidates.length > 0)
240
+ ) {
241
+ return { steps: [{ op: "createIntervalLegend", args }], finish: "auto" };
242
+ }
243
+ const wantsShape = channels?.includes("shape") === true;
244
+ const wantsSize = channels?.includes("size") === true;
245
+ const pointCandidates = candidates.filter(layer =>
246
+ layer.mark?.type === "point" &&
247
+ (channels === undefined
248
+ ? ["color", "shape"].some(channel => layer.encoding?.[channel]?.scale !== undefined)
249
+ : (wantsShape || wantsSize) && channels.every(channel =>
250
+ ["color", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
251
+ );
252
+ const requestedPoint = requestedCandidate(program, args.target, pointCandidates);
253
+ const inferredSize = channels === undefined && pointCandidates.some(isSizeLegendPoint);
254
+ if ((wantsSize || inferredSize) && requestedPoint === undefined) {
255
+ throw new Error("Combined size legend requires one eligible point mark or an explicit target.");
256
+ }
257
+ if (requestedPoint !== undefined) {
258
+ const { count, ...categoricalArgs } = args;
259
+ const combined = requestedPoint.encoding?.size?.scale !== undefined &&
260
+ (channels === undefined || wantsSize);
261
+ if (count !== undefined && !combined) {
262
+ throw new Error("Legend count requires a selected size legend.");
245
263
  }
246
- const strokeWidthCandidates = this.semanticSpec.layers.filter(
247
- isStrokeWidthLegendLayer
248
- );
249
- const requestedStrokeWidthLayer = args.target === undefined
250
- ? undefined
251
- : findLayer(this, args.target);
252
- const hasOtherStrokeWidthLegendCandidate = this.semanticSpec.layers.some(
253
- layer => ["color", "shape", "strokeDash", "size", "opacity"].some(
254
- channel => layer.encoding?.[channel]?.scale !== undefined
255
- )
256
- );
257
- const inferredStrokeWidth = channels === undefined && (
258
- (args.target !== undefined &&
259
- isStrokeWidthLegendLayer(requestedStrokeWidthLayer) &&
260
- !hasNonSizeLegendEncoding(requestedStrokeWidthLayer)) ||
261
- (args.target === undefined && strokeWidthCandidates.length === 1 &&
262
- !hasOtherStrokeWidthLegendCandidate)
263
- );
264
- if (explicitStrokeWidth || inferredStrokeWidth) {
265
- return createStandaloneLegend(this, args, "strokeWidth");
264
+ if (combined && categoricalArgs.layout === "legacy-bottom") {
265
+ throw new Error('Combined size legends require layout "edge".');
266
266
  }
267
- const opacityCandidates = this.semanticSpec.layers.filter(layer =>
268
- layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
269
- );
270
- const hasOtherLegendCandidate = this.semanticSpec.layers.some(layer =>
271
- ["color", "shape", "strokeDash", "size"].some(channel =>
272
- layer.encoding?.[channel]?.scale !== undefined
273
- )
267
+ const inferredChannels = ["color", "shape"].filter(
268
+ channel => requestedPoint.encoding?.[channel]?.scale !== undefined
274
269
  );
275
- if (
276
- (channels?.length === 1 && channels[0] === "opacity") ||
277
- (channels === undefined && opacityCandidates.length === 1 &&
278
- !hasOtherLegendCandidate)
279
- ) {
280
- return finishLegend(this.createOpacityLegend(args));
281
- }
282
- const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
283
- const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
284
- ? layer.encoding?.color
285
- : undefined;
286
- const scale = findSemanticScale(this, encoding?.scale);
287
- return scale?.type === "sequential";
288
- });
289
- const continuousColor = requestedCandidate(
290
- this, args.target, continuousColorCandidates
291
- );
292
- if (
293
- (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
294
- (channels === undefined && continuousColor)
295
- ) {
296
- return finishLegend(this.createGradientLegend(args));
270
+ const { format: sizeFormat, ...categoricalLabels } = categoricalArgs.labels ?? {};
271
+ if (!combined && sizeFormat !== undefined && sizeFormat !== "auto") {
272
+ throw new Error("Categorical legend labels do not accept format.");
297
273
  }
298
- const intervalColorCandidates = this.semanticSpec.layers.filter(layer => {
299
- const encoding = layer.mark?.type === "point" ? layer.encoding?.color : undefined;
300
- const scale = findSemanticScale(this, encoding?.scale);
301
- return ["quantize", "quantile", "threshold"].includes(scale?.type);
302
- });
303
- if (
304
- (channels?.length === 1 && channels[0] === "color" &&
305
- intervalColorCandidates.length > 0) ||
306
- (channels === undefined && intervalColorCandidates.length > 0)
307
- ) {
308
- return finishLegend(this.createIntervalLegend(args));
309
- }
310
- const wantsShape = channels?.includes("shape") === true;
311
- const pointCandidates = this.semanticSpec.layers.filter(layer =>
312
- layer.mark?.type === "point" &&
313
- layer.encoding?.shape?.scale !== undefined &&
314
- (wantsShape || layer.encoding?.color?.scale !== undefined)
315
- );
316
- const requestedPoint = requestedCandidate(this, args.target, pointCandidates);
317
- if (requestedPoint !== undefined) {
318
- const { count, ...categoricalArgs } = args;
319
- if (
320
- requestedPoint.encoding?.size?.scale !== undefined &&
321
- categoricalArgs.position !== undefined &&
322
- !["right", "left"].includes(categoricalArgs.position)
323
- ) {
324
- throw new Error(
325
- "Combined point series and size legends currently require a side position."
326
- );
274
+ const steps = [{ op: "createCategoricalLegend", args: {
275
+ ...categoricalArgs,
276
+ ...(categoricalArgs.labels === undefined ? {} : { labels: categoricalLabels }),
277
+ target: requestedPoint.id,
278
+ channels: channels?.filter(channel => channel !== "size") ?? inferredChannels
279
+ } }];
280
+ if (combined) steps.push({ op: "createSizeLegend", args: {
281
+ target: requestedPoint.id,
282
+ ...(count === undefined ? {} : { count }),
283
+ ...(categoricalArgs.labels === undefined ? {} : { labels: {
284
+ ...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
285
+ ...(sizeFormat === undefined ? {} : { format: sizeFormat })
286
+ } }),
287
+ inheritAppearance: true
288
+ } });
289
+ // Validate both content owners before any component action starts.
290
+ resolveCategoricalLegendConfig(program, steps[0].args);
291
+ if (combined) {
292
+ const size = resolveSizeLegendConfig(program, steps[1].args);
293
+ const existing = program.guideConfigs.legend?.size;
294
+ if (existing !== undefined && existing.target !== size.target) {
295
+ throw new Error("Combined point series legend requires the active size legend to share its target.");
327
296
  }
328
- const hasMatchingLine = this.semanticSpec.layers.some(candidate =>
329
- candidate.mark?.type === "line" &&
330
- candidate.encoding?.color?.field === requestedPoint.encoding.color.field &&
331
- candidate.encoding.color.scale === requestedPoint.encoding.color.scale
332
- );
333
- const symbol = categoricalArgs.symbol ?? {
334
- layers: [
335
- ...(hasMatchingLine
336
- ? [{ type: "line", length: 32, lineWidth: 3 }]
337
- : []),
338
- {
339
- type: "point",
340
- size: Math.sqrt(64 / Math.PI),
341
- stroke: "white",
342
- strokeWidth: 0
343
- }
344
- ]
345
- };
346
- const inferredChannels = ["color", "shape"].filter(
347
- channel => requestedPoint.encoding?.[channel]?.scale !== undefined
348
- );
349
- let next = this.createCategoricalLegend({
350
- ...categoricalArgs,
351
- target: requestedPoint.id,
352
- channels: categoricalArgs.channels ?? inferredChannels,
353
- symbol
354
- });
355
- if (requestedPoint.encoding?.size?.scale !== undefined) {
356
- const existingSize = next.guideConfigs.legend?.size;
357
- if (existingSize === undefined) {
358
- next = next.createSizeLegend({
359
- target: requestedPoint.id,
360
- ...(count === undefined ? {} : { count }),
361
- inheritAppearance:
362
- categoricalArgs.position === "left" ||
363
- categoricalArgs.labels !== undefined ||
364
- categoricalArgs.titleStyle !== undefined
365
- });
366
- } else if (existingSize.target !== requestedPoint.id) {
367
- throw new Error(
368
- "Combined point series legend requires the active size legend to share its target."
369
- );
370
- } else if (count !== undefined && count !== existingSize.count) {
371
- throw new Error(
372
- "Existing size legend count must be edited before recreating the categorical block."
373
- );
374
- }
375
- next = next.rematerializeLegend();
297
+ if (existing !== undefined && count !== undefined && existing.count !== count) {
298
+ throw new Error("Existing size legend count must be edited before recreating the categorical block.");
376
299
  }
377
- return next;
378
300
  }
379
- return finishLegend(this.createCategoricalLegend(args));
301
+ return { steps, combined, finish: combined ? "always" : "auto" };
302
+ }
303
+ return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
304
+ }
305
+
306
+ export function applyLegendCreationPlan(program, plan) {
307
+ if (plan.steps.length === 0) return program;
308
+ let next = program;
309
+ for (const step of plan.steps) {
310
+ const existingSize = next.guideConfigs.legend?.size;
311
+ // The plan already validated that a retained size block is compatible.
312
+ if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
313
+ next = next[step.op](step.args);
314
+ }
380
315
  }
316
+ return plan.finish === "always" ? next.rematerializeLegend()
317
+ : plan.finish === "auto" ? finishLegend(next) : next;
318
+ }
319
+
320
+ export const createLegend = action(
321
+ { op: "createLegend", description: "Create an inferred legend for selected channels." },
322
+ withGuideLayoutValidation(function (args = {}) {
323
+ return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
324
+ })
381
325
  );
382
326
 
383
327
  export const removeCategoricalLegend = action(
@@ -385,7 +329,7 @@ export const removeCategoricalLegend = action(
385
329
  op: "removeCategoricalLegend",
386
330
  description: "Remove the active categorical legend and its concrete components."
387
331
  },
388
- function (args = {}) {
332
+ withGuideLayoutValidation(function (args = {}) {
389
333
  noOptions(args, "removeCategoricalLegend");
390
334
  const entries = ["series", "color"]
391
335
  .filter(kind => this.guideConfigs.legend?.[kind] !== undefined);
@@ -405,5 +349,5 @@ export const removeCategoricalLegend = action(
405
349
  next = next.editGraphics({ target, remove: true });
406
350
  }
407
351
  return next._withoutMaterializationConfig(["guides", "legend", kind]);
408
- }
352
+ })
409
353
  );
@@ -1,8 +1,7 @@
1
1
  import { action } from "../../../../core/action.js";
2
2
  import { formatVisibleText } from "../../../../core/textMetrics.js";
3
3
  import { activeConfig, graphic, noOptions, resolveLayout } from "./layout.js";
4
- import { resolveLegendGraphicPlacement } from
5
- "../../../../materialization/graphicHierarchy.js";
4
+ import { resolveCategoricalLegendPlacement } from "../lifecycle.js";
6
5
 
7
6
  export const rematerializeLegendLabels = action(
8
7
  { op: "rematerializeLegendLabels", description: "Rematerialize categorical legend labels." },
@@ -46,7 +45,7 @@ export const createLegendLabels = action(
46
45
  id,
47
46
  type: "text",
48
47
  length: config.domain.length,
49
- ...resolveLegendGraphicPlacement(this)
48
+ ...resolveCategoricalLegendPlacement(this)
50
49
  })
51
50
  .rematerializeLegendLabels();
52
51
  }
@@ -95,7 +94,7 @@ export const createLegendTitle = action(
95
94
  .createGraphics({
96
95
  id,
97
96
  type: "text",
98
- ...resolveLegendGraphicPlacement(this)
97
+ ...resolveCategoricalLegendPlacement(this)
99
98
  })
100
99
  .rematerializeLegendTitle();
101
100
  }
@@ -138,7 +137,7 @@ export const createLegendBackground = action(
138
137
  .createGraphics({
139
138
  id,
140
139
  type: "rect",
141
- ...resolveLegendGraphicPlacement(this)
140
+ ...resolveCategoricalLegendPlacement(this)
142
141
  })
143
142
  .rematerializeLegendBackground();
144
143
  }