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,30 +1,44 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
+ import { resolveLegendStepConfig } from "./creation.js";
3
+ import { normalizeLegendOrder } from "../../../grammar/categoryOrder.js";
4
+ import { resolveDefinition } from "./categorical/resolve.js";
1
5
  import { action } from "../../../core/action.js";
2
6
  import {
3
7
  validateGeneratedItemLimit,
4
8
  validateOptionObject
5
9
  } from "../../../core/validation.js";
6
10
  import { normalizeOptions } from "./categorical/options.js";
7
- import { symbolGraphic } from "./categorical/layout.js";
11
+ import { resolveLegendSymbol } from "./categorical/recipes.js";
12
+ import { resolveLayout } from "./categorical/layout.js";
13
+ import { resolveLegendCreationPlan } from "./categorical/actions.js";
14
+ import { categoricalSymbolIds, reconcileCategoricalSymbols, createCategoricalLegendFromConfig, resolveCategoricalLegendRevision, removeLegendKinds } from "./lifecycle.js";
15
+ import { createGradientLegendFromConfig } from "./continuous/gradient.js";
8
16
  import {
17
+ normalizeItemLegendLayout,
18
+ normalizeLegendBorder,
9
19
  normalizeLegendTextOptions,
20
+ normalizeLegendTitleOptions,
10
21
  normalizeContinuousLegend,
11
22
  validatePositive
12
23
  } from "./continuous/common.js";
13
- import { normalizeOpacitySymbol } from "./continuous/opacity.js";
14
- import { normalizeIntervalLegend } from "./continuous/interval.js";
24
+ import { normalizeOpacitySymbol, createOpacityLegendFromConfig } from "./continuous/opacity.js";
25
+ import { normalizeIntervalLegend, resolveIntervalConfig, resolveIntervalLayout, createIntervalLegendFromConfig } from "./continuous/interval.js";
15
26
  import { findLayer } from "../../../selectors/layers.js";
16
27
  import { resolveLegendGraphicPlacement } from
17
28
  "../../../materialization/graphicHierarchy.js";
18
- import { resolveLegendTarget } from "./target.js";
29
+ import { resolveLegendTarget, validateLegendChannels } from "./target.js";
30
+ import { SIZE_LEGEND_LABELS, SIZE_LEGEND_TITLE_STYLE, resolveSizeLegendLayout, createSizeLegendFromConfig } from "./size.js";
19
31
  import {
20
32
  STROKE_WIDTH_LEGEND_LABELS,
21
- STROKE_WIDTH_LEGEND_TITLE_STYLE
33
+ STROKE_WIDTH_LEGEND_TITLE_STYLE,
34
+ resolveStrokeWidthLegendLayout,
35
+ createStrokeWidthLegendFromConfig
22
36
  } from "./strokeWidth.js";
23
37
 
24
38
  const OPTIONS = Object.freeze([
25
- "target", "position", "align", "direction", "columns", "offset",
39
+ "target", "channels", "position", "layout", "align", "direction", "columns", "offset",
26
40
  "titlePosition", "title", "symbol", "labels", "titleStyle", "itemGap",
27
- "border", "count", "gradient"
41
+ "border", "count", "gradient", "order"
28
42
  ]);
29
43
 
30
44
  function mergeObject(previous, patch) {
@@ -46,16 +60,19 @@ function reconcileGraphic(program, id, shouldExist, definition) {
46
60
  return program.createGraphics({
47
61
  id,
48
62
  ...definition,
49
- ...resolveLegendGraphicPlacement(program)
63
+ ...resolveLegendGraphicPlacement(program, {
64
+ ...(definition.before === undefined ? {} : { before: definition.before }),
65
+ ...(definition.after === undefined ? {} : { after: definition.after })
66
+ })
50
67
  });
51
68
  }
52
69
  return program;
53
70
  }
54
71
 
55
- function editContinuous(program, kind, previous, args) {
72
+ function resolveContinuousEdit(program, kind, previous, args) {
56
73
  const allowed = kind === "gradient"
57
74
  ? ["target", "position", "align", "offset", "title", "labels",
58
- "titleStyle", "border", "count", "gradient"]
75
+ "titleStyle", "titlePosition", "border", "count", "gradient"]
59
76
  : ["target", "position", "align", "offset", "title", "symbol", "labels",
60
77
  "titleStyle", "titlePosition", "itemGap", "border", "count"];
61
78
  for (const key of Object.keys(args)) {
@@ -108,6 +125,11 @@ function editContinuous(program, kind, previous, args) {
108
125
  } else {
109
126
  config.symbol = normalizeOpacitySymbol(args.symbol ?? previous.symbol);
110
127
  }
128
+ return { config, titleMode, title, titleVisible };
129
+ }
130
+
131
+ function editContinuous(program, kind, previous, args) {
132
+ const { config, titleMode, title, titleVisible } = resolveContinuousEdit(program, kind, previous, args);
111
133
  const prefix = kind === "gradient" ? "colorGradient" : "opacityLegend";
112
134
  let next = program;
113
135
  if (titleMode === "auto" || typeof titleMode === "string") {
@@ -125,11 +147,11 @@ function editContinuous(program, kind, previous, args) {
125
147
  return next.rematerializeLegend();
126
148
  }
127
149
 
128
- function editInterval(program, previous, args) {
150
+ function resolveIntervalEdit(program, previous, args) {
129
151
  for (const key of Object.keys(args)) {
130
152
  if (![
131
153
  "target", "position", "align", "direction", "offset", "title",
132
- "symbol", "labels", "titleStyle", "itemGap", "border"
154
+ "symbol", "labels", "titleStyle", "itemGap", "border", "layout", "columns", "titlePosition"
133
155
  ].includes(key)) {
134
156
  throw new Error(`interval legend does not accept ${key}.`);
135
157
  }
@@ -149,7 +171,11 @@ function editInterval(program, previous, args) {
149
171
  target: previous.target,
150
172
  position: args.position ?? previous.position,
151
173
  align: args.align ?? previous.align,
152
- direction: args.direction ?? previous.direction,
174
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
175
+ ? undefined : previous.direction),
176
+ layout: args.layout === undefined ? previous.layout : args.layout,
177
+ columns: args.columns ?? previous.columns,
178
+ titlePosition: args.titlePosition ?? previous.titlePosition,
153
179
  offset: args.offset ?? previous.offset,
154
180
  title,
155
181
  symbol: mergeObject(previous.symbol, args.symbol),
@@ -158,12 +184,15 @@ function editInterval(program, previous, args) {
158
184
  itemGap: args.itemGap ?? previous.itemGap,
159
185
  border: mergeBorder(previous.border, args.border)
160
186
  });
161
- let next = program._withLegendConfig("interval", {
162
- ...previous,
163
- ...normalized,
164
- inferredTitle,
165
- titleVisible
166
- });
187
+ const config = { ...previous, ...normalized, inferredTitle, titleVisible };
188
+ const resolved = resolveIntervalConfig(program, config);
189
+ resolveIntervalLayout(program, resolved.config, resolved.scale);
190
+ return { normalized, config, titleMode, title, titleVisible };
191
+ }
192
+
193
+ function editInterval(program, previous, args) {
194
+ const { normalized, config, titleMode, title, titleVisible } = resolveIntervalEdit(program, previous, args);
195
+ let next = program._withLegendConfig("interval", config);
167
196
  if (titleMode === "auto" || typeof titleMode === "string") {
168
197
  next = next.editSemantic({
169
198
  property: "guide.legend.color.title",
@@ -182,20 +211,23 @@ function editInterval(program, previous, args) {
182
211
  return next.rematerializeLegend();
183
212
  }
184
213
 
185
- function editStrokeWidth(program, previous, args) {
186
- const allowed = ["target", "title", "count", "labels", "titleStyle"];
214
+ function resolveSampledLegendEdit(program, kind, previous, args) {
215
+ const size = kind === "size";
216
+ const label = size ? "size" : "stroke-width";
217
+ const allowed = ["target", "title", "count", "labels", "titleStyle",
218
+ "position", "layout", "align", "direction", "columns", "titlePosition", "offset", "itemGap", "border"];
187
219
  for (const key of Object.keys(args)) {
188
220
  if (!allowed.includes(key)) {
189
- throw new Error(`stroke-width legend does not accept ${key}.`);
221
+ throw new Error(`${label} legend does not accept ${key}.`);
190
222
  }
191
223
  }
192
224
  const count = args.count ?? previous.count;
193
225
  if (!Number.isInteger(count) || count < 2) {
194
226
  throw new RangeError(
195
- "Stroke-width legend count must be an integer of at least 2."
227
+ `${label} legend count must be an integer of at least 2.`
196
228
  );
197
229
  }
198
- validateGeneratedItemLimit(count, "Stroke-width legend count");
230
+ validateGeneratedItemLimit(count, `${label} legend count`);
199
231
  const layer = findLayer(program, previous.target);
200
232
  const titleMode = args.title;
201
233
  const inferredTitle = titleMode === "auto"
@@ -205,7 +237,7 @@ function editStrokeWidth(program, previous, args) {
205
237
  ? false
206
238
  : titleMode === undefined ? previous.titleVisible !== false : true;
207
239
  const title = titleMode === "auto"
208
- ? layer?.encoding?.strokeWidth?.field
240
+ ? layer?.encoding?.[kind]?.field
209
241
  : typeof titleMode === "string" ? titleMode : previous.title;
210
242
  const config = {
211
243
  ...previous,
@@ -213,36 +245,60 @@ function editStrokeWidth(program, previous, args) {
213
245
  inferredTitle,
214
246
  titleVisible,
215
247
  count,
248
+ ...(size ? { inheritAppearance: false } : {}),
216
249
  labels: normalizeLegendTextOptions(
217
250
  args.labels,
218
251
  "editLegend.labels",
219
- previous.labels ?? STROKE_WIDTH_LEGEND_LABELS
252
+ previous.labels ?? (size ? SIZE_LEGEND_LABELS : STROKE_WIDTH_LEGEND_LABELS)
220
253
  ),
221
- titleStyle: normalizeLegendTextOptions(
254
+ titleStyle: normalizeLegendTitleOptions(
222
255
  args.titleStyle,
223
256
  "editLegend.titleStyle",
224
- previous.titleStyle ?? STROKE_WIDTH_LEGEND_TITLE_STYLE
257
+ previous.titleStyle ?? (size ? SIZE_LEGEND_TITLE_STYLE : STROKE_WIDTH_LEGEND_TITLE_STYLE)
225
258
  )
226
259
  };
260
+ Object.assign(config, normalizeItemLegendLayout({ ...previous, ...args,
261
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
262
+ ? undefined : previous.direction) }));
263
+ config.border = normalizeLegendBorder(mergeBorder(previous.border, args.border));
264
+ (size ? resolveSizeLegendLayout : resolveStrokeWidthLegendLayout)(program, config);
265
+ return { config, titleMode, title, titleVisible };
266
+ }
267
+
268
+ function editSampledLegend(program, kind, previous, args) {
269
+ const { config, titleMode, title, titleVisible } = resolveSampledLegendEdit(program, kind, previous, args);
270
+ const prefix = kind === "size" ? "sizeLegend" : "strokeWidthLegend";
227
271
  let next = program;
228
272
  if (titleMode === "auto" || typeof titleMode === "string") {
229
273
  next = next.editSemantic({
230
- property: "guide.legend.strokeWidth.title",
274
+ property: `guide.legend.${kind}.title`,
231
275
  value: title
232
276
  });
233
277
  }
234
- next = next._withLegendConfig("strokeWidth", config);
235
- next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
236
- type: "text"
278
+ next = next._withLegendConfig(kind, config);
279
+ next = reconcileGraphic(next, `${prefix}Background`, config.border !== false, {
280
+ type: "rect", before: `${prefix}Symbols`
281
+ });
282
+ next = reconcileGraphic(next, `${prefix}Title`, titleVisible, {
283
+ type: "text", after: `${prefix}Labels`
237
284
  });
238
285
  return next.rematerializeLegend();
239
286
  }
240
287
 
241
- function categoricalSymbolIds(config) {
242
- return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
288
+ function resolveCompanionSizeEdit(previous, size, args) {
289
+ if (size === undefined) return undefined;
290
+ const config = { ...size, count: args.count ?? size.count };
291
+ if (args.labels === undefined && args.titleStyle === undefined) return config;
292
+ const labels = size.inheritAppearance
293
+ ? { ...previous.labels, offset: size.labels.offset } : size.labels ?? SIZE_LEGEND_LABELS;
294
+ const titleStyle = size.inheritAppearance
295
+ ? previous.titleStyle : size.titleStyle ?? SIZE_LEGEND_TITLE_STYLE;
296
+ return { ...config, inheritAppearance: false,
297
+ labels: normalizeLegendTextOptions(args.labels, "editLegend.labels", labels),
298
+ titleStyle: normalizeLegendTitleOptions(args.titleStyle, "editLegend.titleStyle", titleStyle) };
243
299
  }
244
300
 
245
- function editCategorical(program, kind, previous, size, args) {
301
+ function resolveCategoricalEdit(program, kind, previous, size, args, storedOrder = program.semanticSpec.guides.legend?.[kind]?.order) {
246
302
  if (args.gradient !== undefined) {
247
303
  throw new Error("Categorical legends do not accept gradient.");
248
304
  }
@@ -258,21 +314,27 @@ function editCategorical(program, kind, previous, size, args) {
258
314
  const title = titleMode === "auto"
259
315
  ? previous.field
260
316
  : typeof titleMode === "string" ? titleMode : previous.title;
317
+ const mergedLabels = mergeObject(previous.labels, args.labels);
318
+ if (mergedLabels.format !== undefined && mergedLabels.format !== "auto" && size === undefined) {
319
+ throw new Error("Categorical legend labels do not accept format.");
320
+ }
321
+ const { format: _sizeFormat, ...categoricalLabels } = mergedLabels;
261
322
  const normalized = normalizeOptions({
262
323
  target: previous.target,
263
324
  channels: previous.channels,
325
+ layout: args.layout === undefined ? previous.layout : args.layout,
264
326
  position: args.position ?? previous.position,
265
327
  align: args.align ?? previous.align,
266
- direction: args.direction ?? (args.position === "left"
267
- ? "vertical"
268
- : previous.direction),
328
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
329
+ ? undefined : previous.direction),
269
330
  ...(args.columns === undefined && previous.columns === undefined
270
331
  ? {} : { columns: args.columns ?? previous.columns }),
271
332
  offset: args.offset ?? previous.offset,
272
333
  titlePosition: args.titlePosition ?? previous.titlePosition,
273
334
  title,
274
- symbol: args.symbol ?? previous.symbol,
275
- labels: mergeObject(previous.labels, args.labels),
335
+ symbol: args.symbol === undefined ? previous.symbol
336
+ : resolveLegendSymbol(program, findLayer(program, previous.target), previous.channels, args.symbol),
337
+ labels: categoricalLabels,
276
338
  titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
277
339
  itemGap: args.itemGap ?? previous.itemGap,
278
340
  border: mergeBorder(previous.border, args.border)
@@ -281,9 +343,25 @@ function editCategorical(program, kind, previous, size, args) {
281
343
  ...previous,
282
344
  ...normalized,
283
345
  inferredTitle,
346
+ inferredSymbol: args.symbol === undefined ? previous.inferredSymbol : args.symbol === "auto",
284
347
  titleVisible
285
348
  };
286
- const oldSymbols = categoricalSymbolIds(previous);
349
+ const order = args.order === undefined
350
+ ? storedOrder
351
+ : normalizeLegendOrder(args.order);
352
+ // Resolve the final domain before changing semantic/config/graphic state.
353
+ resolveDefinition(program, findLayer(program, previous.target), previous.channels, title, order);
354
+ if (args.count !== undefined) {
355
+ if (!Number.isInteger(args.count) || args.count < 2) {
356
+ throw new RangeError("Size legend count must be an integer of at least 2.");
357
+ }
358
+ validateGeneratedItemLimit(args.count, "Size legend count");
359
+ }
360
+ return { config, order, titleMode, title, titleVisible, sizeConfig: resolveCompanionSizeEdit(previous, size, args) };
361
+ }
362
+
363
+ function editCategorical(program, kind, previous, size, args) {
364
+ const { config, order, titleMode, title, titleVisible, sizeConfig } = resolveCategoricalEdit(program, kind, previous, size, args);
287
365
  const newSymbols = categoricalSymbolIds(config);
288
366
  let next = program;
289
367
  if (titleMode === "auto" || typeof titleMode === "string") {
@@ -292,49 +370,99 @@ function editCategorical(program, kind, previous, size, args) {
292
370
  value: title
293
371
  });
294
372
  }
295
- next = next._withLegendConfig(kind, config);
296
- for (const id of oldSymbols) {
297
- if (!newSymbols.has(id) && next.graphicSpec.objects[id] !== undefined) {
298
- next = next.editGraphics({ target: id, remove: true });
373
+ if (args.order !== undefined) {
374
+ if (order === "scale") {
375
+ if (program.semanticSpec.guides.legend?.[kind]?.order !== undefined) {
376
+ next = next.editSemantic({ property: `guide.legend.${kind}.order`, remove: true });
377
+ }
378
+ } else {
379
+ next = next.editSemantic({ property: `guide.legend.${kind}.order`, value: order });
299
380
  }
300
381
  }
382
+ next = next._withLegendConfig(kind, config);
383
+ next = reconcileCategoricalSymbols(next, previous, config);
301
384
  next = reconcileGraphic(next, `${kind === "series" ? "series" : "color"}LegendBackground`,
302
385
  config.border !== false, {
303
386
  type: "rect",
304
387
  before: [...newSymbols][0]
305
388
  });
306
- for (const layer of config.symbol.layers) {
307
- const id = symbolGraphic(config, layer.type);
308
- if (next.graphicSpec.objects[id] === undefined) {
309
- const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[layer.type];
310
- next = next[`createLegendSymbol${suffix}`]();
311
- }
312
- }
313
389
  const titleId = `${kind === "series" ? "series" : "color"}LegendTitle`;
314
390
  next = reconcileGraphic(next, titleId, titleVisible, {
315
391
  type: "text",
316
392
  ...(next.graphicSpec.objects.sizeLegendSymbols === undefined
317
- ? {} : { before: "sizeLegendSymbols" })
393
+ ? {} : { before: next.graphicSpec.objects.sizeLegendBackground === undefined
394
+ ? "sizeLegendSymbols" : "sizeLegendBackground" })
318
395
  });
319
396
  if (size !== undefined) {
320
- if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 2)) {
321
- throw new RangeError("Size legend count must be an integer of at least 2.");
397
+ next = next._withLegendConfig("size", sizeConfig);
398
+ }
399
+ return next.rematerializeLegend();
400
+ }
401
+
402
+
403
+ function editLegendContent(program, target, args) {
404
+ validateLegendChannels(args.channels, "editLegend");
405
+ const { channels, target: _target, ...patch } = args;
406
+ const previous = program.guideConfigs.legend ?? {};
407
+ const removed = Object.keys(previous).filter(kind => previous[kind].target === target);
408
+ let view = removed.reduce((next, kind) => next._withoutMaterializationConfig(["guides", "legend", kind]), program);
409
+ const creation = resolveLegendCreationPlan(view, { target, channels });
410
+ const oldCategorical = removed.find(kind => ["series", "color"].includes(kind));
411
+ const plans = creation.steps.map(step => {
412
+ const descriptor = resolveLegendStepConfig(view, step);
413
+ const { kind } = descriptor;
414
+ const otherCategorical = ["series", "color"].some(key => previous[key] !== undefined && previous[key].target !== target);
415
+ if (previous[kind] !== undefined && previous[kind].target !== target ||
416
+ ["series", "color"].includes(kind) && otherCategorical) {
417
+ throw new Error(`Legend ${kind} content already belongs to another target.`);
322
418
  }
323
- if (args.count !== undefined) {
324
- validateGeneratedItemLimit(args.count, "Size legend count");
419
+ if (["series", "color"].includes(kind) && oldCategorical !== undefined) {
420
+ const order = patch.order === undefined ? program.semanticSpec.guides.legend?.[oldCategorical]?.order
421
+ : normalizeLegendOrder(patch.order);
422
+ return { ...resolveCategoricalLegendRevision(view, oldCategorical, previous[oldCategorical],
423
+ descriptor.config.channels, { order, validateLayout: false }), kind };
325
424
  }
326
- next = next._withLegendConfig("size", {
327
- ...size,
328
- count: args.count ?? size.count,
329
- inheritAppearance: true
330
- });
425
+ return { ...descriptor, config: previous[kind]?.target === target ? previous[kind] : descriptor.config };
426
+ });
427
+ const categorical = plans.find(plan => ["series", "color"].includes(plan.kind));
428
+ const size = plans.find(plan => plan.kind === "size");
429
+ if (categorical !== undefined) {
430
+ if (size !== undefined && previous.size?.target !== target) {
431
+ size.config = { ...size.config, inheritAppearance: categorical.config.position === "left" ||
432
+ patch.position === "left" || patch.labels !== undefined || patch.titleStyle !== undefined };
433
+ }
434
+ const edited = resolveCategoricalEdit(view, categorical.kind, categorical.config, size?.config, patch, categorical.order);
435
+ categorical.config = edited.config;
436
+ categorical.order = edited.order;
437
+ if (size !== undefined) {
438
+ size.config = edited.sizeConfig;
439
+ if (categorical.config.layout === "legacy-bottom") {
440
+ throw new Error('Combined size legends require layout "edge".');
441
+ }
442
+ }
443
+ } else {
444
+ const plan = plans[0];
445
+ if (plan.kind === "size" || plan.kind === "strokeWidth") {
446
+ plan.config = resolveSampledLegendEdit(view, plan.kind, plan.config, patch).config;
447
+ } else if (plan.kind === "interval") plan.config = resolveIntervalEdit(view, plan.config, patch).config;
448
+ else plan.config = resolveContinuousEdit(view, plan.kind, plan.config, patch).config;
449
+ }
450
+ // The complete final content and styles are validated before removing resources.
451
+ for (const { kind, config } of plans) view = view._withLegendConfig(kind, config);
452
+ if (categorical !== undefined) resolveLayout(view, categorical.config);
453
+ let next = removeLegendKinds(program, removed);
454
+ for (const { kind, config, order } of plans) {
455
+ if (["series", "color"].includes(kind)) next = createCategoricalLegendFromConfig(next, config, order);
456
+ else next = ({ size: createSizeLegendFromConfig, strokeWidth: createStrokeWidthLegendFromConfig,
457
+ gradient: createGradientLegendFromConfig, opacity: createOpacityLegendFromConfig,
458
+ interval: createIntervalLegendFromConfig })[kind](next, config);
331
459
  }
332
460
  return next.rematerializeLegend();
333
461
  }
334
462
 
335
463
  export const editLegend = action(
336
- { op: "editLegend", description: "Edit one stable legend layout or appearance." },
337
- function (args = {}) {
464
+ { op: "editLegend", description: "Edit one stable legend content, layout or appearance." },
465
+ withGuideLayoutValidation(function (args = {}) {
338
466
  validateOptionObject(args, OPTIONS, "editLegend");
339
467
  const changes = Object.keys(args).filter(key => key !== "target");
340
468
  if (changes.length === 0) {
@@ -346,6 +474,7 @@ export const editLegend = action(
346
474
  throw new TypeError('editLegend title must be a non-empty string, "auto", or false.');
347
475
  }
348
476
  const target = resolveLegendTarget(this, args.target, "editLegend");
477
+ if (args.channels !== undefined) return editLegendContent(this, target, args);
349
478
  const configs = this.guideConfigs.legend ?? {};
350
479
  const categoricalKind = ["series", "color"].find(
351
480
  kind => configs[kind]?.target === target
@@ -363,11 +492,14 @@ export const editLegend = action(
363
492
  return editInterval(this, configs.interval, args);
364
493
  }
365
494
  if (configs.strokeWidth?.target === target) {
366
- return editStrokeWidth(this, configs.strokeWidth, args);
495
+ return editSampledLegend(this, "strokeWidth", configs.strokeWidth, args);
496
+ }
497
+ if (configs.size?.target === target) {
498
+ return editSampledLegend(this, "size", configs.size, args);
367
499
  }
368
500
  const continuousKind = ["gradient", "opacity"].find(
369
501
  kind => configs[kind]?.target === target
370
502
  );
371
503
  return editContinuous(this, continuousKind, configs[continuousKind], args);
372
- }
504
+ })
373
505
  );
@@ -20,7 +20,7 @@ function makeFocusedLegendEdit({ name, options, map }) {
20
20
  const editLegendLayout = makeFocusedLegendEdit({
21
21
  name: "editLegendLayout",
22
22
  options: [
23
- "position", "align", "direction", "columns", "offset",
23
+ "position", "layout", "align", "direction", "columns", "offset",
24
24
  "titlePosition", "itemGap"
25
25
  ],
26
26
  map: args => args
@@ -28,7 +28,7 @@ const editLegendLayout = makeFocusedLegendEdit({
28
28
 
29
29
  const editLegendLabels = makeFocusedLegendEdit({
30
30
  name: "editLegendLabels",
31
- options: ["color", "fontSize", "fontFamily", "fontWeight"],
31
+ options: ["offset", "color", "fontSize", "fontFamily", "fontWeight", "format"],
32
32
  map: ({ target, ...labels }) => ({ target, labels })
33
33
  });
34
34