ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,3 +1,4 @@
1
+ import { axisTargetArgs } from "../../selectors/axes.js";
1
2
  import { validatePolarAxisArgs } from "./polar/axes/facade.js";
2
3
  import { prefix as polarPrefix, resolveAngle } from "./polar/axes/shared.js";
3
4
  import { validateAxesArgs } from "./axes/axes.js";
@@ -47,24 +48,26 @@ export function scopeFacadeAxes(program, layer, args) {
47
48
  }
48
49
  const scoped = { coordinate: { id: coordinate.id, type: "cartesian" } };
49
50
  for (const channel of ["x", "y"]) {
50
- const option = args[channel];
51
- if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
52
- scoped[channel] = false;
53
- continue;
54
- }
55
- const scale = layer.encoding?.[channel]?.scale;
56
- if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
57
- if (option?.scale !== undefined && option.scale !== scale) {
58
- guideConflict(`axes.${channel}.scale does not belong to this facade`);
59
- }
60
- if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
61
- guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
62
- }
63
- scoped[channel] = { ...option, scale, coordinate: coordinate.id };
64
- validateAxisArgs(scoped[channel], `create${channel.toUpperCase()}Axis`);
65
- if (scoped[channel].ticksAndLabels !== false) {
66
- validateAxisTickGroupArgs(scoped[channel].ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
67
- }
51
+ const scope = option => {
52
+ if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
53
+ return false;
54
+ }
55
+ const scale = layer.encoding?.[channel]?.scale;
56
+ if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
57
+ if (option?.scale !== undefined && option.scale !== scale) {
58
+ guideConflict(`axes.${channel}.scale does not belong to this facade`);
59
+ }
60
+ if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
61
+ guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
62
+ }
63
+ const scopedOption = { ...option, scale, coordinate: coordinate.id };
64
+ validateAxisArgs(scopedOption, `create${channel.toUpperCase()}Axis`);
65
+ if (scopedOption.ticksAndLabels !== false) {
66
+ validateAxisTickGroupArgs(scopedOption.ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
67
+ }
68
+ return scopedOption;
69
+ };
70
+ scoped[channel] = Array.isArray(args[channel]) ? args[channel].map(scope) : scope(args[channel]);
68
71
  }
69
72
  return scoped;
70
73
  }
@@ -76,21 +79,25 @@ function tickMode(config) {
76
79
 
77
80
  function planAxis(program, channel, args, explicit) {
78
81
  const prefix = channel.toUpperCase();
79
- const guide = program.semanticSpec.guides.axis?.[channel];
80
- const configs = program.guideConfigs.axis?.[channel] ?? {};
82
+ const axisId = args.id ?? channel;
83
+ const guide = program.semanticSpec.guides.axis?.[axisId];
84
+ const configs = program.guideConfigs.axis?.[axisId] ?? {};
81
85
  if (guide === undefined && Object.keys(configs).length === 0) {
82
86
  return [{ op: `create${prefix}Axis`, args }];
83
87
  }
84
88
  if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
85
89
  guideConflict(`${channel} axis uses a different coordinate or scale`);
86
90
  }
91
+ if (explicit.offset !== undefined && explicit.offset !== (configs.frame?.offset ?? 0)) {
92
+ guideConflict(`${channel} axis uses a different frame offset`);
93
+ }
87
94
  const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
88
95
  if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
89
96
  const position = explicit.position ?? positions[0];
90
- const shared = { scale: args.scale, ...(position === undefined ? {} : { position }) };
97
+ const shared = { ...axisTargetArgs(axisId, channel), scale: args.scale, ...(position === undefined ? {} : { position }) };
91
98
  const explicitShared = explicit.position === undefined ? {} : { position: explicit.position };
92
99
  const steps = guide.coordinate === undefined ? [{
93
- op: "editSemantic", args: { property: `guide.axis.${channel}.coordinate`, value: args.coordinate }
100
+ op: "editSemantic", args: { property: `guide.axis.${axisId}.coordinate`, value: args.coordinate }
94
101
  }] : [];
95
102
  const group = args.ticksAndLabels ?? {};
96
103
  const explicitGroup = explicit.ticksAndLabels ?? {};
@@ -106,7 +113,7 @@ function planAxis(program, channel, args, explicit) {
106
113
  for (const component of ["line", "ticks", "labels", "title"]) {
107
114
  const config = configs[component];
108
115
  if (axisComponentDisabled(args, component)) {
109
- if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
116
+ if (hasAxisComponent(program, channel, component, axisId)) guideConflict(`${channel} axis already has ${component}`);
110
117
  continue;
111
118
  }
112
119
  if (config === undefined) continue;
@@ -154,7 +161,9 @@ export function planFacadeAxes(program, layer, scoped, explicit = {}) {
154
161
  : planPolarAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
155
162
  }
156
163
  return ["x", "y"].flatMap(channel => scoped[channel] === false ? []
157
- : planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
164
+ : Array.isArray(scoped[channel])
165
+ ? scoped[channel].flatMap((entry, index) => planAxis(program, channel, entry, explicit[channel]?.[index] ?? {}))
166
+ : planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
158
167
  }
159
168
 
160
169
  function planPolarAxis(program, channel, args, explicit) {
@@ -4,8 +4,8 @@ import { legendResourcePolicy } from "../../materialization/guides/resources.js"
4
4
  import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
5
5
 
6
6
  function compatibleLegend(program, kind, requested, existing, explicit) {
7
- const expectedScales = requested.scales ?? [requested.scale];
8
- const actualScales = existing.scales ?? [existing.scale];
7
+ const expectedScales = kind === "colorSize" ? [requested.colorScale,requested.sizeScale] : requested.scales ?? [requested.scale];
8
+ const actualScales = kind === "colorSize" ? [existing.colorScale,existing.sizeScale] : existing.scales ?? [existing.scale];
9
9
  if (!sameGuideValue(expectedScales, actualScales) ||
10
10
  !sameGuideValue(requested.channels, existing.channels)) {
11
11
  guideConflict(`${kind} legend uses different channels or scales`);
@@ -17,10 +17,18 @@ function compatibleLegend(program, kind, requested, existing, explicit) {
17
17
  if (requested.symbol?.layers !== undefined && !sameGuideValue(
18
18
  requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
19
19
  )) guideConflict(`${kind} legend uses a different symbol recipe`);
20
+ if (explicit.representation !== undefined && (requested.representation ?? "gradient") !== (existing.representation ?? "gradient")) guideConflict(`${kind} legend uses a different color representation`);
21
+ if (requested.representation === "symbols" || kind === "colorSize") {
22
+ if (explicit.values !== undefined && !sameGuideValue(requested.sampling.values, existing.sampling?.values)) guideConflict(`${kind} legend uses different exact samples`);
23
+ if (explicit.count !== undefined && (existing.sampling?.mode !== "auto" || requested.sampling.count !== existing.sampling?.count)) guideConflict(`${kind} legend uses a different sample count`);
24
+ }
20
25
  const appearance = {};
21
26
  for (const [key, value] of Object.entries(explicit)) {
22
- if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
23
- if (key === "symbol" && requested.symbol?.layers !== undefined) {
27
+ if (["target", "channels", "representation", "values"].includes(key) || requested[key] === undefined) continue;
28
+ if (key === "title") {
29
+ appearance.titleVisible = value !== false;
30
+ if (value !== false) appearance.title = value;
31
+ } else if (key === "symbol" && requested.symbol?.layers !== undefined) {
24
32
  appearance.symbol = value === "auto" ? requested.symbol
25
33
  : value.layers !== undefined ? value
26
34
  : { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
@@ -8,6 +8,7 @@ import {
8
8
  refreshGridConfig,
9
9
  resolveGridConfig,
10
10
  resolveGridGeometry,
11
+ resolveGridPlacement,
11
12
  resolveGridResources,
12
13
  validateGridCreateArgs,
13
14
  validateGridEditArgs
@@ -60,7 +61,12 @@ function makeRematerialize(direction) {
60
61
 
61
62
  const config = refreshGridConfig(this, storedConfig);
62
63
  const geometry = resolveGridGeometry(this, config);
63
- let next = this._withGridConfig(direction, config).editGraphics({
64
+ let next = this._withGridConfig(direction, config);
65
+ if (config.zIndex !== undefined) {
66
+ const placement = resolveGridPlacement(next, config);
67
+ next = next.editGraphics({ target: operation.graphic, remove: true }).createGraphics({ id: operation.graphic, type: "line", length: 0, ...placement });
68
+ }
69
+ next = next.editGraphics({
64
70
  target: operation.graphic,
65
71
  property: "length",
66
72
  value: geometry.values.length
@@ -79,7 +85,7 @@ function makeRematerialize(direction) {
79
85
  .editGraphics({
80
86
  target: operation.graphic,
81
87
  property: "strokeDash",
82
- value: geometry.values.map(() => config.strokeDash)
88
+ value: geometry.strokeDash
83
89
  });
84
90
  }
85
91
  );
@@ -1,3 +1,7 @@
1
+ import { cartesianAxisOwners } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
+ import { isNominalValue } from "../../../grammar/scales/fields.js";
4
+ import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
1
5
  import { isSourceOwnedText } from "../../../grammar/text.js";
2
6
  import { validateUserId } from "../../../core/identifiers.js";
3
7
  import {
@@ -8,6 +12,7 @@ import {
8
12
  } from "../../../core/validation.js";
9
13
  import {
10
14
  isTransformedScaleType,
15
+ mapOrdinalPositionValues,
11
16
  mapContinuousScaleValues
12
17
  } from "../../../grammar/scales/index.js";
13
18
  import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
@@ -20,14 +25,15 @@ import {
20
25
  } from "../tickValues.js";
21
26
  import {
22
27
  findGraphicParent,
28
+ graphicSiblings,
23
29
  walkGraphicDrawOrder
24
30
  } from "../../../grammar/schemas/graphicTree.js";
25
31
 
26
32
  const GRID_OPTIONS = Object.freeze([
27
- "scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash"
33
+ "scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
28
34
  ]);
29
35
  const GRID_EDIT_OPTIONS = Object.freeze([
30
- "count", "values", "color", "lineWidth", "strokeDash"
36
+ "count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
31
37
  ]);
32
38
  const DEFAULT_STYLE = Object.freeze({
33
39
  color: DEFAULT_COLORS.grid,
@@ -46,19 +52,11 @@ export function gridNames(direction) {
46
52
  }
47
53
 
48
54
  function validateStrokeDash(value) {
49
- if (
50
- !Array.isArray(value) ||
51
- value.length % 2 !== 0 ||
52
- !value.every(item => Number.isFinite(item) && item >= 0)
53
- ) {
54
- throw new TypeError(
55
- "Grid strokeDash must be an even-length array of non-negative finite numbers."
56
- );
57
- }
58
- return value;
55
+ return validateGuideDash(value, "Grid strokeDash");
59
56
  }
60
57
 
61
58
  export function validateGridCreateArgs(args, operation) {
59
+ if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
62
60
  validateOptionObject(args, GRID_OPTIONS, operation);
63
61
  if (Array.isArray(args.values)) {
64
62
  validateGeneratedItemLimit(args.values.length, "Grid value count");
@@ -75,20 +73,24 @@ export function validateGridCreateArgs(args, operation) {
75
73
  if (Object.hasOwn(args, "values") && (
76
74
  !Array.isArray(args.values) ||
77
75
  args.values.length === 0 ||
78
- !args.values.every(Number.isFinite)
76
+ !args.values.every(isNominalValue)
79
77
  )) {
80
- throw new TypeError("Grid values must be a non-empty finite number array.");
78
+ throw new TypeError("Grid values must be a non-empty finite number array or nominal category array; continuous grids require finite numbers.");
81
79
  }
80
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
82
81
  if (Object.hasOwn(args, "color")) {
83
82
  validateNonEmptyString(args.color, "Grid color");
84
83
  }
85
84
  if (Object.hasOwn(args, "lineWidth")) {
86
85
  validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
87
86
  }
87
+ if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
88
+ if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
88
89
  if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
89
90
  }
90
91
 
91
92
  export function validateGridEditArgs(args, operation) {
93
+ if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
92
94
  validateOptionObject(args, GRID_EDIT_OPTIONS, operation, {
93
95
  allowEmpty: false
94
96
  });
@@ -109,18 +111,21 @@ export function validateGridEditArgs(args, operation) {
109
111
  if (Object.hasOwn(args, "values") && args.values !== "auto" && (
110
112
  !Array.isArray(args.values) ||
111
113
  args.values.length === 0 ||
112
- !args.values.every(Number.isFinite)
114
+ !args.values.every(isNominalValue)
113
115
  )) {
114
116
  throw new TypeError(
115
117
  'Grid values must be "auto" or a non-empty finite number array.'
116
118
  );
117
119
  }
120
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
118
121
  if (Object.hasOwn(args, "color")) {
119
122
  validateNonEmptyString(args.color, "Grid color");
120
123
  }
121
124
  if (Object.hasOwn(args, "lineWidth")) {
122
125
  validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
123
126
  }
127
+ if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
128
+ if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
124
129
  if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
125
130
  }
126
131
 
@@ -129,44 +134,30 @@ export function resolveGridResources(program, direction, args) {
129
134
  let layers = program.semanticSpec.layers.filter(
130
135
  layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
131
136
  );
132
- if (layers.length === 0) throw new Error(`${direction} grid requires a ${channel} encoding.`);
133
- const requestedCoordinate = args.coordinate === undefined
134
- ? undefined
135
- : validateUserId(args.coordinate, "Grid coordinate id");
136
- const requestedScale = args.scale === undefined
137
- ? undefined
138
- : validateUserId(args.scale, "Grid scale id");
139
- if (requestedCoordinate !== undefined) {
140
- layers = layers.filter(layer => layer.coordinate === requestedCoordinate);
141
- }
142
- if (requestedScale !== undefined) {
143
- layers = layers.filter(layer => layer.encoding[channel].scale === requestedScale);
144
- }
145
- if (layers.length === 0) throw new Error(`${direction} grid found no matching encoded layers.`);
146
- const coordinateIds = [...new Set(layers.map(layer => layer.coordinate).filter(Boolean))];
147
- if (layers.some(layer => layer.coordinate === undefined)) {
148
- throw new Error(`${direction} grid requires a stored coordinate.`);
149
- }
150
- if (coordinateIds.length === 0) throw new Error(`Unknown grid coordinate "${requestedCoordinate}".`);
151
- if (coordinateIds.length > 1) {
152
- throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
153
- }
137
+ const requestedCoordinate = args.coordinate === undefined ? undefined : validateUserId(args.coordinate, "Grid coordinate id");
138
+ const requestedScale = args.scale === undefined ? undefined : validateUserId(args.scale, "Grid scale id");
139
+ const axes = cartesianAxisOwners(program).filter(owner => owner.channel === channel && owner.scale !== undefined && owner.coordinate !== undefined);
140
+ const bindings = [...layers.map(layer => ({ coordinate: layer.coordinate, scale: layer.encoding[channel].scale })),
141
+ ...axes.map(owner => ({ coordinate: owner.coordinate, scale: owner.scale }))]
142
+ .filter(binding => (requestedCoordinate === undefined || binding.coordinate === requestedCoordinate) &&
143
+ (requestedScale === undefined || binding.scale === requestedScale));
144
+ if (bindings.length === 0) throw new Error(layers.length === 0
145
+ ? `${direction} grid requires a ${channel} encoding or bound Cartesian axis.`
146
+ : `${direction} grid found no matching encoded layers.`);
147
+ if (bindings.some(binding => binding.coordinate === undefined)) throw new Error(`${direction} grid requires a stored coordinate.`);
148
+ const coordinateIds = [...new Set(bindings.map(binding => binding.coordinate))];
149
+ if (coordinateIds.length > 1) throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
154
150
  const coordinate = requestedCoordinate ?? coordinateIds[0];
155
- const definition = findCoordinate(program, coordinate);
156
- if (definition?.type !== "cartesian") {
157
- throw new Error(`${direction} grid requires a Cartesian coordinate.`);
158
- }
159
- const scaleIds = [...new Set(layers.map(layer => layer.encoding[channel].scale))];
160
- if (scaleIds.length === 0) throw new Error(`Unknown grid scale "${requestedScale}".`);
161
- if (scaleIds.length > 1) {
162
- throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
163
- }
151
+ if (findCoordinate(program, coordinate)?.type !== "cartesian") throw new Error(`${direction} grid requires a Cartesian coordinate.`);
152
+ const scaleIds = [...new Set(bindings.map(binding => binding.scale))];
153
+ if (scaleIds.length > 1) throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
164
154
  const scale = requestedScale ?? scaleIds[0];
155
+ layers = layers.filter(layer => layer.coordinate === coordinate && layer.encoding[channel].scale === scale);
165
156
  if (
166
- !["linear", "time"].includes(program.resolvedScales[scale]?.type) &&
157
+ !["linear", "time", "band", "point"].includes(program.resolvedScales[scale]?.type) &&
167
158
  !isTransformedScaleType(program.resolvedScales[scale]?.type)
168
159
  ) {
169
- throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
160
+ throw new Error(`${direction} grid requires resolved continuous, band or point scale "${scale}".`);
170
161
  }
171
162
  const sourceIds = new Set(layers.map(layer => layer.id));
172
163
  const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
@@ -176,9 +167,15 @@ export function resolveGridResources(program, direction, args) {
176
167
  const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
177
168
  return leader === undefined ? [layer.id] : [leader, layer.id];
178
169
  }));
170
+ if (related.size === 0) {
171
+ for (const owner of axes.filter(owner => owner.scale === scale && owner.coordinate === coordinate)) {
172
+ for (const id of axisGraphicIds(channel, owner.id)) if (program.graphicSpec.objects[id] !== undefined) related.add(id);
173
+ }
174
+ }
179
175
  let before;
176
+ let after;
180
177
  walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
181
- if (before === undefined && related.has(id)) before = id;
178
+ if (related.has(id)) { if (before === undefined) before = id; after = id; }
182
179
  });
183
180
  if (before === undefined) throw new Error(`${direction} grid requires related mark graphics.`);
184
181
  const owner = findGraphicParent(program.graphicSpec, before);
@@ -187,6 +184,7 @@ export function resolveGridResources(program, direction, args) {
187
184
  coordinate,
188
185
  scale,
189
186
  before,
187
+ after,
190
188
  ...(owner?.kind === "parent" ? { parent: owner.id } : {})
191
189
  };
192
190
  }
@@ -199,8 +197,11 @@ export function resolveGridConfig(program, direction, args, resources) {
199
197
  : Object.hasOwn(args, "count")
200
198
  ? { mode: "count", count: args.count }
201
199
  : inferGridTickConfig(program, resources.channel, resources.scale);
202
- return {
200
+ return patchGuideDash({
203
201
  direction,
202
+ ...(args.bandPosition === undefined ? {} : { bandPosition: args.bandPosition }),
203
+ ...(args.zIndex === undefined ? {} : { zIndex: args.zIndex }),
204
+ ...(args.alongOffset === undefined ? {} : { alongOffset: args.alongOffset }),
204
205
  scale: resources.scale,
205
206
  coordinate: resources.coordinate,
206
207
  ...explicit,
@@ -208,7 +209,7 @@ export function resolveGridConfig(program, direction, args, resources) {
208
209
  color: args.color ?? DEFAULT_STYLE.color,
209
210
  lineWidth: args.lineWidth ?? DEFAULT_STYLE.lineWidth,
210
211
  strokeDash: args.strokeDash ?? DEFAULT_STYLE.strokeDash
211
- };
212
+ }, args);
212
213
  }
213
214
 
214
215
  export function refreshGridConfig(program, config) {
@@ -243,7 +244,7 @@ export function editGridConfig(previous, args) {
243
244
  } else if (args.values === "auto") {
244
245
  config.inferredValues = true;
245
246
  }
246
- return config;
247
+ return patchGuideDash(config, args);
247
248
  }
248
249
 
249
250
  export function resolveGridGeometry(program, config) {
@@ -255,23 +256,66 @@ export function resolveGridGeometry(program, config) {
255
256
  channel
256
257
  });
257
258
  if ((
258
- !["linear", "time"].includes(scale?.type) &&
259
+ !["linear", "time", "band", "point"].includes(scale?.type) &&
259
260
  !isTransformedScaleType(scale?.type)
260
261
  ) || bounds === undefined) {
261
- throw new Error("Grid materialization requires a continuous scale and Canvas bounds.");
262
+ throw new Error("Grid materialization requires a continuous, band or point scale and Canvas bounds.");
263
+ }
264
+ if (config.bandPosition !== undefined && scale.type !== "band") throw new Error("Grid bandPosition requires a band scale.");
265
+ if (["band", "point"].includes(scale.type)) {
266
+ if (config.mode === "count") throw new Error("Categorical grid values cannot use count.");
267
+ const values = valuesFromTickConfig(program, config);
268
+ if (!values.every(value => scale.domain.includes(value))) throw new RangeError("Grid values must be inside the scale domain.");
269
+ const centers = mapOrdinalPositionValues(values, scale);
270
+ const direction = scale.range[1] >= scale.range[0] ? 1 : -1;
271
+ const owners = new Map();
272
+ if (config.bandPosition === "boundary") {
273
+ centers.forEach((center, index) => {
274
+ for (const edge of [center - direction * scale.bandwidth / 2, center + direction * scale.bandwidth / 2]) {
275
+ if (!owners.has(edge)) owners.set(edge, values[index]);
276
+ }
277
+ });
278
+ }
279
+ const positions = config.bandPosition === "boundary"
280
+ ? [...owners.keys()].sort((a, b) => direction * (a - b)) : centers;
281
+ validateGeneratedItemLimit(positions.length, "Grid value count");
282
+ return {
283
+ ...resolveGridLineGeometry({ direction: config.direction, values: positions, positions: positions.map(value => value + (config.alongOffset ?? 0)), bounds }),
284
+ strokeDash: resolveGuideDashes(config.bandPosition === "boundary" ? positions.map(value => owners.get(value)) : values, config)
285
+ };
262
286
  }
263
287
  const values = valuesFromTickConfig(program, config);
264
288
  validateGeneratedItemLimit(values.length, "Grid value count");
265
289
  const low = Math.min(...scale.domain);
266
290
  const high = Math.max(...scale.domain);
267
- if (!values.every(value => value >= low && value <= high)) {
291
+ if (!values.every(value => Number.isFinite(value) && value >= low && value <= high)) {
268
292
  throw new RangeError("Grid values must be inside the scale domain.");
269
293
  }
270
294
  const positions = mapContinuousScaleValues(values, scale);
271
- return resolveGridLineGeometry({
295
+ return { ...resolveGridLineGeometry({
272
296
  direction: config.direction,
273
297
  values,
274
- positions,
298
+ positions: positions.map(value => value + (config.alongOffset ?? 0)),
275
299
  bounds
276
- });
300
+ }), strokeDash: resolveGuideDashes(values, config) };
301
+ }
302
+
303
+
304
+ export function resolveGridPlacement(program, config) {
305
+ const resources = resolveGridResources(program, config.direction, { scale: config.scale, coordinate: config.coordinate });
306
+ const id = gridNames(config.direction).graphic;
307
+ const siblings = graphicSiblings(program.graphicSpec, resources.before);
308
+ const rank = direction => [program.guideConfigs.grid?.[direction]?.zIndex ?? 0, direction === "horizontal" ? 0 : 1];
309
+ const ownRank = [config.zIndex ?? 0, config.direction === "horizontal" ? 0 : 1];
310
+ const compare = (a, b) => a[0] - b[0] || a[1] - b[1];
311
+ const neighbors = ["horizontal", "vertical"].filter(direction => {
312
+ const other = gridNames(direction).graphic;
313
+ return other !== id && siblings.includes(other) && ((rank(direction)[0] > 0) === (ownRank[0] > 0));
314
+ }).sort((a, b) => compare(rank(a), rank(b)));
315
+ const greater = neighbors.find(direction => compare(rank(direction), ownRank) > 0);
316
+ const lower = neighbors.filter(direction => compare(rank(direction), ownRank) < 0).at(-1);
317
+ return {
318
+ ...(resources.parent === undefined ? {} : { parent: resources.parent }),
319
+ ...(greater ? { before: gridNames(greater).graphic } : lower ? { after: gridNames(lower).graphic } : ownRank[0] > 0 ? { after: resources.after } : { before: resources.before })
320
+ };
277
321
  }
@@ -1,3 +1,7 @@
1
+ import { editFacetParentLegend } from "../../../materialization/facetGuides/edit.js";
2
+
3
+ import {patchLegendClipHeight, validateLegendClip} from "../../../grammar/legendClip.js";
4
+ import { LEGEND_POSITIONS } from "../../../core/vocabulary.js";
1
5
  import { action } from "../../../core/action.js";
2
6
  import { isPlainObject } from "../../../core/immutable.js";
3
7
  import {
@@ -23,11 +27,11 @@ import {
23
27
  resolveLegendBlock
24
28
  } from "./target.js";
25
29
  import { validateFontWeight } from "./categorical/validation.js";
26
- import { normalizeDisplayLabelMap } from "../../../grammar/displayLabels.js";
30
+ import { normalizeDisplayLabelMap, normalizeNumericDisplayLabelMap } from "../../../grammar/displayLabels.js";
27
31
 
28
32
  const OPTIONS = Object.freeze([
29
33
  "target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
30
- "labelMap"
34
+ "labelMap", "position", "clipHeight"
31
35
  ]);
32
36
  const TEXT_OPTIONS = Object.freeze(["fontSize", "fontFamily", "fontWeight", "color"]);
33
37
  const SYMBOL_OPTIONS = Object.freeze(["size", "fill", "stroke", "strokeWidth", "opacity"]);
@@ -63,10 +67,12 @@ function categoricalSupports(config, property) {
63
67
 
64
68
  function validateSymbolSupport(descriptor, property) {
65
69
  const { channels, family, kind, config } = descriptor;
70
+ if(kind === "colorSize" && ["fill","size"].includes(property)) throw new Error("Color-size symbols cannot override mapped fill or size.");
66
71
  if (family === "gradient") {
67
72
  throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
68
73
  }
69
74
  if (family === "categorical") {
75
+ if (property === "size" && channels.includes("size")) throw new Error("Combined size-shape block cannot override mapped size.");
70
76
  if (property === "fill" && channels.includes("color")) {
71
77
  throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
72
78
  }
@@ -78,6 +84,7 @@ function validateSymbolSupport(descriptor, property) {
78
84
  }
79
85
  return;
80
86
  }
87
+ if (kind === "gradient" && property === "fill") throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
81
88
  if (kind === "size" && property === "size") {
82
89
  throw new Error("editLegendBlock symbol.size conflicts with the size mapping.");
83
90
  }
@@ -124,13 +131,21 @@ function normalizeSymbolPatch(value, descriptor) {
124
131
  return { ...value };
125
132
  }
126
133
 
134
+ function normalizeBlockLabelMap(descriptor, map) {
135
+ if (["size","colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
136
+ return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
137
+ }
138
+ if (descriptor.family !== "categorical") {
139
+ throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
140
+ }
141
+ return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
142
+ }
143
+
127
144
  export function validateLegendBlockOverride(descriptor, override) {
145
+ if (override.position !== undefined && !LEGEND_POSITIONS.includes(override.position)) throw new Error("Invalid local legend position.");
128
146
  if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
129
147
  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");
148
+ normalizeBlockLabelMap(descriptor, override.labelMap);
134
149
  }
135
150
  return override;
136
151
  }
@@ -175,6 +190,8 @@ export function patchLegendBlockOverride(config, key, patch) {
175
190
  entry = withoutEmptyNested(entry, "symbol", patch.symbol);
176
191
  }
177
192
  if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
193
+ if (Object.hasOwn(patch, "position")) entry.position = patch.position;
194
+ if (patch.removePosition === true) delete entry.position;
178
195
  if (patch.removeLabelMap === true) delete entry.labelMap;
179
196
  const blockOverrides = { ...(config.blockOverrides ?? {}) };
180
197
  if (Object.keys(entry).length === 0) delete blockOverrides[key];
@@ -207,7 +224,7 @@ function applyCategoricalSymbol(symbol, patch) {
207
224
  }
208
225
 
209
226
  export function resolveEffectiveLegendBlockConfig(program, kind, config) {
210
- const channels = kind === "series" || kind === "color" || kind === "stroke"
227
+ const channels = kind === "colorSize" ? ["color","size"] : kind === "series" || kind === "color" || kind === "stroke"
211
228
  ? config.channels
212
229
  : [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
213
230
  gradient: "color", interval: "color", strokeGradient: "stroke",
@@ -232,6 +249,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
232
249
  : {})
233
250
  };
234
251
  if (override.symbol === undefined) return effective;
252
+ if(kind === "colorSize") return {...effective,symbol:{...config.symbol,...override.symbol}};
235
253
  if (["series", "color", "stroke"].includes(kind)) {
236
254
  return {
237
255
  ...effective,
@@ -239,7 +257,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
239
257
  blockSymbol: override.symbol
240
258
  };
241
259
  }
242
- if (kind === "opacity") {
260
+ if (kind === "opacity" || kind === "gradient" && config.representation === "symbols") {
243
261
  return {
244
262
  ...effective,
245
263
  symbol: {
@@ -289,6 +307,8 @@ export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
289
307
  }
290
308
 
291
309
  const SPECIALIZED_SYMBOLS = Object.freeze({
310
+ colorSize: Object.freeze({id:"colorSizeLegendSymbols",type:"circle"}),
311
+ gradient: Object.freeze({ id: "colorGradientSymbols", type: "circle" }),
292
312
  size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
293
313
  opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
294
314
  strokeWidth: Object.freeze({ id: "strokeWidthLegendSymbols", type: "line" }),
@@ -324,6 +344,13 @@ function recreateSymbolGraphics(program, descriptor) {
324
344
 
325
345
  function normalizeBlockPatch(args, descriptor) {
326
346
  const patch = {};
347
+ if (Object.hasOwn(args, "position")) {
348
+ if (args.position === "inherit") patch.removePosition = true;
349
+ else {
350
+ if (!LEGEND_POSITIONS.includes(args.position)) throw new Error("Legend block position requires an anchor or inherit.");
351
+ patch.position = args.position;
352
+ }
353
+ }
327
354
  if (Object.hasOwn(args, "title")) {
328
355
  if (typeof args.title !== "string") {
329
356
  throw new TypeError("editLegendBlock title must be a string.");
@@ -339,26 +366,21 @@ function normalizeBlockPatch(args, descriptor) {
339
366
  patch.symbol = normalizeSymbolPatch(args.symbol, descriptor);
340
367
  }
341
368
  if (Object.hasOwn(args, "labelMap")) {
342
- if (descriptor.family !== "categorical") {
343
- throw new Error("editLegendBlock labelMap requires a categorical block.");
344
- }
369
+ // Validate eligibility even for a reset, without introducing an empty map owner.
370
+ const map = normalizeBlockLabelMap(descriptor, args.labelMap === "auto" ? [] : args.labelMap);
345
371
  if (args.labelMap === "auto") patch.removeLabelMap = true;
346
- else {
347
- patch.labelMap = normalizeDisplayLabelMap(
348
- args.labelMap,
349
- "editLegendBlock labelMap"
350
- );
351
- }
372
+ else patch.labelMap = map;
352
373
  }
353
374
  return patch;
354
375
  }
355
376
 
356
377
  export const editLegendBlock = /* @__PURE__ */ action(
357
378
  {
358
- op: "editLegendBlock",
379
+ op: "editLegendBlock", scope: "any",
359
380
  description: "Edit one logical legend block selected by channel."
360
381
  },
361
382
  withGuideLayoutValidation(function (args = {}) {
383
+ if (this.compositionSpec !== undefined) return editFacetParentLegend(this, "editLegendBlock", args);
362
384
  validateOptionObject(args, OPTIONS, "editLegendBlock");
363
385
  if (!Object.hasOwn(args, "target") || typeof args.target !== "string") {
364
386
  throw new TypeError("editLegendBlock target must be an explicit legend target id.");
@@ -375,7 +397,9 @@ export const editLegendBlock = /* @__PURE__ */ action(
375
397
  throw new Error("editLegendBlock values and count require a sampled legend block.");
376
398
  }
377
399
  const patch = normalizeBlockPatch(args, descriptor);
378
- let config = descriptor.config;
400
+ let config = Object.hasOwn(args, "clipHeight")
401
+ ? patchLegendClipHeight(descriptor.config, args.clipHeight) : descriptor.config;
402
+ validateLegendClip(descriptor.kind, config);
379
403
  const blockOverrides = patchLegendBlockOverride(config, descriptor.key, patch);
380
404
  if (blockOverrides === undefined) {
381
405
  const { blockOverrides: _removed, ...rest } = config;
@@ -421,6 +445,8 @@ export const editLegendBlock = /* @__PURE__ */ action(
421
445
  }
422
446
  next = reconcileLegendBlockTitleGraphic(next, descriptor.kind,
423
447
  effective.titleVisible !== false);
424
- return next.rematerializeLegend();
448
+ return Object.hasOwn(args, "position")
449
+ ? next.editLegend({target: args.target, channel: args.channel, position: args.position})
450
+ : next.rematerializeLegend();
425
451
  })
426
452
  );