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,8 +1,10 @@
1
+ import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
1
2
  import { action } from "../../../../core/action.js";
2
3
  import { isPlainObject } from "../../../../core/immutable.js";
3
4
  import {
4
5
  validateKeys,
5
- validateOptionObject
6
+ validateOptionObject,
7
+ validateNonEmptyString
6
8
  } from "../../../../core/validation.js";
7
9
  import { formatDiscretizedIntervals } from "../../../../grammar/scales/index.js";
8
10
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
@@ -12,11 +14,12 @@ import {
12
14
  editGraphicProperties,
13
15
  editLegendBackground,
14
16
  normalizeLegendBorder,
17
+ normalizeItemLegendLayout,
15
18
  normalizeLegendTextOptions,
19
+ normalizeLegendTitleOptions,
16
20
  resolveContinuousBounds,
17
- resolveContinuousPoint,
21
+ resolveContinuousColorLayer,
18
22
  resolveLegendBackgroundFromBounds,
19
- resolveLegendTextBounds,
20
23
  styleContinuousText,
21
24
  validateNonNegative,
22
25
  validatePositive
@@ -26,7 +29,7 @@ import { resolveLegendGraphicPlacement } from
26
29
 
27
30
  const OPTIONS = [
28
31
  "target", "channels", "position", "align", "offset", "title",
29
- "symbol", "labels", "titleStyle", "itemGap", "direction", "border"
32
+ "symbol", "labels", "titleStyle", "itemGap", "direction", "border", "layout", "columns", "titlePosition"
30
33
  ];
31
34
  const SYMBOL_OPTIONS = [
32
35
  "width", "height", "stroke", "strokeWidth"
@@ -41,12 +44,7 @@ export function normalizeIntervalLegend(args) {
41
44
  )) {
42
45
  throw new Error('Interval legend requires channels: ["color"].');
43
46
  }
44
- if ((args.position ?? "right") !== "right") {
45
- throw new Error('Interval legends currently support position "right".');
46
- }
47
- if ((args.direction ?? "vertical") !== "vertical") {
48
- throw new Error('Interval legends currently support direction "vertical".');
49
- }
47
+ const layout = normalizeItemLegendLayout(args);
50
48
  if (args.symbol !== undefined && !isPlainObject(args.symbol)) {
51
49
  throw new TypeError("createLegend.symbol must be a plain object.");
52
50
  }
@@ -61,17 +59,11 @@ export function normalizeIntervalLegend(args) {
61
59
  validatePositive(symbol.width, "Legend symbol width");
62
60
  validatePositive(symbol.height, "Legend symbol height");
63
61
  validateNonNegative(symbol.strokeWidth, "Legend symbol strokeWidth");
64
- const offset = args.offset ?? 30;
65
- const itemGap = args.itemGap ?? 28;
66
- validateNonNegative(offset, "Legend offset");
67
- validatePositive(itemGap, "Legend itemGap");
62
+ if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
63
+ validateNonEmptyString(symbol.stroke, "Legend symbol stroke");
68
64
  return {
69
65
  target: args.target,
70
- position: "right",
71
- direction: "vertical",
72
- align: args.align ?? "center",
73
- offset,
74
- itemGap,
66
+ ...layout,
75
67
  title: args.title,
76
68
  inferredTitle: args.title === undefined,
77
69
  titleVisible: true,
@@ -83,7 +75,7 @@ export function normalizeIntervalLegend(args) {
83
75
  fontFamily: DEFAULT_FONT_FAMILY,
84
76
  fontWeight: "normal"
85
77
  }),
86
- titleStyle: normalizeLegendTextOptions(args.titleStyle, "createLegend.titleStyle", {
78
+ titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", {
87
79
  color: DEFAULT_COLORS.text,
88
80
  fontSize: 13,
89
81
  fontFamily: DEFAULT_FONT_FAMILY,
@@ -93,8 +85,8 @@ export function normalizeIntervalLegend(args) {
93
85
  };
94
86
  }
95
87
 
96
- function resolveIntervalConfig(program, stored) {
97
- const layer = resolveContinuousPoint(program, stored.target, "color");
88
+ export function resolveIntervalConfig(program, stored) {
89
+ const layer = resolveContinuousColorLayer(program, stored.target);
98
90
  const encoding = layer.encoding.color;
99
91
  if (encoding.fieldType !== "quantitative") {
100
92
  throw new Error("Interval legend requires quantitative color.");
@@ -111,47 +103,26 @@ function resolveIntervalConfig(program, stored) {
111
103
  } };
112
104
  }
113
105
 
114
- function resolveIntervalLayout(program, config, scale) {
106
+ export function resolveIntervalLayout(program, config, scale) {
115
107
  const { plot, canvas } = resolveContinuousBounds(program);
116
- const labels = formatDiscretizedIntervals(scale.thresholds);
117
- const symbolX = plot.x + plot.width + config.offset;
118
- const itemY = labels.map((_, index) => plot.y + 52 + index * config.itemGap);
119
- const labelX = symbolX + config.symbol.width + config.labels.offset;
120
- const title = { x: symbolX, y: plot.y + 20 };
121
- const strokeExtent = config.symbol.strokeWidth / 2;
122
- const occupiedBounds = [
123
- resolveLegendTextBounds(
124
- { ...title, align: "left" },
125
- config.title,
126
- config.titleStyle
127
- ),
128
- ...labels.map((label, index) => resolveLegendTextBounds(
129
- { x: labelX, y: itemY[index], align: "left" },
130
- label,
131
- config.labels
132
- )),
133
- ...itemY.map(y => ({
134
- left: symbolX - strokeExtent,
135
- right: symbolX + config.symbol.width + strokeExtent,
136
- top: y - config.symbol.height / 2 - strokeExtent,
137
- bottom: y + config.symbol.height / 2 + strokeExtent
138
- }))
139
- ];
108
+ const labels = formatDiscretizedIntervals(scale.thresholds, config.labels.format);
109
+ const layout = resolveLegendItemLayout(plot, config, labels, config.symbol);
110
+ const occupiedBounds = layout.bounds;
140
111
  assertLegendBoundsInsideCanvas(
141
112
  occupiedBounds,
142
113
  canvas,
143
- "Interval legend layout"
114
+ "Interval legend layout", config
144
115
  );
145
116
  const background = resolveLegendBackgroundFromBounds(
146
117
  occupiedBounds,
147
118
  config.border,
148
119
  canvas,
149
- "Interval legend"
120
+ "Interval legend", config
150
121
  );
151
- return { labels, symbolX, labelX, itemY, title, background };
122
+ return { labels, ...layout, background };
152
123
  }
153
124
 
154
- export const rematerializeIntervalLegend = action(
125
+ export const rematerializeIntervalLegend = /* @__PURE__ */ action(
155
126
  {
156
127
  op: "rematerializeIntervalLegend",
157
128
  description: "Rematerialize a discretized color interval legend."
@@ -196,11 +167,57 @@ export const rematerializeIntervalLegend = action(
196
167
  y: layout.title.y,
197
168
  text: config.title
198
169
  });
199
- return styleContinuousText(next, "colorLegendTitle", config.titleStyle);
170
+ return styleContinuousText(next, "colorLegendTitle", config.titleStyle, { align: layout.title.align });
200
171
  }
201
172
  );
202
173
 
203
- export const createIntervalLegend = action(
174
+ export function createIntervalLegendFromConfig(program, config) {
175
+ const resolved = resolveIntervalConfig(program, config);
176
+ resolveIntervalLayout(program, resolved.config, resolved.scale);
177
+ let next = program
178
+ .editSemantic({
179
+ property: "guide.legend.color.scale",
180
+ value: resolved.encoding.scale
181
+ })
182
+ .editSemantic({
183
+ property: "guide.legend.color.title",
184
+ value: resolved.config.title
185
+ })
186
+ ._withLegendConfig("interval", resolved.config);
187
+ const placement = resolveLegendGraphicPlacement(next);
188
+ if (resolved.config.border !== false) {
189
+ next = next.createGraphics({
190
+ id: "colorLegendBackground",
191
+ type: "rect",
192
+ ...placement
193
+ });
194
+ }
195
+ next = next
196
+ .createGraphics({
197
+ id: "colorLegendSymbols",
198
+ type: "rect",
199
+ length: 0,
200
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
201
+ ? {}
202
+ : { after: "colorLegendBackground" })
203
+ })
204
+ .createGraphics({
205
+ id: "colorLegendLabels",
206
+ type: "text",
207
+ length: 0,
208
+ ...placement
209
+ });
210
+ if (resolved.config.titleVisible !== false) {
211
+ next = next.createGraphics({
212
+ id: "colorLegendTitle",
213
+ type: "text",
214
+ ...placement
215
+ });
216
+ }
217
+ return next.rematerializeIntervalLegend();
218
+ }
219
+
220
+ export const createIntervalLegend = /* @__PURE__ */ action(
204
221
  {
205
222
  op: "createIntervalLegend",
206
223
  description: "Create a discretized color interval legend."
@@ -212,44 +229,6 @@ export const createIntervalLegend = action(
212
229
  if (this.graphicSpec.objects.colorLegendSymbols !== undefined) {
213
230
  throw new Error("createIntervalLegend requires a missing interval legend.");
214
231
  }
215
- let next = this
216
- .editSemantic({
217
- property: "guide.legend.color.scale",
218
- value: resolved.encoding.scale
219
- })
220
- .editSemantic({
221
- property: "guide.legend.color.title",
222
- value: resolved.config.title
223
- })
224
- ._withLegendConfig("interval", resolved.config);
225
- const placement = resolveLegendGraphicPlacement(next);
226
- if (resolved.config.border !== false) {
227
- next = next.createGraphics({
228
- id: "colorLegendBackground",
229
- type: "rect",
230
- ...placement
231
- });
232
- }
233
- return next
234
- .createGraphics({
235
- id: "colorLegendSymbols",
236
- type: "rect",
237
- length: 0,
238
- ...resolveLegendGraphicPlacement(next, resolved.config.border === false
239
- ? {}
240
- : { after: "colorLegendBackground" })
241
- })
242
- .createGraphics({
243
- id: "colorLegendLabels",
244
- type: "text",
245
- length: 0,
246
- ...placement
247
- })
248
- .createGraphics({
249
- id: "colorLegendTitle",
250
- type: "text",
251
- ...placement
252
- })
253
- .rematerializeIntervalLegend();
232
+ return createIntervalLegendFromConfig(this, resolved.config);
254
233
  }
255
234
  );
@@ -11,7 +11,7 @@ import {
11
11
  normalizeContinuousLegend,
12
12
  requireResolvedLegendScale,
13
13
  resolveContinuousBounds,
14
- resolveContinuousPoint,
14
+ resolveContinuousLegendLayer,
15
15
  resolveLegendBackgroundFromBounds,
16
16
  resolveLegendTextBounds,
17
17
  sampleContinuousValues,
@@ -62,7 +62,7 @@ export function normalizeOpacitySymbol(value) {
62
62
  }
63
63
 
64
64
  function resolveOpacityConfig(program, config) {
65
- const layer = resolveContinuousPoint(program, config.target, "opacity");
65
+ const layer = resolveContinuousLegendLayer(program, config.target, "opacity");
66
66
  const encoding = layer.encoding.opacity;
67
67
  if (encoding.fieldType !== "quantitative") {
68
68
  throw new Error("Opacity legend requires quantitative field opacity.");
@@ -86,8 +86,14 @@ function resolveOpacityLayout(program, config, scale) {
86
86
  const { plot, canvas } = resolveContinuousBounds(program);
87
87
  const vertical = ["right", "left"].includes(config.position);
88
88
  const values = sampleContinuousValues(scale.domain, config.count);
89
- const texts = formatContinuousValues(values, scale.domain, "quantitative");
90
- const radius = config.symbol.radius;
89
+ const texts = formatContinuousValues(
90
+ values,
91
+ scale.domain,
92
+ "quantitative",
93
+ config.labels.format
94
+ );
95
+ const symbolExtent = config.symbol.radius + (config.symbol.strokeWidth ?? 0) / 2;
96
+ const labelWidths = texts.map(text => measureTextWidth(text, config.labels));
91
97
  let symbols;
92
98
  let labels;
93
99
  let title;
@@ -95,57 +101,61 @@ function resolveOpacityLayout(program, config, scale) {
95
101
  const baseX = config.position === "right"
96
102
  ? plot.x + plot.width + config.offset
97
103
  : plot.x - config.offset;
98
- const y = values.map((_, index) => plot.y + 46 + index * config.itemGap);
104
+ const itemHeight = Math.max(symbolExtent * 2, config.labels.fontSize);
105
+ const pitch = Math.max(config.itemGap, itemHeight);
106
+ const firstY = Math.max(plot.y + 46, config.titleVisible === false ? plot.y + 46
107
+ : plot.y + 20 + config.titleStyle.fontSize / 2 + 12 + itemHeight / 2);
108
+ const y = values.map((_, index) => firstY + index * pitch);
99
109
  symbols = y.map(itemY => ({
100
- x: config.position === "right" ? baseX + radius : baseX - radius,
110
+ x: config.position === "right" ? baseX + symbolExtent : baseX - symbolExtent,
101
111
  y: itemY
102
112
  }));
103
113
  labels = y.map(itemY => ({
104
114
  x: config.position === "right"
105
- ? baseX + radius * 2 + config.labels.offset - 2
106
- : baseX - radius * 2 - config.labels.offset + 2,
115
+ ? baseX + symbolExtent * 2 + config.labels.offset
116
+ : baseX - symbolExtent * 2 - config.labels.offset,
107
117
  y: itemY,
108
118
  align: config.position === "right" ? "left" : "right"
109
119
  }));
110
120
  title = {
111
- x: config.position === "right" ? baseX : baseX - radius * 2,
121
+ x: config.position === "right" ? baseX : baseX - symbolExtent * 2,
112
122
  y: plot.y + 20,
113
123
  align: config.position === "right" ? "left" : "right"
114
124
  };
115
125
  } else if (config.titlePosition === "left") {
116
- const titleWidth = measureTextWidth(config.title, config.titleStyle);
117
- const labelWidths = texts.map(text =>
118
- measureTextWidth(text, config.labels)
119
- );
126
+ const titleWidth = config.titleVisible === false ? 0 : measureTextWidth(config.title, config.titleStyle);
127
+ const titlePrefix = config.titleVisible === false ? 0 : titleWidth + 20;
120
128
  const samplesWidth = labelWidths.reduce(
121
- (sum, width) => sum + radius * 2 + config.labels.offset + width,
129
+ (sum, width) => sum + symbolExtent * 2 + config.labels.offset + width,
122
130
  0
123
131
  ) + config.itemGap * (values.length - 1);
124
- const width = titleWidth + 20 + samplesWidth;
132
+ const width = titlePrefix + samplesWidth;
125
133
  const startX = config.align === "left" ? plot.x
126
134
  : config.align === "right" ? plot.x + plot.width - width
127
135
  : plot.x + (plot.width - width) / 2;
128
136
  const height = Math.max(
129
- radius * 2,
137
+ symbolExtent * 2,
130
138
  config.labels.fontSize,
131
- config.titleStyle.fontSize
139
+ config.titleVisible === false ? 0 : config.titleStyle.fontSize
132
140
  );
133
141
  const centerY = config.position === "top"
134
142
  ? plot.y - config.offset - height / 2
135
143
  : plot.y + plot.height + config.offset + height / 2;
136
144
  title = { x: startX, y: centerY, align: "left" };
137
- let cursor = startX + titleWidth + 20;
145
+ let cursor = startX + titlePrefix;
138
146
  symbols = [];
139
147
  labels = [];
140
148
  for (let index = 0; index < values.length; index += 1) {
141
- const symbolX = cursor + radius;
149
+ const symbolX = cursor + symbolExtent;
142
150
  symbols.push({ x: symbolX, y: centerY });
143
- const labelX = symbolX + radius + config.labels.offset;
151
+ const labelX = symbolX + symbolExtent + config.labels.offset;
144
152
  labels.push({ x: labelX, y: centerY, align: "left" });
145
153
  cursor = labelX + labelWidths[index] + config.itemGap;
146
154
  }
147
155
  } else {
148
- const width = (values.length - 1) * Math.max(56, config.itemGap * 2);
156
+ const pitch = Math.max(56, config.itemGap * 2,
157
+ symbolExtent * 2 + config.itemGap, Math.max(...labelWidths) + config.itemGap);
158
+ const width = (values.length - 1) * pitch;
149
159
  const startX = config.align === "left" ? plot.x
150
160
  : config.align === "right" ? plot.x + plot.width - width
151
161
  : plot.x + (plot.width - width) / 2;
@@ -153,27 +163,25 @@ function resolveOpacityLayout(program, config, scale) {
153
163
  const labelHalf = config.labels.fontSize / 2;
154
164
  const y = config.position === "top"
155
165
  ? plot.y - config.offset - config.labels.fontSize -
156
- config.labels.offset - radius
166
+ config.labels.offset - symbolExtent
157
167
  : plot.y + plot.height + config.offset +
158
- config.titleStyle.fontSize + 12 + radius;
168
+ (config.titleVisible === false ? 0 : config.titleStyle.fontSize + 12) + symbolExtent;
159
169
  symbols = values.map((_, index) => ({
160
170
  x: startX + index * width / (values.length - 1),
161
171
  y
162
172
  }));
163
173
  labels = symbols.map(symbol => ({
164
174
  x: symbol.x,
165
- y: symbol.y + radius + config.labels.offset + labelHalf,
175
+ y: symbol.y + symbolExtent + config.labels.offset + labelHalf,
166
176
  align: "center"
167
177
  }));
168
178
  title = {
169
179
  x: startX + width / 2,
170
- y: y - radius - 12 - titleHalf,
180
+ y: y - symbolExtent - 12 - titleHalf,
171
181
  align: "center"
172
182
  };
173
183
  }
174
- const strokeExtent = (config.symbol.strokeWidth ?? 0) / 2;
175
- const symbolExtent = radius + strokeExtent;
176
- const titleBounds = resolveLegendTextBounds(
184
+ const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
177
185
  title,
178
186
  config.title,
179
187
  config.titleStyle
@@ -187,22 +195,22 @@ function resolveOpacityLayout(program, config, scale) {
187
195
  top: symbol.y - symbolExtent,
188
196
  bottom: symbol.y + symbolExtent
189
197
  }));
190
- const occupiedBounds = [titleBounds, ...labelBounds, ...symbolBounds];
198
+ const occupiedBounds = [...(config.titleVisible === false ? [] : [titleBounds]), ...labelBounds, ...symbolBounds];
191
199
  assertLegendBoundsInsideCanvas(
192
200
  occupiedBounds,
193
201
  canvas,
194
- "Opacity legend layout"
202
+ "Opacity legend layout", config
195
203
  );
196
204
  const background = resolveLegendBackgroundFromBounds(
197
205
  occupiedBounds,
198
206
  config.border,
199
207
  canvas,
200
- "Opacity legend"
208
+ "Opacity legend", config
201
209
  );
202
210
  return { values, texts, symbols, labels, title, background };
203
211
  }
204
212
 
205
- export const rematerializeOpacityLegend = action(
213
+ export const rematerializeOpacityLegend = /* @__PURE__ */ action(
206
214
  {
207
215
  op: "rematerializeOpacityLegend",
208
216
  description: "Rematerialize a field-opacity sample legend."
@@ -330,72 +338,85 @@ export const rematerializeOpacityLegend = action(
330
338
  }
331
339
  );
332
340
 
333
- export const createOpacityLegend = action(
341
+
342
+ export function resolveOpacityLegendCreation(program, args = {}) {
343
+ const config = normalizeContinuousLegend(args, "opacity");
344
+ if (args.channels !== undefined && (
345
+ !Array.isArray(args.channels) ||
346
+ args.channels.length !== 1 ||
347
+ args.channels[0] !== "opacity"
348
+ )) {
349
+ throw new Error('Opacity legend requires channels: ["opacity"].');
350
+ }
351
+ config.symbol = normalizeOpacitySymbol(args.symbol);
352
+ config.titleVisible = true;
353
+ const resolved = resolveOpacityConfig(program, config);
354
+ return resolved;
355
+ }
356
+
357
+ export function createOpacityLegendFromConfig(program, config) {
358
+ const resolved = resolveOpacityConfig(program, config);
359
+ resolveOpacityLayout(program, resolved.config, resolved.scale);
360
+ let next = program
361
+ .editSemantic({
362
+ property: "guide.legend.opacity.scale",
363
+ value: resolved.encoding.scale
364
+ })
365
+ .editSemantic({
366
+ property: "guide.legend.opacity.title",
367
+ value: resolved.config.title
368
+ })
369
+ ._withLegendConfig("opacity", resolved.config);
370
+ if (resolved.config.border !== false) {
371
+ next = next.createGraphics({
372
+ id: "opacityLegendBackground",
373
+ type: "rect",
374
+ ...resolveLegendGraphicPlacement(next)
375
+ });
376
+ }
377
+ next = next
378
+ .createGraphics({
379
+ id: "opacityLegendSymbols",
380
+ type: "circle",
381
+ length: 0,
382
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
383
+ ? {}
384
+ : { after: "opacityLegendBackground" })
385
+ })
386
+ .createGraphics({
387
+ id: "opacityLegendLabels",
388
+ type: "text",
389
+ length: 0,
390
+ ...resolveLegendGraphicPlacement(next)
391
+ });
392
+ if (resolved.config.titleVisible !== false) {
393
+ next = next.createGraphics({
394
+ id: "opacityLegendTitle",
395
+ type: "text",
396
+ ...resolveLegendGraphicPlacement(next)
397
+ });
398
+ }
399
+ return next.rematerializeOpacityLegend();
400
+ }
401
+
402
+ export const createOpacityLegend = /* @__PURE__ */ action(
334
403
  {
335
404
  op: "createOpacityLegend",
336
405
  description: "Create a field-opacity sample legend."
337
406
  },
338
407
  function (args = {}) {
339
- const config = normalizeContinuousLegend(args, "opacity");
340
- if (args.channels !== undefined && (
341
- !Array.isArray(args.channels) ||
342
- args.channels.length !== 1 ||
343
- args.channels[0] !== "opacity"
344
- )) {
345
- throw new Error('Opacity legend requires channels: ["opacity"].');
346
- }
347
- config.symbol = normalizeOpacitySymbol(args.symbol);
348
- config.titleVisible = true;
349
- const resolved = resolveOpacityConfig(this, config);
408
+ const resolved = resolveOpacityLegendCreation(this, args);
350
409
  resolveOpacityLayout(this, resolved.config, resolved.scale);
351
410
  if (this.graphicSpec.objects.opacityLegendSymbols !== undefined) {
352
411
  throw new Error(
353
412
  "createOpacityLegend requires a missing opacity legend."
354
413
  );
355
414
  }
356
- let next = this
357
- .editSemantic({
358
- property: "guide.legend.opacity.scale",
359
- value: resolved.encoding.scale
360
- })
361
- .editSemantic({
362
- property: "guide.legend.opacity.title",
363
- value: resolved.config.title
364
- })
365
- ._withLegendConfig("opacity", resolved.config);
366
- if (resolved.config.border !== false) {
367
- next = next.createGraphics({
368
- id: "opacityLegendBackground",
369
- type: "rect",
370
- ...resolveLegendGraphicPlacement(next)
371
- });
372
- }
373
- return next
374
- .createGraphics({
375
- id: "opacityLegendSymbols",
376
- type: "circle",
377
- length: 0,
378
- ...resolveLegendGraphicPlacement(next),
379
- ...(resolved.config.border === false
380
- ? {}
381
- : { after: "opacityLegendBackground" })
382
- })
383
- .createGraphics({
384
- id: "opacityLegendLabels",
385
- type: "text",
386
- length: 0,
387
- ...resolveLegendGraphicPlacement(next)
388
- })
389
- .createGraphics({
390
- id: "opacityLegendTitle",
391
- type: "text",
392
- ...resolveLegendGraphicPlacement(next)
393
- })
394
- .rematerializeOpacityLegend();
415
+ return createOpacityLegendFromConfig(this, resolved.config);
395
416
  }
396
417
  );
397
418
 
398
- export const removeOpacityLegend = action(
419
+ export const removeOpacityLegend = /* @__PURE__ */ action(
399
420
  {
400
421
  op: "removeOpacityLegend",
401
422
  description: "Remove a field-opacity legend after switching to constant opacity."
@@ -0,0 +1,27 @@
1
+ import { resolveCategoricalLegendConfig } from "./categorical/actions.js";
2
+ import { resolveGradientLegendCreation } from "./continuous/gradient.js";
3
+ import { resolveOpacityLegendCreation } from "./continuous/opacity.js";
4
+ import { normalizeIntervalLegend, resolveIntervalConfig } from "./continuous/interval.js";
5
+ import { resolveSizeLegendConfig } from "./size.js";
6
+ import { resolveStrokeWidthLegendConfig } from "./strokeWidth.js";
7
+
8
+ export function resolveLegendStepConfig(program, step) {
9
+ switch (step.op) {
10
+ case "createCategoricalLegend": {
11
+ const config = resolveCategoricalLegendConfig(program, step.args);
12
+ return { kind: config.kind, config };
13
+ }
14
+ case "createGradientLegend":
15
+ return { kind: "gradient", config: resolveGradientLegendCreation(program, step.args).config };
16
+ case "createOpacityLegend":
17
+ return { kind: "opacity", config: resolveOpacityLegendCreation(program, step.args).config };
18
+ case "createIntervalLegend":
19
+ return { kind: "interval", config: resolveIntervalConfig(program, normalizeIntervalLegend(step.args)).config };
20
+ case "createSizeLegend":
21
+ return { kind: "size", config: resolveSizeLegendConfig(program, step.args) };
22
+ case "createStrokeWidthLegend":
23
+ return { kind: "strokeWidth", config: resolveStrokeWidthLegendConfig(program, step.args) };
24
+ default: throw new Error(`Unsupported legend creation owner "${step.op}".`);
25
+ }
26
+ }
27
+