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 { editLegendBackground, resolveLegendBackgroundFromBounds } from "./continuous/common.js";
1
2
  import { action } from "../../../core/action.js";
2
3
  import { noOptions } from "../../../core/validation.js";
3
4
  import {
@@ -8,6 +9,9 @@ import { finiteMidpoint } from "../../../grammar/numeric.js";
8
9
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
9
10
  import {
10
11
  resolveHorizontalLegendLane,
12
+ resolveHorizontalLegendGroup,
13
+ resolveSingleHorizontalLegendPlacement,
14
+ isHorizontalEdgeLegend,
11
15
  resolveSideLegendLane
12
16
  } from "../../../layout/legendLane.js";
13
17
  import { findCanvasGraphic } from
@@ -33,16 +37,13 @@ function categoricalFor(program, target) {
33
37
 
34
38
  function legendPosition(program, kind, config) {
35
39
  if (kind === "size") {
36
- return categoricalFor(program, config.target)?.position ?? "right";
40
+ return categoricalFor(program, config.target)?.position ?? config.position;
37
41
  }
38
- if (kind === "strokeWidth" || kind === "interval") return "right";
39
42
  return config.position;
40
43
  }
41
44
 
42
- function requestedOffset(kind, config) {
43
- return ["size", "strokeWidth"].includes(kind)
44
- ? 30
45
- : config.offset;
45
+ function requestedOffset(config) {
46
+ return config.offset;
46
47
  }
47
48
 
48
49
  function existingIds(program, kind) {
@@ -117,7 +118,8 @@ function blockDescriptor(program, kind, config) {
117
118
  id: kind,
118
119
  kind,
119
120
  target: config.target,
120
- offset: requestedOffset(kind, config),
121
+ border: borderFor(kind, config),
122
+ offset: requestedOffset(config),
121
123
  bounds,
122
124
  title,
123
125
  symbol: {
@@ -130,19 +132,17 @@ function blockDescriptor(program, kind, config) {
130
132
  x: textAnchor(program, components.labelId),
131
133
  width: labels.right - labels.left
132
134
  },
133
- occupiedBounds: components.backgroundId === undefined
134
- ? bounds
135
- : resolveConcreteGraphicBounds(
136
- program.graphicSpec,
137
- components.backgroundId
138
- ),
135
+ occupiedBounds: unionConcreteGraphicBounds(program.graphicSpec, [
136
+ ...foregroundIds,
137
+ ...(components.backgroundId === undefined ? [] : [components.backgroundId])
138
+ ]),
139
139
  ...components,
140
140
  foregroundIds
141
141
  };
142
142
  }
143
143
 
144
144
  function borderFor(kind, config) {
145
- return ["series", "color", "gradient", "opacity"].includes(kind)
145
+ return ["series", "color", "gradient", "opacity", "interval", "strokeWidth", "size"].includes(kind)
146
146
  ? config.border
147
147
  : false;
148
148
  }
@@ -159,7 +159,8 @@ function groupBlocks(blocks, configs) {
159
159
  ) {
160
160
  groups.push({
161
161
  id: `${block.kind}+size`,
162
- blocks: [block, next],
162
+ blocks: [block, next.backgroundId === undefined ? next : { ...next,
163
+ bounds: next.occupiedBounds, inset: next.border.padding + next.border.lineWidth / 2 }],
163
164
  border: borderFor(block.kind, config),
164
165
  backgroundId: block.backgroundId
165
166
  });
@@ -193,13 +194,15 @@ export function hasMultiSideLegendLane(program) {
193
194
  return ["right", "left"].some(side => edgeKinds(program, side).length > 1);
194
195
  }
195
196
 
196
- export function hasMultiHorizontalLegendLane(program) {
197
- return ["top", "bottom"].some(edge => edgeKinds(program, edge).length > 1);
197
+ export function hasHorizontalLegendLane(program) {
198
+ return ["top", "bottom"].some(edge => edgeKinds(program, edge).some(
199
+ ([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
200
+ ));
198
201
  }
199
202
 
200
- export function hasMultiLegendLane(program) {
203
+ export function hasLegendLane(program) {
201
204
  return hasMultiSideLegendLane(program) ||
202
- hasMultiHorizontalLegendLane(program);
205
+ hasHorizontalLegendLane(program);
203
206
  }
204
207
 
205
208
  function translateCommands(commands, dx, dy) {
@@ -269,35 +272,20 @@ function translateGraphic(program, id, dx, dy) {
269
272
  return next;
270
273
  }
271
274
 
272
- function yAxisBounds(program) {
273
- const ids = ["yAxisLine", "yAxisTicks", "yAxisLabels", "yAxisTitle"]
274
- .filter(id => program.graphicSpec.objects[id] !== undefined);
275
- return ids.length === 0
276
- ? undefined
277
- : unionConcreteGraphicBounds(program.graphicSpec, ids);
278
- }
279
-
280
- function horizontalCollisionBounds(program, edge) {
281
- const ids = edge === "top"
282
- ? ["chartTitle", "chartSubtitle"]
283
- : ["xAxisLine", "xAxisTicks", "xAxisLabels", "xAxisTitle"];
284
- return ids
285
- .filter(id => program.graphicSpec.objects[id] !== undefined)
286
- .map(id => resolveConcreteGraphicBounds(program.graphicSpec, id))
287
- .filter(bounds => bounds !== undefined);
288
- }
289
-
290
275
  function horizontalGroups(program, groups) {
291
276
  return groups.map(group => {
292
277
  const representative = group.blocks[0];
293
278
  const config = program.guideConfigs.legend?.[representative.kind];
294
- const titleId = representative.titleId;
279
+ const atomic = group.blocks.length > 1;
280
+ const titleId = atomic ? undefined : representative.titleId;
295
281
  const contentIds = group.blocks.flatMap(block => block.foregroundIds)
296
282
  .filter(id => id !== titleId);
283
+ if (atomic) contentIds.push(...group.blocks.slice(1).flatMap(block =>
284
+ block.backgroundId === undefined ? [] : [block.backgroundId]));
297
285
  const content = unionConcreteGraphicBounds(program.graphicSpec, contentIds);
298
286
  const foreground = unionConcreteGraphicBounds(
299
287
  program.graphicSpec,
300
- group.blocks.flatMap(block => block.foregroundIds)
288
+ [...contentIds, ...(titleId === undefined ? [] : [titleId])]
301
289
  );
302
290
  if (content === undefined || foreground === undefined) {
303
291
  throw new Error(`Legend lane could not measure ${group.id} content.`);
@@ -308,11 +296,12 @@ function horizontalGroups(program, groups) {
308
296
  : border.padding + border.lineWidth / 2;
309
297
  return {
310
298
  id: group.id,
299
+ atomic,
311
300
  titleId,
312
301
  contentIds,
313
- title: representative.title,
302
+ title: atomic ? undefined : representative.title,
314
303
  inline: config?.titlePosition === "left",
315
- element: representative.symbol.bounds,
304
+ element: atomic ? foreground : representative.symbol.bounds,
316
305
  content,
317
306
  horizontal: { left: foreground.left, right: foreground.right,
318
307
  top: foreground.top, bottom: foreground.bottom },
@@ -323,6 +312,33 @@ function horizontalGroups(program, groups) {
323
312
  });
324
313
  }
325
314
 
315
+ function applyHorizontalPlan(program, groups, plan) {
316
+ let next = program;
317
+ const byId = new Map(groups.map(group => [group.id, group]));
318
+ for (const placement of plan.placements) {
319
+ const group = byId.get(placement.id);
320
+ if (group.titleId !== undefined) {
321
+ next = translateGraphic(next, group.titleId, placement.dx, placement.titleDy);
322
+ }
323
+ for (const id of group.contentIds) {
324
+ next = translateGraphic(next, id, placement.dx, placement.contentDy);
325
+ }
326
+ if (placement.background !== undefined) {
327
+ for (const property of ["x", "y", "width", "height"]) {
328
+ next = next.editGraphics({ target: placement.background.id, property,
329
+ value: placement.background[property] });
330
+ }
331
+ }
332
+ }
333
+ if (plan.background !== undefined) {
334
+ for (const property of ["x", "y", "width", "height"]) {
335
+ next = next.editGraphics({ target: plan.background.id, property,
336
+ value: plan.background[property] });
337
+ }
338
+ }
339
+ return next;
340
+ }
341
+
326
342
  export const rematerializeSideLegendLane = action(
327
343
  {
328
344
  op: "rematerializeSideLegendLane",
@@ -346,8 +362,7 @@ export const rematerializeSideLegendLane = action(
346
362
  side,
347
363
  plot,
348
364
  canvas,
349
- groups: groupBlocks(blocks, configs),
350
- axisBounds: yAxisBounds(this)
365
+ groups: groupBlocks(blocks, configs)
351
366
  });
352
367
  return [{ plan, blocks }];
353
368
  });
@@ -387,6 +402,12 @@ export const rematerializeSideLegendLane = action(
387
402
  value: "left"
388
403
  });
389
404
  }
405
+ for (const block of blocks) {
406
+ if (block.backgroundId === undefined || plan.backgrounds.some(item => item.id === block.backgroundId)) continue;
407
+ const bounds = unionConcreteGraphicBounds(next.graphicSpec, block.foregroundIds);
408
+ const background = resolveLegendBackgroundFromBounds([bounds], block.border, canvas, "Legend");
409
+ next = editLegendBackground(next, block.backgroundId, background, block.border);
410
+ }
390
411
  for (const background of plan.backgrounds) {
391
412
  for (const property of ["x", "y", "width", "height"]) {
392
413
  next = next.editGraphics({
@@ -414,55 +435,46 @@ export const rematerializeHorizontalLegendLane = action(
414
435
  throw new Error("Legend lane requires resolved Canvas and plot bounds.");
415
436
  }
416
437
  const configs = this.guideConfigs.legend ?? {};
417
- const plans = ["top", "bottom"].flatMap(edge => {
418
- const entries = edgeKinds(this, edge);
419
- if (entries.length < 2) return [];
420
- const blocks = entries.map(([kind, config]) =>
421
- blockDescriptor(this, kind, config)
438
+ let next = this;
439
+ for (const edge of ["top", "bottom"]) {
440
+ const entries = edgeKinds(next, edge).filter(
441
+ ([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
422
442
  );
423
- const groups = groupBlocks(blocks, configs);
424
- if (groups.length < 2) return [];
425
- const horizontal = horizontalGroups(this, groups);
443
+ if (entries.length === 0) continue;
444
+ if (entries.length === 1) {
445
+ const [kind, config] = entries[0];
446
+ const ids = existingIds(next, kind);
447
+ const bounds = unionConcreteGraphicBounds(next.graphicSpec, ids);
448
+ const { dx, dy } = resolveSingleHorizontalLegendPlacement({
449
+ plot, canvas, config, bounds
450
+ });
451
+ for (const id of ids) next = translateGraphic(next, id, dx, dy);
452
+ continue;
453
+ }
454
+ let groups = groupBlocks(entries.map(([kind, config]) =>
455
+ blockDescriptor(next, kind, config)), configs);
456
+ for (const group of groups.filter(group => group.blocks.length > 1)) {
457
+ const config = configs[group.blocks[0].kind];
458
+ const children = horizontalGroups(next, group.blocks.map((block, index) => ({
459
+ id: block.id, blocks: [block], border: index === 0 ? false : block.border,
460
+ backgroundId: index === 0 ? undefined : block.backgroundId
461
+ }))).map(child => ({ ...child, element: child.content }));
462
+ const plan = resolveHorizontalLegendGroup({ edge, plot, canvas, groups: children,
463
+ align: config.align, offset: config.offset, border: group.border,
464
+ backgroundId: group.backgroundId });
465
+ next = applyHorizontalPlan(next, children, plan);
466
+ }
467
+ if (groups.length < 2) continue;
468
+ groups = groupBlocks(entries.map(([kind, config]) =>
469
+ blockDescriptor(next, kind, config)), configs);
470
+ const horizontal = horizontalGroups(next, groups);
426
471
  const plan = resolveHorizontalLegendLane({
427
472
  edge,
428
473
  plot,
429
474
  canvas,
430
- groups: horizontal,
431
- collisionBounds: horizontalCollisionBounds(this, edge)
475
+ groups: horizontal
432
476
  });
433
- return [{ plan, groups: horizontal }];
434
- });
435
- let next = this;
436
- for (const { plan, groups } of plans) {
437
- const byId = new Map(groups.map(group => [group.id, group]));
438
- for (const placement of plan.placements) {
439
- const group = byId.get(placement.id);
440
- if (group.titleId !== undefined) {
441
- next = translateGraphic(
442
- next,
443
- group.titleId,
444
- placement.dx,
445
- placement.titleDy
446
- );
447
- }
448
- for (const id of group.contentIds) {
449
- next = translateGraphic(
450
- next,
451
- id,
452
- placement.dx,
453
- placement.contentDy
454
- );
455
- }
456
- if (placement.background !== undefined) {
457
- for (const property of ["x", "y", "width", "height"]) {
458
- next = next.editGraphics({
459
- target: placement.background.id,
460
- property,
461
- value: placement.background[property]
462
- });
463
- }
464
- }
465
- }
477
+ next = applyHorizontalPlan(next, horizontal, plan);
466
478
  }
467
479
  return next;
468
480
  }
@@ -0,0 +1,132 @@
1
+ import {
2
+ legendGraphicIds, legendResourcePolicy, legendResourcePolicies
3
+ } from "../../../materialization/guides/resources.js";
4
+ import { findLayer } from "../../../selectors/layers.js";
5
+ import { resolveDefinition } from "./categorical/resolve.js";
6
+ import { normalizeRecipe, resolveLegendSymbol } from "./categorical/recipes.js";
7
+ import { resolveLayout, symbolGraphic } from "./categorical/layout.js";
8
+ import { resolveLegendGraphicPlacement } from "../../../materialization/graphicHierarchy.js";
9
+
10
+ export function removeLegendKinds(program, kinds) {
11
+ const removed = new Set(kinds);
12
+ const retainedSemanticKinds = new Set(
13
+ Object.entries(program.guideConfigs.legend ?? {})
14
+ .filter(([kind]) => !removed.has(kind))
15
+ .map(([kind]) => legendResourcePolicy(kind).semanticKind)
16
+ );
17
+ const semanticKinds = new Set(
18
+ kinds.map(kind => legendResourcePolicy(kind).semanticKind)
19
+ );
20
+ let next = program;
21
+ for (const kind of semanticKinds) {
22
+ if (
23
+ !retainedSemanticKinds.has(kind) &&
24
+ next.semanticSpec.guides.legend?.[kind] !== undefined
25
+ ) {
26
+ next = next.editSemantic({
27
+ property: `guide.legend.${kind}`,
28
+ remove: true
29
+ });
30
+ }
31
+ }
32
+ for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
33
+ if (next.graphicSpec.objects[id] !== undefined) {
34
+ next = next.editGraphics({ target: id, remove: true });
35
+ }
36
+ }
37
+ for (const kind of kinds) {
38
+ next = next._withoutMaterializationConfig(["guides", "legend", kind]);
39
+ }
40
+ return next;
41
+ }
42
+
43
+ export function resolveCategoricalLegendRevision(program, kind, previous, channels, {
44
+ order = program.semanticSpec.guides.legend?.[kind]?.order, validateLayout = true
45
+ } = {}) {
46
+ const layer = findLayer(program, previous.target);
47
+ const definition = resolveDefinition(program, layer, channels,
48
+ previous.inferredTitle ? undefined : previous.title, order);
49
+ const config = {
50
+ ...previous,
51
+ ...definition,
52
+ symbol: normalizeRecipe(resolveLegendSymbol(program, layer, channels,
53
+ previous.inferredSymbol ? undefined : previous.symbol), definition.kind)
54
+ };
55
+ if (validateLayout) resolveLayout(program, config);
56
+ return { kind, config, order };
57
+ }
58
+
59
+ export function createCategoricalLegendFromConfig(program, config, order) {
60
+ const { kind } = config;
61
+ let next = program;
62
+ if (kind === "series") {
63
+ next = next
64
+ .editSemantic({
65
+ property: "guide.legend.series.channels",
66
+ value: config.channels
67
+ })
68
+ .editSemantic({
69
+ property: "guide.legend.series.scales",
70
+ value: config.scales
71
+ })
72
+ .editSemantic({
73
+ property: "guide.legend.series.title",
74
+ value: config.title
75
+ });
76
+ } else {
77
+ next = next
78
+ .editSemantic({
79
+ property: "guide.legend.color.scale",
80
+ value: config.scales[0]
81
+ })
82
+ .editSemantic({
83
+ property: "guide.legend.color.title",
84
+ value: config.title
85
+ });
86
+ }
87
+ if (order !== undefined && order !== "scale") {
88
+ next = next.editSemantic({
89
+ property: `guide.legend.${kind}.order`,
90
+ value: order
91
+ });
92
+ }
93
+ next = next._withLegendConfig(kind, config);
94
+ if (config.border !== false) next = next.createLegendBackground();
95
+ next = next.createLegendSymbols().createLegendLabels();
96
+ return config.titleVisible === false ? next : next.createLegendTitle();
97
+ }
98
+
99
+ // Categorical content precedes its sampled companions in both creation and revision.
100
+ export function resolveCategoricalLegendPlacement(program) {
101
+ const companions = new Set(legendResourcePolicies()
102
+ .filter(policy => policy.family !== "categorical" && program.guideConfigs.legend?.[policy.kind] !== undefined)
103
+ .flatMap(policy => legendGraphicIds(policy.kind)));
104
+ const before = program.graphicSpec.objects.canvas?.children?.find(id => companions.has(id));
105
+ return resolveLegendGraphicPlacement(program, before === undefined ? {} : { before });
106
+ }
107
+
108
+ export function categoricalSymbolIds(config) {
109
+ return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
110
+ }
111
+
112
+ export function reconcileCategoricalSymbols(program, previous, config) {
113
+ const oldSymbols = categoricalSymbolIds(previous);
114
+ const newSymbols = categoricalSymbolIds(config);
115
+ const sameOrder = oldSymbols.size === newSymbols.size &&
116
+ [...oldSymbols].every((id, index) => id === [...newSymbols][index]);
117
+ let next = program;
118
+ if (!sameOrder) {
119
+ for (const id of oldSymbols) {
120
+ if (next.graphicSpec.objects[id] !== undefined) {
121
+ next = next.editGraphics({ target: id, remove: true });
122
+ }
123
+ }
124
+ }
125
+ for (const layer of config.symbol.layers) {
126
+ if (next.graphicSpec.objects[symbolGraphic(config, layer.type)] === undefined) {
127
+ const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[layer.type];
128
+ next = next[`createLegendSymbol${suffix}`]();
129
+ }
130
+ }
131
+ return next;
132
+ }
@@ -1,9 +1,13 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import { validateKeys } from "../../../core/validation.js";
3
- import { legendGraphicIds, legendResourcePolicy } from
4
- "../../../materialization/guides/resources.js";
5
- import { LEGEND_CHANNELS } from "../../../core/vocabulary.js";
6
- import { resolveLegendTarget } from "./target.js";
3
+ import {
4
+ removeLegendKinds,
5
+ resolveCategoricalLegendRevision,
6
+ createCategoricalLegendFromConfig
7
+ } from "./lifecycle.js";
8
+ import { resolveLegendTarget, validateLegendChannels } from "./target.js";
9
+
10
+ export { removeLegendKinds } from "./lifecycle.js";
7
11
 
8
12
  const OPTIONS = Object.freeze(["target", "channels"]);
9
13
 
@@ -18,85 +22,33 @@ function legendKindChannels(kind, config) {
18
22
  }[kind];
19
23
  }
20
24
 
21
- function validateRequestedChannels(channels) {
22
- if (!Array.isArray(channels)) {
23
- throw new TypeError("removeLegend channels must be an array.");
24
- }
25
- if (channels.length === 0) {
26
- throw new Error("removeLegend channels must select at least one channel.");
27
- }
28
- const seen = new Set();
29
- for (const channel of channels) {
30
- if (!LEGEND_CHANNELS.includes(channel)) {
31
- throw new Error(`Unsupported legend channel "${channel}".`);
32
- }
33
- if (seen.has(channel)) {
34
- throw new Error(`removeLegend channels contains duplicate "${channel}".`);
35
- }
36
- seen.add(channel);
37
- }
38
- }
39
-
40
- function resolveRequestedKinds(program, target, channels) {
41
- validateRequestedChannels(channels);
25
+ function resolveRequestedRemoval(program, target, channels) {
26
+ validateLegendChannels(channels, "removeLegend");
42
27
  const requested = new Set(channels);
43
28
  const matched = new Set();
44
29
  const kinds = [];
30
+ let partial;
45
31
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
46
32
  if (config?.target !== target) continue;
47
33
  const owned = legendKindChannels(kind, config);
48
34
  const overlap = owned.filter(channel => requested.has(channel));
49
- if (overlap.length > 0 && overlap.length !== owned.length) {
50
- throw new Error(
51
- `Legend channels ${owned.map(channel => `"${channel}"`).join(", ")} ` +
52
- "belong to one combined block and must be removed together."
53
- );
54
- }
55
- if (overlap.length === owned.length) {
56
- kinds.push(kind);
57
- for (const channel of owned) matched.add(channel);
35
+ if (overlap.length === 0) continue;
36
+ kinds.push(kind);
37
+ for (const channel of overlap) matched.add(channel);
38
+ if (overlap.length !== owned.length) {
39
+ partial = { kind, config, channels: owned.filter(channel => !requested.has(channel)) };
58
40
  }
59
41
  }
60
42
  const missing = channels.filter(channel => !matched.has(channel));
61
43
  if (missing.length > 0) {
62
- throw new Error(
63
- `Legend target "${target}" has no complete block for channel "${missing[0]}".`
64
- );
65
- }
66
- return kinds;
67
- }
68
-
69
- export function removeLegendKinds(program, kinds) {
70
- const removed = new Set(kinds);
71
- const retainedSemanticKinds = new Set(
72
- Object.entries(program.guideConfigs.legend ?? {})
73
- .filter(([kind]) => !removed.has(kind))
74
- .map(([kind]) => legendResourcePolicy(kind).semanticKind)
75
- );
76
- const semanticKinds = new Set(
77
- kinds.map(kind => legendResourcePolicy(kind).semanticKind)
78
- );
79
- let next = program;
80
- for (const kind of semanticKinds) {
81
- if (
82
- !retainedSemanticKinds.has(kind) &&
83
- next.semanticSpec.guides.legend?.[kind] !== undefined
84
- ) {
85
- next = next.editSemantic({
86
- property: `guide.legend.${kind}`,
87
- remove: true
88
- });
89
- }
90
- }
91
- for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
92
- if (next.graphicSpec.objects[id] !== undefined) {
93
- next = next.editGraphics({ target: id, remove: true });
94
- }
44
+ throw new Error(`Legend target "${target}" has no complete block for channel "${missing[0]}".`);
95
45
  }
96
- for (const kind of kinds) {
97
- next = next._withoutMaterializationConfig(["guides", "legend", kind]);
98
- }
99
- return next;
46
+ // Remove departing sibling configs from the immutable layout preflight view.
47
+ const view = kinds.reduce((next, kind) =>
48
+ next._withoutMaterializationConfig(["guides", "legend", kind]), program);
49
+ const revision = partial === undefined ? undefined
50
+ : resolveCategoricalLegendRevision(view, partial.kind, partial.config, partial.channels);
51
+ return { kinds, revision };
100
52
  }
101
53
 
102
54
  export function removeOwnedColorLegends(program, target) {
@@ -108,17 +60,20 @@ export function removeOwnedColorLegends(program, target) {
108
60
  }
109
61
 
110
62
  export const removeLegend = action(
111
- { op: "removeLegend", description: "Remove every legend block owned by one mark." },
63
+ { op: "removeLegend", description: "Remove selected legend content or every block owned by one mark." },
112
64
  function (args = {}) {
113
65
  validateKeys(args, OPTIONS, "removeLegend");
114
66
  const target = resolveLegendTarget(this, args.target, "removeLegend");
115
67
  const targetKinds = Object.entries(this.guideConfigs.legend ?? {})
116
68
  .filter(([, config]) => config?.target === target)
117
69
  .map(([kind]) => kind);
118
- const kinds = args.channels === undefined
119
- ? targetKinds
120
- : resolveRequestedKinds(this, target, args.channels);
70
+ const { kinds, revision } = args.channels === undefined
71
+ ? { kinds: targetKinds }
72
+ : resolveRequestedRemoval(this, target, args.channels);
121
73
  let next = removeLegendKinds(this, kinds);
74
+ if (revision !== undefined) {
75
+ next = createCategoricalLegendFromConfig(next, revision.config, revision.order);
76
+ }
122
77
  const retainedKinds = Object.keys(next.guideConfigs.legend ?? {});
123
78
  if (args.channels === undefined) {
124
79
  return retainedKinds.length === 0
@@ -128,7 +83,7 @@ export const removeLegend = action(
128
83
  const remainingTargetKinds = targetKinds.filter(kind => !kinds.includes(kind));
129
84
  const removedCategorical = kinds.some(kind => ["series", "color"].includes(kind));
130
85
  if (
131
- removedCategorical &&
86
+ removedCategorical && revision === undefined &&
132
87
  remainingTargetKinds.includes("size") &&
133
88
  next.guideConfigs.legend.size.inheritAppearance === true
134
89
  ) {