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
@@ -10,7 +10,7 @@ import {
10
10
  isTransformedScaleType,
11
11
  mapContinuousScaleValues
12
12
  } from "../../../grammar/scales/index.js";
13
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
13
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
14
14
  import { resolveGridLineGeometry } from "../../../layout/grid.js";
15
15
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
16
16
  import { findCoordinate } from "../../../selectors/coordinates.js";
@@ -248,7 +248,12 @@ export function editGridConfig(previous, args) {
248
248
 
249
249
  export function resolveGridGeometry(program, config) {
250
250
  const scale = program.resolvedScales[config.scale];
251
- const bounds = resolveGraphicBounds(program);
251
+ const channel = config.direction === "horizontal" ? "y" : "x";
252
+ const bounds = resolveGuideCoordinateBounds(program, {
253
+ coordinate: config.coordinate,
254
+ scale: config.scale,
255
+ channel
256
+ });
252
257
  if ((
253
258
  !["linear", "time"].includes(scale?.type) &&
254
259
  !isTransformedScaleType(scale?.type)
@@ -89,6 +89,13 @@ function mergeInferredOptions(inferred, explicit) {
89
89
  return merged;
90
90
  }
91
91
 
92
+ function hasStoredGuideCollection(program, collection) {
93
+ return [
94
+ program.semanticSpec.guides?.[collection],
95
+ program.guideConfigs?.[collection]
96
+ ].some(value => value !== undefined && Object.keys(value).length > 0);
97
+ }
98
+
92
99
  export function resolveGuideOptions(program, args = {}, layers = program.semanticSpec.layers) {
93
100
  validateOptions(args);
94
101
  const applicability = resolveGuideApplicability(program, layers);
@@ -128,9 +135,18 @@ const createGuides = action(
128
135
  }
129
136
 
130
137
  let next = this;
131
- if (axes !== undefined) next = next.createAxes(axes);
132
- if (grid !== undefined) next = next.createGrid(grid);
133
- if (legend !== undefined) next = next.createLegend(legend);
138
+ if (
139
+ axes !== undefined &&
140
+ !(args.axes === undefined && hasStoredGuideCollection(this, "axis"))
141
+ ) next = next.createAxes(axes);
142
+ if (
143
+ grid !== undefined &&
144
+ !(args.grid === undefined && hasStoredGuideCollection(this, "grid"))
145
+ ) next = next.createGrid(grid);
146
+ if (
147
+ legend !== undefined &&
148
+ !(args.legend === undefined && hasStoredGuideCollection(this, "legend"))
149
+ ) next = next.createLegend(legend);
134
150
  return next;
135
151
  }
136
152
  );
@@ -0,0 +1,426 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { isPlainObject } from "../../../core/immutable.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateOptionObject,
7
+ validatePositiveFinite,
8
+ validateUnitInterval
9
+ } from "../../../core/validation.js";
10
+ import { isNominalValue } from "../../../grammar/scales/fields.js";
11
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
12
+ import { legendResourcePolicy } from "../../../materialization/guides/resources.js";
13
+ import {
14
+ preserveGraphicPlacement,
15
+ resolveLegendGraphicPlacement
16
+ } from "../../../materialization/graphicHierarchy.js";
17
+ import {
18
+ normalizeLegendSampling,
19
+ validateSamplingAgainstScale
20
+ } from "./sampling.js";
21
+ import {
22
+ legendBlockKey,
23
+ resolveLegendBlock
24
+ } from "./target.js";
25
+ import { validateFontWeight } from "./categorical/validation.js";
26
+ import { normalizeDisplayLabelMap } from "../../../grammar/displayLabels.js";
27
+
28
+ const OPTIONS = Object.freeze([
29
+ "target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
30
+ "labelMap"
31
+ ]);
32
+ const TEXT_OPTIONS = Object.freeze(["fontSize", "fontFamily", "fontWeight", "color"]);
33
+ const SYMBOL_OPTIONS = Object.freeze(["size", "fill", "stroke", "strokeWidth", "opacity"]);
34
+
35
+ function validatePatchObject(value, keys, label) {
36
+ if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object.`);
37
+ validateOptionObject(value, keys, label);
38
+ }
39
+
40
+ function normalizeTextPatch(value) {
41
+ validatePatchObject(value, TEXT_OPTIONS, "editLegendBlock text");
42
+ if (value.fontSize !== undefined) {
43
+ validatePositiveFinite(value.fontSize, "editLegendBlock text.fontSize");
44
+ }
45
+ if (value.fontFamily !== undefined) {
46
+ validateNonEmptyString(value.fontFamily, "editLegendBlock text.fontFamily");
47
+ }
48
+ if (value.fontWeight !== undefined) {
49
+ validateFontWeight(value.fontWeight, "editLegendBlock text.fontWeight");
50
+ }
51
+ if (value.color !== undefined) {
52
+ validateNonEmptyString(value.color, "editLegendBlock text.color");
53
+ }
54
+ return { ...value };
55
+ }
56
+
57
+ function categoricalSupports(config, property) {
58
+ const types = new Set(config.symbol.layers.map(layer => layer.type));
59
+ if (property === "size") return types.has("point");
60
+ if (property === "fill") return types.has("point") || types.has("swatch");
61
+ return types.size > 0;
62
+ }
63
+
64
+ function validateSymbolSupport(descriptor, property) {
65
+ const { channels, family, kind, config } = descriptor;
66
+ if (family === "gradient") {
67
+ throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
68
+ }
69
+ if (family === "categorical") {
70
+ if (property === "fill" && channels.includes("color")) {
71
+ throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
72
+ }
73
+ if (property === "stroke" && channels.includes("stroke")) {
74
+ throw new Error("editLegendBlock symbol.stroke conflicts with the stroke mapping.");
75
+ }
76
+ if (!categoricalSupports(config, property)) {
77
+ throw new Error(`editLegendBlock symbol.${property} is unsupported by this categorical recipe.`);
78
+ }
79
+ return;
80
+ }
81
+ if (kind === "size" && property === "size") {
82
+ throw new Error("editLegendBlock symbol.size conflicts with the size mapping.");
83
+ }
84
+ if (kind === "opacity" && property === "opacity") {
85
+ throw new Error("editLegendBlock symbol.opacity conflicts with the opacity mapping.");
86
+ }
87
+ if (kind === "strokeWidth" && property === "strokeWidth") {
88
+ throw new Error("editLegendBlock symbol.strokeWidth conflicts with the strokeWidth mapping.");
89
+ }
90
+ if (kind === "strokeWidth" && ["size", "fill"].includes(property)) {
91
+ throw new Error(`editLegendBlock symbol.${property} is unsupported for strokeWidth blocks.`);
92
+ }
93
+ if (kind === "interval" && property === "fill") {
94
+ throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
95
+ }
96
+ if (kind === "strokeInterval" && property === "stroke") {
97
+ throw new Error("editLegendBlock symbol.stroke conflicts with the stroke mapping.");
98
+ }
99
+ if (["interval", "strokeInterval"].includes(kind) && property === "size") {
100
+ throw new Error(`editLegendBlock symbol.size is unsupported for ${kind} blocks.`);
101
+ }
102
+ }
103
+
104
+ function normalizeSymbolPatch(value, descriptor) {
105
+ validatePatchObject(value, SYMBOL_OPTIONS, "editLegendBlock symbol");
106
+ if (value.size !== undefined) {
107
+ validateNonNegativeFinite(value.size, "editLegendBlock symbol.size");
108
+ }
109
+ if (value.fill !== undefined) {
110
+ validateNonEmptyString(value.fill, "editLegendBlock symbol.fill");
111
+ }
112
+ if (value.stroke !== undefined) {
113
+ validateNonEmptyString(value.stroke, "editLegendBlock symbol.stroke");
114
+ }
115
+ if (value.strokeWidth !== undefined) {
116
+ validateNonNegativeFinite(value.strokeWidth, "editLegendBlock symbol.strokeWidth");
117
+ }
118
+ if (value.opacity !== undefined) {
119
+ validateUnitInterval(value.opacity, "editLegendBlock symbol.opacity");
120
+ }
121
+ for (const property of Object.keys(value)) {
122
+ validateSymbolSupport(descriptor, property);
123
+ }
124
+ return { ...value };
125
+ }
126
+
127
+ export function validateLegendBlockOverride(descriptor, override) {
128
+ if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
129
+ if (override.labelMap !== undefined) {
130
+ if (descriptor.family !== "categorical") {
131
+ throw new Error("editLegendBlock labelMap requires a categorical block.");
132
+ }
133
+ normalizeDisplayLabelMap(override.labelMap, "editLegendBlock labelMap");
134
+ }
135
+ return override;
136
+ }
137
+
138
+ function exactCategoricalOrder(program, descriptor, value) {
139
+ if (descriptor.family !== "categorical") {
140
+ throw new Error("editLegendBlock order requires a categorical block.");
141
+ }
142
+ if (!Array.isArray(value) || value.length === 0) {
143
+ throw new TypeError("editLegendBlock order must be a non-empty array.");
144
+ }
145
+ if (!value.every(isNominalValue)) {
146
+ throw new TypeError("editLegendBlock order must contain categorical values.");
147
+ }
148
+ if (value.some((item, index) =>
149
+ value.slice(0, index).some(previous => Object.is(previous, item)))) {
150
+ throw new Error("editLegendBlock order values must be unique.");
151
+ }
152
+ const domain = program.resolvedScales[descriptor.scaleIds[0]].domain;
153
+ if (domain.length !== value.length ||
154
+ domain.some(item => !value.some(candidate => Object.is(candidate, item)))) {
155
+ throw new Error("editLegendBlock order must be an exact permutation of the current domain.");
156
+ }
157
+ return Object.freeze({ values: Object.freeze([...value]) });
158
+ }
159
+
160
+ function withoutEmptyNested(entry, key, value) {
161
+ if (Object.keys(value).length > 0) return { ...entry, [key]: value };
162
+ const { [key]: _removed, ...rest } = entry;
163
+ return rest;
164
+ }
165
+
166
+ export function patchLegendBlockOverride(config, key, patch) {
167
+ const previous = config.blockOverrides?.[key] ?? {};
168
+ let entry = { ...previous };
169
+ if (Object.hasOwn(patch, "title")) entry.title = patch.title;
170
+ if (Object.hasOwn(patch, "gap")) entry.gap = patch.gap;
171
+ if (Object.hasOwn(patch, "text")) {
172
+ entry = withoutEmptyNested(entry, "text", patch.text);
173
+ }
174
+ if (Object.hasOwn(patch, "symbol")) {
175
+ entry = withoutEmptyNested(entry, "symbol", patch.symbol);
176
+ }
177
+ if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
178
+ if (patch.removeLabelMap === true) delete entry.labelMap;
179
+ const blockOverrides = { ...(config.blockOverrides ?? {}) };
180
+ if (Object.keys(entry).length === 0) delete blockOverrides[key];
181
+ else blockOverrides[key] = entry;
182
+ return Object.keys(blockOverrides).length === 0
183
+ ? undefined
184
+ : blockOverrides;
185
+ }
186
+
187
+ function applyCategoricalSymbol(symbol, patch) {
188
+ return {
189
+ ...symbol,
190
+ layers: symbol.layers.map(layer => ({
191
+ ...layer,
192
+ ...(patch.size !== undefined && layer.type === "point"
193
+ ? { size: Math.sqrt(patch.size / Math.PI) }
194
+ : {}),
195
+ ...(patch.fill !== undefined && ["point", "swatch"].includes(layer.type)
196
+ ? { fill: patch.fill }
197
+ : {}),
198
+ ...(patch.stroke !== undefined ? { stroke: patch.stroke } : {}),
199
+ ...(patch.strokeWidth !== undefined
200
+ ? layer.type === "line"
201
+ ? { lineWidth: patch.strokeWidth }
202
+ : { strokeWidth: patch.strokeWidth }
203
+ : {}),
204
+ ...(patch.opacity !== undefined ? { opacity: patch.opacity } : {})
205
+ }))
206
+ };
207
+ }
208
+
209
+ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
210
+ const channels = kind === "series" || kind === "color" || kind === "stroke"
211
+ ? config.channels
212
+ : [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
213
+ gradient: "color", interval: "color", strokeGradient: "stroke",
214
+ strokeInterval: "stroke" }[kind]];
215
+ const override = config.blockOverrides?.[legendBlockKey(channels)] ?? {};
216
+ const effective = {
217
+ ...config,
218
+ ...(override.title === undefined || override.title === ""
219
+ ? {}
220
+ : { title: override.title }),
221
+ ...(override.title === ""
222
+ ? { titleVisible: false }
223
+ : override.title === undefined
224
+ ? {}
225
+ : { titleVisible: true }),
226
+ ...(override.gap === undefined ? {} : { itemGap: override.gap, blockGap: override.gap }),
227
+ ...(override.text === undefined
228
+ ? {}
229
+ : { labels: { ...config.labels, ...override.text } }),
230
+ ...(Object.hasOwn(override, "labelMap")
231
+ ? { labelMap: override.labelMap }
232
+ : {})
233
+ };
234
+ if (override.symbol === undefined) return effective;
235
+ if (["series", "color", "stroke"].includes(kind)) {
236
+ return {
237
+ ...effective,
238
+ symbol: applyCategoricalSymbol(config.symbol, override.symbol),
239
+ blockSymbol: override.symbol
240
+ };
241
+ }
242
+ if (kind === "opacity") {
243
+ return {
244
+ ...effective,
245
+ symbol: {
246
+ ...config.symbol,
247
+ ...(override.symbol.size === undefined
248
+ ? {}
249
+ : { radius: Math.sqrt(override.symbol.size / Math.PI) }),
250
+ ...Object.fromEntries(Object.entries(override.symbol)
251
+ .filter(([key]) => ["fill", "stroke", "strokeWidth"].includes(key)))
252
+ },
253
+ blockSymbol: override.symbol
254
+ };
255
+ }
256
+ if (["interval", "strokeInterval"].includes(kind)) {
257
+ return {
258
+ ...effective,
259
+ symbol: { ...config.symbol, ...override.symbol },
260
+ blockSymbol: override.symbol
261
+ };
262
+ }
263
+ return { ...effective, blockSymbol: override.symbol };
264
+ }
265
+
266
+ function titleGraphicId(kind) {
267
+ return legendResourcePolicy(kind).graphicIds.find(id => id.endsWith("Title"));
268
+ }
269
+
270
+ export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
271
+ const id = titleGraphicId(kind);
272
+ const exists = program.graphicSpec.objects[id] !== undefined;
273
+ if (!visible && exists) return program.editGraphics({ target: id, remove: true });
274
+ if (visible && !exists) {
275
+ const owned = new Set(legendResourcePolicy(kind).graphicIds);
276
+ const anchor = program.graphicSpec.objects.canvas.children
277
+ .filter(childId => childId !== id && owned.has(childId))
278
+ .at(-1);
279
+ if (anchor === undefined) {
280
+ throw new Error(`editLegendBlock cannot place the ${kind} legend title.`);
281
+ }
282
+ return program.createGraphics({
283
+ id,
284
+ type: "text",
285
+ ...resolveLegendGraphicPlacement(program, { after: anchor })
286
+ });
287
+ }
288
+ return program;
289
+ }
290
+
291
+ const SPECIALIZED_SYMBOLS = Object.freeze({
292
+ size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
293
+ opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
294
+ strokeWidth: Object.freeze({ id: "strokeWidthLegendSymbols", type: "line" }),
295
+ interval: Object.freeze({ id: "colorLegendSymbols", type: "rect" }),
296
+ strokeInterval: Object.freeze({ id: "strokeIntervalSymbols", type: "rect" })
297
+ });
298
+
299
+ // Symbol patches replace the complete block-local patch. Recreating the concrete
300
+ // symbol owner removes properties that existed only in the previous patch before
301
+ // the ordinary rematerializer writes the new effective appearance.
302
+ function recreateSymbolGraphics(program, descriptor) {
303
+ if (descriptor.family === "gradient") return program;
304
+ if (descriptor.family === "categorical") {
305
+ const symbolIds = legendResourcePolicy(descriptor.kind).graphicIds
306
+ .filter(id => id.includes("Symbol"));
307
+ let next = program;
308
+ for (const id of symbolIds) {
309
+ if (next.graphicSpec.objects[id] !== undefined) {
310
+ next = next.editGraphics({ target: id, remove: true });
311
+ }
312
+ }
313
+ return next.createLegendSymbols({ kind: descriptor.kind });
314
+ }
315
+ const symbol = SPECIALIZED_SYMBOLS[descriptor.kind];
316
+ if (symbol === undefined || program.graphicSpec.objects[symbol.id] === undefined) {
317
+ throw new Error(`editLegendBlock cannot recreate ${descriptor.kind} legend symbols.`);
318
+ }
319
+ const placement = preserveGraphicPlacement(program, symbol.id);
320
+ return program
321
+ .editGraphics({ target: symbol.id, remove: true })
322
+ .createGraphics({ id: symbol.id, type: symbol.type, length: 0, ...placement });
323
+ }
324
+
325
+ function normalizeBlockPatch(args, descriptor) {
326
+ const patch = {};
327
+ if (Object.hasOwn(args, "title")) {
328
+ if (typeof args.title !== "string") {
329
+ throw new TypeError("editLegendBlock title must be a string.");
330
+ }
331
+ patch.title = args.title;
332
+ }
333
+ if (Object.hasOwn(args, "gap")) {
334
+ validateNonNegativeFinite(args.gap, "editLegendBlock gap");
335
+ patch.gap = args.gap;
336
+ }
337
+ if (Object.hasOwn(args, "text")) patch.text = normalizeTextPatch(args.text);
338
+ if (Object.hasOwn(args, "symbol")) {
339
+ patch.symbol = normalizeSymbolPatch(args.symbol, descriptor);
340
+ }
341
+ if (Object.hasOwn(args, "labelMap")) {
342
+ if (descriptor.family !== "categorical") {
343
+ throw new Error("editLegendBlock labelMap requires a categorical block.");
344
+ }
345
+ if (args.labelMap === "auto") patch.removeLabelMap = true;
346
+ else {
347
+ patch.labelMap = normalizeDisplayLabelMap(
348
+ args.labelMap,
349
+ "editLegendBlock labelMap"
350
+ );
351
+ }
352
+ }
353
+ return patch;
354
+ }
355
+
356
+ export const editLegendBlock = action(
357
+ {
358
+ op: "editLegendBlock",
359
+ description: "Edit one logical legend block selected by channel."
360
+ },
361
+ withGuideLayoutValidation(function (args = {}) {
362
+ validateOptionObject(args, OPTIONS, "editLegendBlock");
363
+ if (!Object.hasOwn(args, "target") || typeof args.target !== "string") {
364
+ throw new TypeError("editLegendBlock target must be an explicit legend target id.");
365
+ }
366
+ if (!Object.hasOwn(args, "channel") || typeof args.channel !== "string") {
367
+ throw new TypeError("editLegendBlock channel must be explicit.");
368
+ }
369
+ if (!Object.keys(args).some(key => !["target", "channel"].includes(key))) {
370
+ throw new Error("editLegendBlock requires at least one change.");
371
+ }
372
+ const descriptor = resolveLegendBlock(this, args, "editLegendBlock");
373
+ if ((Object.hasOwn(args, "values") || Object.hasOwn(args, "count")) &&
374
+ descriptor.family !== "sampled") {
375
+ throw new Error("editLegendBlock values and count require a sampled legend block.");
376
+ }
377
+ const patch = normalizeBlockPatch(args, descriptor);
378
+ let config = descriptor.config;
379
+ const blockOverrides = patchLegendBlockOverride(config, descriptor.key, patch);
380
+ if (blockOverrides === undefined) {
381
+ const { blockOverrides: _removed, ...rest } = config;
382
+ config = rest;
383
+ } else {
384
+ config = { ...config, blockOverrides };
385
+ }
386
+ if (Object.hasOwn(args, "values") || Object.hasOwn(args, "count")) {
387
+ const sampling = normalizeLegendSampling(args, {
388
+ previous: config,
389
+ operation: "edit",
390
+ label: `editLegendBlock ${descriptor.channel ?? args.channel} legend`
391
+ });
392
+ validateSamplingAgainstScale(sampling, this.resolvedScales[descriptor.scaleIds[0]],
393
+ "editLegendBlock values");
394
+ const { count: _legacyCount, ...withoutCount } = config;
395
+ config = { ...withoutCount, sampling };
396
+ }
397
+ const order = Object.hasOwn(args, "order")
398
+ ? exactCategoricalOrder(this, descriptor, args.order)
399
+ : undefined;
400
+ if (args.title !== undefined && args.title !== "") {
401
+ config = { ...config, title: args.title, inferredTitle: false };
402
+ }
403
+ let next = this._withLegendConfig(descriptor.kind, config);
404
+ const semanticKind = legendResourcePolicy(descriptor.kind).semanticKind;
405
+ if (order !== undefined) {
406
+ next = next.editSemantic({
407
+ property: `guide.legend.${semanticKind}.order`,
408
+ value: order
409
+ });
410
+ }
411
+ if (args.title !== undefined && args.title !== "") {
412
+ next = next.editSemantic({
413
+ property: `guide.legend.${semanticKind}.title`,
414
+ value: args.title
415
+ });
416
+ }
417
+ const effective = resolveEffectiveLegendBlockConfig(next, descriptor.kind,
418
+ next.guideConfigs.legend[descriptor.kind]);
419
+ if (Object.hasOwn(args, "symbol")) {
420
+ next = recreateSymbolGraphics(next, descriptor);
421
+ }
422
+ next = reconcileLegendBlockTitleGraphic(next, descriptor.kind,
423
+ effective.titleVisible !== false);
424
+ return next.rematerializeLegend();
425
+ })
426
+ );