ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -26,6 +26,11 @@ import {
26
26
  resolveTarget,
27
27
  sameValues
28
28
  } from "./resolve.js";
29
+ import { readLegendSampling } from "../sampling.js";
30
+ import {
31
+ reconcileLegendBlockTitleGraphic,
32
+ resolveEffectiveLegendBlockConfig
33
+ } from "../blocks.js";
29
34
 
30
35
  function finishLegend(program) {
31
36
  return hasLegendLane(program) ? program.rematerializeLegend() : program;
@@ -50,25 +55,35 @@ export const rematerializeLegend = action(
50
55
  withGuideLayoutValidation(function (args = {}) {
51
56
  noOptions(args, "rematerializeLegend");
52
57
  let next = this;
53
- const hasCategorical =
54
- this.guideConfigs.legend?.series !== undefined ||
55
- this.guideConfigs.legend?.color !== undefined;
56
- if (hasCategorical) {
57
- const { kind, config } = activeConfig(this);
58
- const definition = resolveCurrentDefinition(this, config);
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);
58
+ const categoricalKinds = ["series", "color", "stroke"].filter(
59
+ kind => this.guideConfigs.legend?.[kind] !== undefined
60
+ );
61
+ for (const kind of categoricalKinds) {
62
+ const stored = next.guideConfigs.legend[kind];
63
+ const definition = resolveCurrentDefinition(next, stored);
64
+ const symbol = stored.inferredSymbol
65
+ ? normalizeRecipe(
66
+ resolveLegendSymbol(
67
+ next,
68
+ findLayer(next, stored.target),
69
+ definition.channels,
70
+ undefined,
71
+ kind
72
+ ),
73
+ kind,
74
+ { internal: true }
75
+ )
76
+ : stored.symbol;
77
+ const symbolChanged = !sameGuideValue(symbol, stored.symbol);
63
78
  const changed = symbolChanged ||
64
- !sameValues(config.channels, definition.channels) ||
65
- !sameValues(config.domain, definition.domain) ||
66
- !sameValues(config.scales, definition.scales) ||
67
- config.field !== definition.field ||
68
- config.title !== definition.title;
79
+ !sameValues(stored.channels, definition.channels) ||
80
+ !sameValues(stored.domain, definition.domain) ||
81
+ !sameValues(stored.scales, definition.scales) ||
82
+ stored.field !== definition.field ||
83
+ stored.title !== definition.title;
69
84
  next = changed
70
- ? this._withLegendConfig(kind, {
71
- ...config,
85
+ ? next._withLegendConfig(kind, {
86
+ ...stored,
72
87
  symbol,
73
88
  channels: definition.channels,
74
89
  scales: definition.scales,
@@ -76,10 +91,17 @@ export const rematerializeLegend = action(
76
91
  title: definition.title,
77
92
  domain: definition.domain
78
93
  })
79
- : this;
94
+ : next;
95
+ const config = resolveEffectiveLegendBlockConfig(
96
+ next,
97
+ kind,
98
+ next.guideConfigs.legend[kind]
99
+ );
100
+ next = reconcileLegendBlockTitleGraphic(next, kind,
101
+ config.titleVisible !== false);
80
102
  if (kind === "series") {
81
103
  if (!sameValues(
82
- this.semanticSpec.guides.legend.series.scales,
104
+ next.semanticSpec.guides.legend.series.scales,
83
105
  definition.scales
84
106
  )) {
85
107
  next = next.editSemantic({
@@ -87,45 +109,56 @@ export const rematerializeLegend = action(
87
109
  value: definition.scales
88
110
  });
89
111
  }
90
- if (this.semanticSpec.guides.legend.series.title !== definition.title) {
112
+ if (next.semanticSpec.guides.legend.series.title !== definition.title) {
91
113
  next = next.editSemantic({
92
114
  property: "guide.legend.series.title",
93
115
  value: definition.title
94
116
  });
95
117
  }
96
118
  } else {
97
- if (this.semanticSpec.guides.legend.color.scale !== definition.scales[0]) {
119
+ if (next.semanticSpec.guides.legend[kind].scale !== definition.scales[0]) {
98
120
  next = next.editSemantic({
99
- property: "guide.legend.color.scale",
121
+ property: `guide.legend.${kind}.scale`,
100
122
  value: definition.scales[0]
101
123
  });
102
124
  }
103
- if (this.semanticSpec.guides.legend.color.title !== definition.title) {
125
+ if (next.semanticSpec.guides.legend[kind].title !== definition.title) {
104
126
  next = next.editSemantic({
105
- property: "guide.legend.color.title",
127
+ property: `guide.legend.${kind}.title`,
106
128
  value: definition.title
107
129
  });
108
130
  }
109
131
  }
110
132
  if (symbolChanged) {
111
- resolveLayout(next, next.guideConfigs.legend[kind]);
112
- next = reconcileCategoricalSymbols(next, config, next.guideConfigs.legend[kind]);
133
+ resolveLayout(next, config);
134
+ next = reconcileCategoricalSymbols(next, stored, config);
135
+ }
136
+ if (config.border !== false) {
137
+ next = next.rematerializeLegendBackground({ kind });
113
138
  }
114
- if (config.border !== false) next = next.rematerializeLegendBackground();
115
139
  next = next
116
- .rematerializeLegendSymbols()
117
- .rematerializeLegendLabels();
118
- if (config.titleVisible !== false) next = next.rematerializeLegendTitle();
140
+ .rematerializeLegendSymbols({ kind })
141
+ .rematerializeLegendLabels({ kind });
142
+ if (config.titleVisible !== false) {
143
+ next = next.rematerializeLegendTitle({ kind });
144
+ }
119
145
  const hasHighlight = Object.values(
120
146
  next.materializationConfigs.highlights ?? {}
121
147
  ).some(highlight => highlight.target === config.target);
122
- if (hasHighlight) next = next.rematerializeLegendHighlights();
148
+ if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
123
149
  }
124
150
  for (const policy of legendResourcePolicies()) {
125
151
  if (
126
152
  policy.rematerializeOp !== undefined &&
127
153
  this.guideConfigs.legend?.[policy.kind] !== undefined
128
154
  ) {
155
+ const config = resolveEffectiveLegendBlockConfig(
156
+ next,
157
+ policy.kind,
158
+ next.guideConfigs.legend[policy.kind]
159
+ );
160
+ next = reconcileLegendBlockTitleGraphic(next, policy.kind,
161
+ config.titleVisible !== false);
129
162
  next = next[policy.rematerializeOp]();
130
163
  }
131
164
  }
@@ -148,14 +181,21 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
148
181
  args.title,
149
182
  args.order === undefined ? undefined : normalizeLegendOrder(args.order)
150
183
  );
184
+ const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
151
185
  const options = normalizeOptions({ ...args,
152
- symbol: resolveLegendSymbol(program, layer, definition.channels, args.symbol)
153
- }, definition.kind);
186
+ symbol: resolveLegendSymbol(
187
+ program,
188
+ layer,
189
+ definition.channels,
190
+ args.symbol,
191
+ definition.kind
192
+ )
193
+ }, definition.kind, { internalSymbol: inferredSymbol });
154
194
  const config = {
155
195
  target: layer.id,
156
196
  ...definition,
157
197
  inferredTitle: !Object.hasOwn(args, "title"),
158
- inferredSymbol: args.symbol === undefined || args.symbol === "auto",
198
+ inferredSymbol,
159
199
  position: options.position,
160
200
  align: options.align,
161
201
  direction: options.direction,
@@ -177,11 +217,10 @@ export const createCategoricalLegend = action(
177
217
  { op: "createCategoricalLegend", description: "Create one categorical legend block." },
178
218
  withGuideLayoutValidation(function (args = {}) {
179
219
  const config = resolveCategoricalLegendConfig(this, args);
180
- if (
181
- this.semanticSpec.guides.legend?.series !== undefined ||
182
- this.semanticSpec.guides.legend?.color !== undefined
183
- ) {
184
- throw new Error("createCategoricalLegend requires a missing legend.");
220
+ if (this.semanticSpec.guides.legend?.[config.kind] !== undefined) {
221
+ throw new Error(
222
+ `createCategoricalLegend requires a missing legend for ${config.kind}.`
223
+ );
185
224
  }
186
225
  resolveLayout(this, config);
187
226
  return createCategoricalLegendFromConfig(this, config,
@@ -195,12 +234,12 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
195
234
  const channels = args.channels;
196
235
  if (channels !== undefined) validateLegendChannels(channels, "createLegend");
197
236
  const standalone = [
198
- ["size", isSizeLegendPoint, ["color", "shape", "strokeDash", "opacity"]],
237
+ ["size", isSizeLegendPoint, ["color", "stroke", "shape", "strokeDash", "opacity"]],
199
238
  ["strokeWidth", isStrokeWidthLegendLayer, args.target === undefined
200
- ? ["color", "shape", "strokeDash", "size", "opacity"]
201
- : ["color", "shape", "strokeDash", "opacity"]],
239
+ ? ["color", "stroke", "shape", "strokeDash", "size", "opacity"]
240
+ : ["color", "stroke", "shape", "strokeDash", "opacity"]],
202
241
  ["opacity", isOpacityLegendLayer,
203
- ["color", "shape", "strokeDash", "size"]]
242
+ ["color", "stroke", "shape", "strokeDash", "size"]]
204
243
  ];
205
244
  for (const [kind, eligible, otherChannels] of standalone) {
206
245
  const explicit = channels?.length === 1 && channels[0] === kind;
@@ -240,6 +279,47 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
240
279
  ) {
241
280
  return { steps: [{ op: "createIntervalLegend", args }], finish: "auto" };
242
281
  }
282
+ const continuousStrokeCandidates = candidates.filter(layer => {
283
+ const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
284
+ return scale?.type === "sequential";
285
+ });
286
+ const continuousStroke = requestedCandidate(
287
+ program,
288
+ args.target,
289
+ continuousStrokeCandidates
290
+ );
291
+ if (
292
+ (channels?.length === 1 && channels[0] === "stroke" && continuousStroke) ||
293
+ (channels === undefined && continuousStroke &&
294
+ !["color", "shape", "strokeDash", "size", "opacity", "strokeWidth"].some(
295
+ channel => continuousStroke.encoding?.[channel]?.scale !== undefined
296
+ ))
297
+ ) {
298
+ return {
299
+ steps: [{ op: "createStrokeGradientLegend", args: {
300
+ ...args,
301
+ channels: ["stroke"]
302
+ } }],
303
+ finish: "auto"
304
+ };
305
+ }
306
+ const intervalStrokeCandidates = candidates.filter(layer => {
307
+ const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
308
+ return ["quantize", "quantile", "threshold"].includes(scale?.type);
309
+ });
310
+ if (
311
+ (channels?.length === 1 && channels[0] === "stroke" &&
312
+ intervalStrokeCandidates.length > 0) ||
313
+ (channels === undefined && intervalStrokeCandidates.length === 1)
314
+ ) {
315
+ return {
316
+ steps: [{ op: "createStrokeIntervalLegend", args: {
317
+ ...args,
318
+ channels: ["stroke"]
319
+ } }],
320
+ finish: "auto"
321
+ };
322
+ }
243
323
  const wantsShape = channels?.includes("shape") === true;
244
324
  const wantsSize = channels?.includes("size") === true;
245
325
  const pointCandidates = candidates.filter(layer =>
@@ -247,7 +327,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
247
327
  (channels === undefined
248
328
  ? ["color", "shape"].some(channel => layer.encoding?.[channel]?.scale !== undefined)
249
329
  : (wantsShape || wantsSize) && channels.every(channel =>
250
- ["color", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
330
+ ["color", "stroke", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
251
331
  );
252
332
  const requestedPoint = requestedCandidate(program, args.target, pointCandidates);
253
333
  const inferredSize = channels === undefined && pointCandidates.some(isSizeLegendPoint);
@@ -255,6 +335,11 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
255
335
  throw new Error("Combined size legend requires one eligible point mark or an explicit target.");
256
336
  }
257
337
  if (requestedPoint !== undefined) {
338
+ if (Object.hasOwn(args, "values")) {
339
+ throw new Error(
340
+ "Combined and categorical legends require a channel block selector for exact values."
341
+ );
342
+ }
258
343
  const { count, ...categoricalArgs } = args;
259
344
  const combined = requestedPoint.encoding?.size?.scale !== undefined &&
260
345
  (channels === undefined || wantsSize);
@@ -264,7 +349,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
264
349
  if (combined && categoricalArgs.layout === "legacy-bottom") {
265
350
  throw new Error('Combined size legends require layout "edge".');
266
351
  }
267
- const inferredChannels = ["color", "shape"].filter(
352
+ const inferredChannels = ["color", "stroke", "shape"].filter(
268
353
  channel => requestedPoint.encoding?.[channel]?.scale !== undefined
269
354
  );
270
355
  const { format: sizeFormat, ...categoricalLabels } = categoricalArgs.labels ?? {};
@@ -294,7 +379,8 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
294
379
  if (existing !== undefined && existing.target !== size.target) {
295
380
  throw new Error("Combined point series legend requires the active size legend to share its target.");
296
381
  }
297
- if (existing !== undefined && count !== undefined && existing.count !== count) {
382
+ if (existing !== undefined && count !== undefined &&
383
+ readLegendSampling(existing).count !== count) {
298
384
  throw new Error("Existing size legend count must be edited before recreating the categorical block.");
299
385
  }
300
386
  }
@@ -331,14 +417,14 @@ export const removeCategoricalLegend = action(
331
417
  },
332
418
  withGuideLayoutValidation(function (args = {}) {
333
419
  noOptions(args, "removeCategoricalLegend");
334
- const entries = ["series", "color"]
420
+ const entries = ["series", "color", "stroke"]
335
421
  .filter(kind => this.guideConfigs.legend?.[kind] !== undefined);
336
422
  if (entries.length === 0) return this;
337
423
  if (entries.length !== 1) {
338
424
  throw new Error("removeCategoricalLegend requires one active categorical legend.");
339
425
  }
340
426
  const kind = entries[0];
341
- const prefix = kind === "series" ? "seriesLegend" : "colorLegend";
427
+ const prefix = `${kind}Legend`;
342
428
  const targets = Object.keys(this.graphicSpec.objects)
343
429
  .filter(id => id.startsWith(prefix));
344
430
  let next = this.editSemantic({
@@ -1,13 +1,18 @@
1
1
  import { action } from "../../../../core/action.js";
2
- import { formatVisibleText } from "../../../../core/textMetrics.js";
3
- import { activeConfig, graphic, noOptions, resolveLayout } from "./layout.js";
2
+ import {
3
+ activeConfig,
4
+ categoricalLegendLabels,
5
+ graphic,
6
+ noOptions,
7
+ resolveLayout
8
+ } from "./layout.js";
4
9
  import { resolveCategoricalLegendPlacement } from "../lifecycle.js";
5
10
 
6
11
  export const rematerializeLegendLabels = action(
7
12
  { op: "rematerializeLegendLabels", description: "Rematerialize categorical legend labels." },
8
13
  function (args = {}) {
9
14
  noOptions(args, "rematerializeLegendLabels");
10
- const { config } = activeConfig(this);
15
+ const { config } = activeConfig(this, args.kind);
11
16
  const id = graphic(config, "Labels");
12
17
  if (this.graphicSpec.objects[id]?.type !== "text") {
13
18
  throw new Error("rematerializeLegendLabels requires existing legend labels.");
@@ -20,7 +25,7 @@ export const rematerializeLegendLabels = action(
20
25
  .editGraphics({
21
26
  target: id,
22
27
  property: "text",
23
- value: config.domain.map(formatVisibleText)
28
+ value: categoricalLegendLabels(config)
24
29
  })
25
30
  .editGraphics({ target: id, property: "fill", value: config.labels.color })
26
31
  .editGraphics({ target: id, property: "fontSize", value: config.labels.fontSize })
@@ -35,7 +40,7 @@ export const createLegendLabels = action(
35
40
  { op: "createLegendLabels", description: "Create categorical legend labels." },
36
41
  function (args = {}) {
37
42
  noOptions(args, "createLegendLabels");
38
- const { config } = activeConfig(this);
43
+ const { config } = activeConfig(this, args.kind);
39
44
  const id = graphic(config, "Labels");
40
45
  if (this.graphicSpec.objects[id] !== undefined) {
41
46
  throw new Error("createLegendLabels requires missing legend labels.");
@@ -47,7 +52,7 @@ export const createLegendLabels = action(
47
52
  length: config.domain.length,
48
53
  ...resolveCategoricalLegendPlacement(this)
49
54
  })
50
- .rematerializeLegendLabels();
55
+ .rematerializeLegendLabels({ kind: config.kind });
51
56
  }
52
57
  );
53
58
 
@@ -55,7 +60,7 @@ export const rematerializeLegendTitle = action(
55
60
  { op: "rematerializeLegendTitle", description: "Rematerialize the categorical legend title." },
56
61
  function (args = {}) {
57
62
  noOptions(args, "rematerializeLegendTitle");
58
- const { config } = activeConfig(this);
63
+ const { config } = activeConfig(this, args.kind);
59
64
  const id = graphic(config, "Title");
60
65
  if (this.graphicSpec.objects[id]?.type !== "text") {
61
66
  throw new Error("rematerializeLegendTitle requires an existing legend title.");
@@ -85,7 +90,7 @@ export const createLegendTitle = action(
85
90
  { op: "createLegendTitle", description: "Create the categorical legend title." },
86
91
  function (args = {}) {
87
92
  noOptions(args, "createLegendTitle");
88
- const { config } = activeConfig(this);
93
+ const { config } = activeConfig(this, args.kind);
89
94
  const id = graphic(config, "Title");
90
95
  if (this.graphicSpec.objects[id] !== undefined) {
91
96
  throw new Error("createLegendTitle requires a missing legend title.");
@@ -96,7 +101,7 @@ export const createLegendTitle = action(
96
101
  type: "text",
97
102
  ...resolveCategoricalLegendPlacement(this)
98
103
  })
99
- .rematerializeLegendTitle();
104
+ .rematerializeLegendTitle({ kind: config.kind });
100
105
  }
101
106
  );
102
107
 
@@ -104,7 +109,7 @@ export const rematerializeLegendBackground = action(
104
109
  { op: "rematerializeLegendBackground", description: "Rematerialize the legend background." },
105
110
  function (args = {}) {
106
111
  noOptions(args, "rematerializeLegendBackground");
107
- const { config } = activeConfig(this);
112
+ const { config } = activeConfig(this, args.kind);
108
113
  if (config.border === false) {
109
114
  throw new Error("rematerializeLegendBackground requires border configuration.");
110
115
  }
@@ -128,7 +133,7 @@ export const createLegendBackground = action(
128
133
  { op: "createLegendBackground", description: "Create the legend background rect." },
129
134
  function (args = {}) {
130
135
  noOptions(args, "createLegendBackground");
131
- const { config } = activeConfig(this);
136
+ const { config } = activeConfig(this, args.kind);
132
137
  const id = graphic(config, "Background");
133
138
  if (this.graphicSpec.objects[id] !== undefined) {
134
139
  throw new Error("createLegendBackground requires a missing background.");
@@ -139,6 +144,6 @@ export const createLegendBackground = action(
139
144
  type: "rect",
140
145
  ...resolveCategoricalLegendPlacement(this)
141
146
  })
142
- .rematerializeLegendBackground();
147
+ .rematerializeLegendBackground({ kind: config.kind });
143
148
  }
144
149
  );
@@ -68,40 +68,44 @@ function createLegendHighlightAction() {
68
68
  },
69
69
  function (args = {}) {
70
70
  noOptions(args, "rematerializeLegendHighlights");
71
- const hasCategorical =
72
- this.guideConfigs.legend?.series !== undefined ||
73
- this.guideConfigs.legend?.color !== undefined;
74
- if (!hasCategorical) return this;
75
- const { config } = activeConfig(this);
76
- const highlights = Object.values(
77
- this.materializationConfigs.highlights ?? {}
78
- ).map(highlight => ({
79
- highlight,
80
- states: exactLegendSelection(this, config, highlight)
81
- })).filter(entry => entry.states !== undefined);
82
- if (highlights.length === 0) return this;
71
+ const kinds = args.kind === undefined
72
+ ? ["series", "color", "stroke"].filter(
73
+ kind => this.guideConfigs.legend?.[kind] !== undefined
74
+ )
75
+ : [args.kind];
76
+ let next = this;
77
+ for (const kind of kinds) {
78
+ const { config } = activeConfig(next, kind);
79
+ const highlights = Object.values(
80
+ next.materializationConfigs.highlights ?? {}
81
+ ).map(highlight => ({
82
+ highlight,
83
+ states: exactLegendSelection(next, config, highlight)
84
+ })).filter(entry => entry.states !== undefined);
85
+ if (highlights.length === 0) continue;
83
86
 
84
- let next = this.rematerializeLegendSymbols();
85
- for (const { highlight, states } of highlights) {
86
- for (const layer of config.symbol.layers) {
87
- const id = symbolGraphic(config, layer.type);
88
- const graphic = next.graphicSpec.objects[id];
89
- const selectedStyle = legendLayerStyle(layer, highlight.style);
90
- const dimOpacity = highlight.dimOthers === false
91
- ? undefined
92
- : highlight.dimOthers.opacity;
93
- next = next.editGraphics({
94
- target: id,
95
- property: "items",
96
- value: graphic.items.map((child, index) => ({
97
- type: child.type ?? graphic.type,
98
- properties: states[index]
99
- ? { ...child.properties, ...selectedStyle }
100
- : dimOpacity === undefined
101
- ? child.properties
102
- : { ...child.properties, opacity: dimOpacity }
103
- }))
104
- });
87
+ next = next.rematerializeLegendSymbols({ kind: config.kind });
88
+ for (const { highlight, states } of highlights) {
89
+ for (const layer of config.symbol.layers) {
90
+ const id = symbolGraphic(config, layer.type);
91
+ const graphic = next.graphicSpec.objects[id];
92
+ const selectedStyle = legendLayerStyle(layer, highlight.style);
93
+ const dimOpacity = highlight.dimOthers === false
94
+ ? undefined
95
+ : highlight.dimOthers.opacity;
96
+ next = next.editGraphics({
97
+ target: id,
98
+ property: "items",
99
+ value: graphic.items.map((child, index) => ({
100
+ type: child.type ?? graphic.type,
101
+ properties: states[index]
102
+ ? { ...child.properties, ...selectedStyle }
103
+ : dimOpacity === undefined
104
+ ? child.properties
105
+ : { ...child.properties, opacity: dimOpacity }
106
+ }))
107
+ });
108
+ }
105
109
  }
106
110
  }
107
111
  return next;