ggaction 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,25 +1,48 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import {
3
3
  validateGeneratedItemLimit,
4
+ validateNonEmptyString,
4
5
  validateKeys
5
6
  } from "../../../core/validation.js";
6
7
  import { mapLinearValues } from "../../../grammar/scales/index.js";
7
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
8
+ import { resolveLegendItemLayout } from "../../../layout/legendItems.js";
8
9
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
9
10
  "../../../theme/defaults.js";
10
- import { resolveLayout as resolveCategoricalLayout } from
11
- "./categorical/layout.js";
12
11
  import { findLayer } from "../../../selectors/layers.js";
13
- import { findCanvasGraphic, resolveLegendGraphicPlacement } from
12
+ import { resolveLegendGraphicPlacement } from
14
13
  "../../../materialization/graphicHierarchy.js";
15
14
  import {
15
+ assertLegendBoundsInsideCanvas,
16
+ editLegendBackground,
17
+ normalizeItemLegendLayout,
18
+ normalizeLegendBorder,
19
+ normalizeLegendTextOptions,
20
+ normalizeLegendTitleOptions,
21
+ resolveContinuousBounds,
22
+ resolveLegendBackgroundFromBounds,
16
23
  formatContinuousValues,
17
24
  sampleContinuousValues,
18
25
  selectLegendLayer,
19
26
  styleContinuousText
20
27
  } from "./continuous/common.js";
28
+ import { legendResourcePolicies } from "../../../materialization/guides/resources.js";
21
29
 
22
- const SIZE_OPTIONS = Object.freeze(["target", "count"]);
30
+ const SIZE_OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
31
+ "direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
32
+
33
+ export const SIZE_LEGEND_LABELS = Object.freeze({
34
+ offset: 12,
35
+ color: DEFAULT_COLORS.text,
36
+ fontSize: 12,
37
+ fontFamily: DEFAULT_FONT_FAMILY,
38
+ fontWeight: "normal"
39
+ });
40
+ export const SIZE_LEGEND_TITLE_STYLE = Object.freeze({
41
+ color: DEFAULT_COLORS.strongText,
42
+ fontSize: 13,
43
+ fontFamily: DEFAULT_FONT_FAMILY,
44
+ fontWeight: 600
45
+ });
23
46
 
24
47
  export function isSizeLegendPoint(layer) {
25
48
  return layer?.mark?.type === "point" &&
@@ -46,17 +69,42 @@ function requireScale(program, id, type) {
46
69
  return scale;
47
70
  }
48
71
 
49
- function bounds(program) {
50
- const value = resolveGraphicBounds(program);
51
- const canvas = findCanvasGraphic(program);
52
- if (
53
- value === undefined ||
54
- ![value.x, value.y, value.width, value.height].every(Number.isFinite) ||
55
- !Number.isFinite(canvas?.properties.width)
56
- ) {
57
- throw new Error("Legend layout requires Canvas bounds and width.");
72
+ export function resolveSizeLegendLayout(program, config) {
73
+ const scale = requireScale(program, config.scale, "linear");
74
+ const categorical = [program.guideConfigs.legend?.series, program.guideConfigs.legend?.color]
75
+ .find(candidate => candidate?.target === config.target);
76
+ const inherit = config.inheritAppearance === true && categorical !== undefined;
77
+ const labels = inherit ? { ...categorical.labels, offset: config.labels.offset } : config.labels;
78
+ const titleStyle = inherit ? categorical.titleStyle : config.titleStyle;
79
+ const position = categorical?.position ?? config.position;
80
+ if (categorical?.layout === "legacy-bottom") {
81
+ throw new Error('Combined size legends require layout "edge".');
58
82
  }
59
- return value;
83
+ const horizontal = categorical !== undefined && ["top", "bottom"].includes(position);
84
+ const geometry = horizontal ? {
85
+ position, align: categorical.align, direction: categorical.direction,
86
+ columns: categorical.columns, titlePosition: categorical.titlePosition,
87
+ offset: categorical.offset, itemGap: categorical.itemGap
88
+ } : { position };
89
+ const values = sampleContinuousValues(scale.domain, config.count);
90
+ const areas = mapLinearValues(values, scale.domain, scale.range, { clamp: scale.clamp ?? false });
91
+ const radii = areas.map(area => Math.sqrt(area / Math.PI));
92
+ const radius = Math.max(...radii);
93
+ const width = Math.max(32, radius * 2);
94
+ const { plot, canvas } = resolveContinuousBounds(program);
95
+ const text = formatContinuousValues(
96
+ values,
97
+ scale.domain,
98
+ "quantitative",
99
+ config.labels.format
100
+ );
101
+ const layout = resolveLegendItemLayout(plot, { ...config, ...geometry, labels, titleStyle }, text, {
102
+ width, height: radius * 2,
103
+ itemBounds: radii.map(r => ({ left: width / 2 - r, right: width / 2 + r, top: -r, bottom: r }))
104
+ });
105
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Size legend layout", { ...config, ...geometry });
106
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas, "Size legend", { ...config, ...geometry });
107
+ return { ...layout, symbolX: layout.symbolX.map(x => x + width / 2), radii, text, labels, titleStyle, background };
60
108
  }
61
109
 
62
110
  export const rematerializeSizeLegend = action(
@@ -81,34 +129,19 @@ export const rematerializeSizeLegend = action(
81
129
  title,
82
130
  domain: scale.domain
83
131
  };
84
- const plot = bounds(this);
85
- const categorical = this.guideConfigs.legend?.series ??
86
- this.guideConfigs.legend?.color;
87
- const leftLayout = categorical?.position === "left"
88
- ? resolveCategoricalLayout(this, categorical).size
89
- : undefined;
90
- const originX = leftLayout?.titleX ?? plot.x + plot.width + 30;
91
- const seriesCount = categorical?.domain.length ?? 0;
92
- const titleY = leftLayout?.titleY ?? plot.y + 56 + seriesCount * 34 + 22;
93
- const values = sampleContinuousValues(scale.domain, currentConfig.count);
94
- const areas = mapLinearValues(values, scale.domain, scale.range, {
95
- clamp: scale.clamp ?? false
96
- });
97
- const itemY = leftLayout?.itemY ??
98
- values.map((_, index) => titleY + 34 + index * 40);
99
- const symbolX = leftLayout?.symbolX ?? values.map(() => originX + 16);
100
- const labelX = leftLayout?.labelX ?? values.map(() => originX + 44);
132
+ const layout = resolveSizeLegendLayout(this, currentConfig);
133
+ const { itemY, symbolX, labelX, radii, labels, titleStyle } = layout;
101
134
  let next = this
102
135
  .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
103
136
  .editSemantic({ property: "guide.legend.size.title", value: title })
104
137
  ._withLegendConfig("size", currentConfig)
105
- .editGraphics({ target: "sizeLegendSymbols", property: "length", value: values.length })
138
+ .editGraphics({ target: "sizeLegendSymbols", property: "length", value: radii.length })
106
139
  .editGraphics({ target: "sizeLegendSymbols", property: "x", value: symbolX })
107
140
  .editGraphics({ target: "sizeLegendSymbols", property: "y", value: itemY })
108
141
  .editGraphics({
109
142
  target: "sizeLegendSymbols",
110
143
  property: "radius",
111
- value: areas.map(area => Math.sqrt(area / Math.PI))
144
+ value: radii
112
145
  })
113
146
  .editGraphics({
114
147
  target: "sizeLegendSymbols",
@@ -116,92 +149,103 @@ export const rematerializeSizeLegend = action(
116
149
  value: DEFAULT_COLORS.sizeSymbol
117
150
  })
118
151
  .editGraphics({ target: "sizeLegendSymbols", property: "opacity", value: 0.7 })
119
- .editGraphics({ target: "sizeLegendLabels", property: "length", value: values.length })
152
+ .editGraphics({ target: "sizeLegendLabels", property: "length", value: radii.length })
120
153
  .editGraphics({ target: "sizeLegendLabels", property: "x", value: labelX })
121
154
  .editGraphics({ target: "sizeLegendLabels", property: "y", value: itemY })
122
155
  .editGraphics({
123
156
  target: "sizeLegendLabels",
124
157
  property: "text",
125
- value: formatContinuousValues(values, scale.domain, "quantitative")
126
- })
127
- .editGraphics({ target: "sizeLegendTitle", property: "x", value: originX })
128
- .editGraphics({ target: "sizeLegendTitle", property: "y", value: titleY })
158
+ value: layout.text
159
+ });
160
+ next = editLegendBackground(next, "sizeLegendBackground", layout.background, currentConfig.border);
161
+ next = styleContinuousText(next, "sizeLegendLabels", labels);
162
+ if (currentConfig.titleVisible === false) return next;
163
+ next = next
164
+ .editGraphics({ target: "sizeLegendTitle", property: "x", value: layout.title.x })
165
+ .editGraphics({ target: "sizeLegendTitle", property: "y", value: layout.title.y })
129
166
  .editGraphics({ target: "sizeLegendTitle", property: "text", value: title });
130
- next = styleContinuousText(
131
- next,
132
- "sizeLegendLabels",
133
- currentConfig.inheritAppearance === true
134
- ? categorical.labels
135
- : {
136
- color: DEFAULT_COLORS.text,
137
- fontSize: 12,
138
- fontFamily: DEFAULT_FONT_FAMILY,
139
- fontWeight: "normal"
140
- }
141
- );
142
- return styleContinuousText(
143
- next,
144
- "sizeLegendTitle",
145
- currentConfig.inheritAppearance === true
146
- ? categorical.titleStyle
147
- : {
148
- color: DEFAULT_COLORS.strongText,
149
- fontSize: 13,
150
- fontFamily: DEFAULT_FONT_FAMILY,
151
- fontWeight: 600
152
- }
153
- );
167
+ return styleContinuousText(next, "sizeLegendTitle", titleStyle, { align: layout.title.align });
154
168
  }
155
169
  );
156
170
 
171
+
172
+ export function resolveSizeLegendConfig(program, args = {}) {
173
+ validateKeys(args, [...SIZE_OPTIONS, "inheritAppearance"], "createSizeLegend");
174
+ if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
175
+ const layer = resolveSizeLegendPoint(program, args.target);
176
+ const encoding = layer.encoding?.size;
177
+ if (encoding?.scale === undefined) {
178
+ throw new Error(`Point mark "${layer.id}" requires a size encoding.`);
179
+ }
180
+ const scale = requireScale(program, encoding.scale, "linear");
181
+ const count = args.count ?? 5;
182
+ if (!Number.isInteger(count) || count < 2) {
183
+ throw new RangeError("Size legend count must be an integer of at least 2.");
184
+ }
185
+ validateGeneratedItemLimit(count, "Size legend count");
186
+ return {
187
+ target: layer.id,
188
+ scale: encoding.scale,
189
+ ...normalizeItemLegendLayout({ ...args, itemGap: args.itemGap ?? 40 }),
190
+ title: args.title ?? encoding.field,
191
+ inferredTitle: args.title === undefined,
192
+ domain: scale.domain,
193
+ count,
194
+ inheritAppearance: args.inheritAppearance === true,
195
+ labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", SIZE_LEGEND_LABELS),
196
+ titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", SIZE_LEGEND_TITLE_STYLE),
197
+ border: normalizeLegendBorder(args.border),
198
+ titleVisible: true
199
+ };
200
+ }
201
+
202
+ export function createSizeLegendFromConfig(program, config) {
203
+ resolveSizeLegendLayout(program, config);
204
+ const { count } = config;
205
+ const following = new Set(legendResourcePolicies().filter(policy =>
206
+ policy.kind !== "size" && policy.family !== "categorical" &&
207
+ program.guideConfigs.legend?.[policy.kind] !== undefined
208
+ ).flatMap(policy => policy.graphicIds));
209
+ const before = program.graphicSpec.objects.canvas?.children?.find(id => following.has(id));
210
+ const placement = resolveLegendGraphicPlacement(program, before === undefined ? {} : { before });
211
+ let next = program
212
+ .editSemantic({ property: "guide.legend.size.scale", value: config.scale })
213
+ .editSemantic({ property: "guide.legend.size.title", value: config.title })
214
+ ._withLegendConfig("size", config);
215
+ if (config.border !== false) {
216
+ next = next.createGraphics({ id: "sizeLegendBackground", type: "rect",
217
+ ...placement });
218
+ }
219
+ next = next.createGraphics({
220
+ id: "sizeLegendSymbols",
221
+ type: "circle",
222
+ length: count,
223
+ ...placement
224
+ })
225
+ .createGraphics({
226
+ id: "sizeLegendLabels",
227
+ type: "text",
228
+ length: count,
229
+ ...placement
230
+ });
231
+ if (config.titleVisible !== false) {
232
+ next = next.createGraphics({
233
+ id: "sizeLegendTitle",
234
+ type: "text",
235
+ ...placement
236
+ });
237
+ }
238
+ return next.rematerializeSizeLegend();
239
+ }
240
+
157
241
  export const createSizeLegend = action(
158
242
  {
159
243
  op: "createSizeLegend",
160
244
  description: "Create a quantitative equal-area point-size legend."
161
245
  },
162
246
  function (args = {}) {
163
- validateKeys(args, [...SIZE_OPTIONS, "inheritAppearance"], "createSizeLegend");
164
- const layer = resolveSizeLegendPoint(this, args.target);
165
- const encoding = layer.encoding?.size;
166
- if (encoding?.scale === undefined) {
167
- throw new Error(`Point mark "${layer.id}" requires a size encoding.`);
168
- }
169
- const scale = requireScale(this, encoding.scale, "linear");
170
- const count = args.count ?? 5;
171
- if (!Number.isInteger(count) || count < 2) {
172
- throw new RangeError("Size legend count must be an integer of at least 2.");
173
- }
174
- validateGeneratedItemLimit(count, "Size legend count");
175
- return this
176
- .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
177
- .editSemantic({ property: "guide.legend.size.title", value: encoding.field })
178
- ._withLegendConfig("size", {
179
- target: layer.id,
180
- scale: encoding.scale,
181
- title: encoding.field,
182
- inferredTitle: true,
183
- domain: scale.domain,
184
- count,
185
- inheritAppearance: args.inheritAppearance === true
186
- })
187
- .createGraphics({
188
- id: "sizeLegendSymbols",
189
- type: "circle",
190
- length: count,
191
- ...resolveLegendGraphicPlacement(this)
192
- })
193
- .createGraphics({
194
- id: "sizeLegendLabels",
195
- type: "text",
196
- length: count,
197
- ...resolveLegendGraphicPlacement(this)
198
- })
199
- .createGraphics({
200
- id: "sizeLegendTitle",
201
- type: "text",
202
- ...resolveLegendGraphicPlacement(this)
203
- })
204
- .rematerializeSizeLegend();
247
+ const config = resolveSizeLegendConfig(this, args);
248
+ return createSizeLegendFromConfig(this, config);
205
249
  }
206
250
  );
207
251
 
@@ -1,23 +1,33 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import {
3
3
  validateGeneratedItemLimit,
4
+ validateNonEmptyString,
4
5
  validateKeys
5
6
  } from "../../../core/validation.js";
6
7
  import { mapContinuousScaleValues } from "../../../grammar/scales/index.js";
7
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
8
+ import { resolveLegendItemLayout } from "../../../layout/legendItems.js";
8
9
  import { resolveLegendGraphicPlacement } from
9
10
  "../../../materialization/graphicHierarchy.js";
10
11
  import { findLayer } from "../../../selectors/layers.js";
11
12
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
12
13
  "../../../theme/defaults.js";
13
14
  import {
15
+ assertLegendBoundsInsideCanvas,
16
+ editLegendBackground,
17
+ normalizeItemLegendLayout,
18
+ normalizeLegendBorder,
19
+ normalizeLegendTextOptions,
20
+ normalizeLegendTitleOptions,
21
+ resolveContinuousBounds,
22
+ resolveLegendBackgroundFromBounds,
14
23
  formatContinuousValues,
15
24
  sampleContinuousValues,
16
25
  selectLegendLayer,
17
26
  styleContinuousText
18
27
  } from "./continuous/common.js";
19
28
 
20
- const OPTIONS = Object.freeze(["target", "count"]);
29
+ const OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
30
+ "direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
21
31
 
22
32
  export const STROKE_WIDTH_LEGEND_LABELS = Object.freeze({
23
33
  offset: 12,
@@ -63,7 +73,24 @@ function requireScale(program, id) {
63
73
  return scale;
64
74
  }
65
75
 
66
- export const rematerializeStrokeWidthLegend = action(
76
+ export function resolveStrokeWidthLegendLayout(program, config) {
77
+ const scale = requireScale(program, config.scale);
78
+ const { plot, canvas } = resolveContinuousBounds(program);
79
+ const values = sampleContinuousValues(scale.domain, config.count);
80
+ const widths = mapContinuousScaleValues(values, scale);
81
+ const labels = formatContinuousValues(
82
+ values,
83
+ scale.domain,
84
+ "quantitative",
85
+ config.labels.format
86
+ );
87
+ const layout = resolveLegendItemLayout(plot, config, labels, { width: 32, height: 0, strokeWidth: widths });
88
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Stroke-width legend layout", config);
89
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas, "Stroke-width legend", config);
90
+ return { ...layout, widths, labels, background };
91
+ }
92
+
93
+ export const rematerializeStrokeWidthLegend = /* @__PURE__ */ action(
67
94
  {
68
95
  op: "rematerializeStrokeWidthLegend",
69
96
  description: "Rematerialize a quantitative stroke-width legend."
@@ -86,16 +113,9 @@ export const rematerializeStrokeWidthLegend = action(
86
113
  throw new Error("Stroke-width legend target requires a strokeWidth encoding.");
87
114
  }
88
115
  const scale = requireScale(this, encoding.scale);
89
- const plot = resolveGraphicBounds(this);
90
- if (plot === undefined) {
91
- throw new Error("Stroke-width legend requires resolved plot bounds.");
92
- }
93
- const values = sampleContinuousValues(scale.domain, config.count);
94
- const widths = mapContinuousScaleValues(values, scale);
95
- const originX = plot.x + plot.width + 30;
96
- const titleY = plot.y + 28;
97
- const y = values.map((_, index) => titleY + 34 + index * 32);
98
116
  const title = config.inferredTitle === true ? encoding.field : config.title;
117
+ const layout = resolveStrokeWidthLegendLayout(this, { ...config, scale: encoding.scale, title });
118
+ const { itemY: y, widths } = layout;
99
119
  let next = this
100
120
  .editSemantic({ property: "guide.legend.strokeWidth.scale", value: encoding.scale })
101
121
  .editSemantic({ property: "guide.legend.strokeWidth.title", value: title })
@@ -105,83 +125,105 @@ export const rematerializeStrokeWidthLegend = action(
105
125
  title,
106
126
  domain: scale.domain
107
127
  })
108
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "length", value: values.length })
109
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "x1", value: values.map(() => originX) })
110
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "x2", value: values.map(() => originX + 32) })
128
+ .editGraphics({ target: "strokeWidthLegendSymbols", property: "length", value: widths.length })
129
+ .editGraphics({ target: "strokeWidthLegendSymbols", property: "x1", value: layout.symbolX })
130
+ .editGraphics({ target: "strokeWidthLegendSymbols", property: "x2", value: layout.symbolX.map(x => x + 32) })
111
131
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "y1", value: y })
112
132
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "y2", value: y })
113
133
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "stroke", value: DEFAULT_COLORS.mark })
114
134
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "strokeWidth", value: widths })
115
- .editGraphics({ target: "strokeWidthLegendLabels", property: "length", value: values.length })
135
+ .editGraphics({ target: "strokeWidthLegendLabels", property: "length", value: widths.length })
116
136
  .editGraphics({
117
137
  target: "strokeWidthLegendLabels",
118
138
  property: "x",
119
- value: values.map(() => originX + 32 + config.labels.offset)
139
+ value: layout.labelX
120
140
  })
121
141
  .editGraphics({ target: "strokeWidthLegendLabels", property: "y", value: y })
122
142
  .editGraphics({
123
143
  target: "strokeWidthLegendLabels",
124
144
  property: "text",
125
- value: formatContinuousValues(values, scale.domain, "quantitative")
145
+ value: layout.labels
126
146
  });
147
+ next = editLegendBackground(next, "strokeWidthLegendBackground", layout.background, config.border);
127
148
  next = styleContinuousText(next, "strokeWidthLegendLabels", config.labels);
128
149
  if (config.titleVisible === false) return next;
129
150
  next = next
130
- .editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: originX })
131
- .editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: titleY })
151
+ .editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: layout.title.x })
152
+ .editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: layout.title.y })
132
153
  .editGraphics({ target: "strokeWidthLegendTitle", property: "text", value: title });
133
- return styleContinuousText(next, "strokeWidthLegendTitle", config.titleStyle);
154
+ return styleContinuousText(next, "strokeWidthLegendTitle", config.titleStyle, { align: layout.title.align });
134
155
  }
135
156
  );
136
157
 
137
- export const createStrokeWidthLegend = action(
158
+
159
+ export function resolveStrokeWidthLegendConfig(program, args = {}) {
160
+ validateKeys(args, OPTIONS, "createStrokeWidthLegend");
161
+ if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
162
+ const layer = resolveLayer(program, args.target);
163
+ const encoding = layer.encoding.strokeWidth;
164
+ requireScale(program, encoding.scale);
165
+ const count = args.count ?? 5;
166
+ if (!Number.isInteger(count) || count < 2) {
167
+ throw new RangeError(
168
+ "Stroke-width legend count must be an integer of at least 2."
169
+ );
170
+ }
171
+ validateGeneratedItemLimit(count, "Stroke-width legend count");
172
+ return {
173
+ target: layer.id,
174
+ scale: encoding.scale,
175
+ ...normalizeItemLegendLayout({ ...args, itemGap: args.itemGap ?? 32 }),
176
+ title: args.title ?? encoding.field,
177
+ inferredTitle: args.title === undefined,
178
+ count,
179
+ labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", STROKE_WIDTH_LEGEND_LABELS),
180
+ titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", STROKE_WIDTH_LEGEND_TITLE_STYLE),
181
+ border: normalizeLegendBorder(args.border),
182
+ titleVisible: true
183
+ };
184
+ }
185
+
186
+ export function createStrokeWidthLegendFromConfig(program, config) {
187
+ resolveStrokeWidthLegendLayout(program, config);
188
+ const { count } = config;
189
+ let next = program
190
+ .editSemantic({ property: "guide.legend.strokeWidth.scale", value: config.scale })
191
+ .editSemantic({ property: "guide.legend.strokeWidth.title", value: config.title })
192
+ ._withLegendConfig("strokeWidth", config);
193
+ if (config.border !== false) {
194
+ next = next.createGraphics({ id: "strokeWidthLegendBackground", type: "rect",
195
+ ...resolveLegendGraphicPlacement(program) });
196
+ }
197
+ next = next.createGraphics({
198
+ id: "strokeWidthLegendSymbols",
199
+ type: "line",
200
+ length: count,
201
+ ...resolveLegendGraphicPlacement(program)
202
+ })
203
+ .createGraphics({
204
+ id: "strokeWidthLegendLabels",
205
+ type: "text",
206
+ length: count,
207
+ ...resolveLegendGraphicPlacement(program)
208
+ });
209
+ if (config.titleVisible !== false) {
210
+ next = next.createGraphics({
211
+ id: "strokeWidthLegendTitle",
212
+ type: "text",
213
+ ...resolveLegendGraphicPlacement(program)
214
+ });
215
+ }
216
+ return next.rematerializeStrokeWidthLegend();
217
+ }
218
+
219
+ export const createStrokeWidthLegend = /* @__PURE__ */ action(
138
220
  {
139
221
  op: "createStrokeWidthLegend",
140
222
  description: "Create a quantitative stroke-width legend."
141
223
  },
142
224
  function (args = {}) {
143
- validateKeys(args, OPTIONS, "createStrokeWidthLegend");
144
- const layer = resolveLayer(this, args.target);
145
- const encoding = layer.encoding.strokeWidth;
146
- requireScale(this, encoding.scale);
147
- const count = args.count ?? 5;
148
- if (!Number.isInteger(count) || count < 2) {
149
- throw new RangeError(
150
- "Stroke-width legend count must be an integer of at least 2."
151
- );
152
- }
153
- validateGeneratedItemLimit(count, "Stroke-width legend count");
154
- return this
155
- .editSemantic({ property: "guide.legend.strokeWidth.scale", value: encoding.scale })
156
- .editSemantic({ property: "guide.legend.strokeWidth.title", value: encoding.field })
157
- ._withLegendConfig("strokeWidth", {
158
- target: layer.id,
159
- scale: encoding.scale,
160
- title: encoding.field,
161
- inferredTitle: true,
162
- count,
163
- labels: { ...STROKE_WIDTH_LEGEND_LABELS },
164
- titleStyle: { ...STROKE_WIDTH_LEGEND_TITLE_STYLE },
165
- titleVisible: true
166
- })
167
- .createGraphics({
168
- id: "strokeWidthLegendSymbols",
169
- type: "line",
170
- length: count,
171
- ...resolveLegendGraphicPlacement(this)
172
- })
173
- .createGraphics({
174
- id: "strokeWidthLegendLabels",
175
- type: "text",
176
- length: count,
177
- ...resolveLegendGraphicPlacement(this)
178
- })
179
- .createGraphics({
180
- id: "strokeWidthLegendTitle",
181
- type: "text",
182
- ...resolveLegendGraphicPlacement(this)
183
- })
184
- .rematerializeStrokeWidthLegend();
225
+ const config = resolveStrokeWidthLegendConfig(this, args);
226
+ return createStrokeWidthLegendFromConfig(this, config);
185
227
  }
186
228
  );
187
229
 
@@ -1,4 +1,16 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
+ import { LEGEND_CHANNELS } from "../../../core/vocabulary.js";
3
+
4
+ export function validateLegendChannels(channels, operation) {
5
+ if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
6
+ if (channels.length === 0) throw new Error(`${operation} channels must select at least one channel.`);
7
+ const seen = new Set();
8
+ for (const channel of channels) {
9
+ if (!LEGEND_CHANNELS.includes(channel)) throw new Error(`Unsupported legend channel "${channel}".`);
10
+ if (seen.has(channel)) throw new Error(`${operation} channels contains duplicate "${channel}".`);
11
+ seen.add(channel);
12
+ }
13
+ }
2
14
 
3
15
  export function legendTargets(program) {
4
16
  return [...new Set(Object.values(program.guideConfigs.legend ?? {})
@@ -0,0 +1,43 @@
1
+ import { isDiscretizedColorScaleType } from "../../../grammar/scales/types.js";
2
+ import { normalizeContinuousLegend } from "./continuous/common.js";
3
+ import { DEFAULT_GRADIENT_SIZE } from "./continuous/gradient.js";
4
+ import { normalizeIntervalLegend } from "./continuous/interval.js";
5
+
6
+ const family = type => type === "sequential" ? "gradient"
7
+ : isDiscretizedColorScaleType(type) ? "interval" : undefined;
8
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
9
+
10
+ export function planColorLegendTransition(program, scale, nextType) {
11
+ const from = family(scale.type), to = family(nextType);
12
+ if (from === undefined || to === undefined || from === to) return undefined;
13
+ const stored = program.guideConfigs.legend?.[from];
14
+ if (stored?.scale !== scale.id) return undefined;
15
+ const side = ["left", "right"].includes(stored.position);
16
+ const defaults = from === "gradient" ? normalizeContinuousLegend({ position: stored.position }, "gradient")
17
+ : normalizeIntervalLegend({ position: stored.position });
18
+ const custom = from === "gradient"
19
+ ? stored.count !== defaults.count || !same(stored.gradient, DEFAULT_GRADIENT_SIZE) || stored.itemGap !== defaults.itemGap
20
+ : !same(stored.symbol, defaults.symbol) || stored.itemGap !== defaults.itemGap ||
21
+ stored.direction !== defaults.direction || (stored.columns !== undefined && !(side && stored.columns === 1));
22
+ if (custom) {
23
+ throw new Error("Color legend transition cannot discard custom family settings; remove and recreate the legend explicitly.");
24
+ }
25
+ const args = {
26
+ target: stored.target, channels: ["color"], position: stored.position,
27
+ labels: stored.labels, titleStyle: stored.titleStyle, border: stored.border,
28
+ align: stored.align, offset: stored.offset, titlePosition: stored.titlePosition,
29
+ ...(stored.inferredTitle ? {} : { title: stored.title })
30
+ };
31
+ try {
32
+ if (to === "interval") normalizeIntervalLegend(args);
33
+ else normalizeContinuousLegend(args, "gradient");
34
+ } catch (error) {
35
+ throw new Error(`Color legend transition has incompatible layout or style: ${error.message}`);
36
+ }
37
+ return { args, titleVisible: stored.titleVisible !== false };
38
+ }
39
+
40
+ export function applyColorLegendTransition(program, plan) {
41
+ const next = program.createLegend(plan.args);
42
+ return plan.titleVisible ? next : next.editLegend({ target: plan.args.target, title: false });
43
+ }