ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,26 +1,32 @@
1
1
  import { isPlainObject } from "../../../../core/immutable.js";
2
2
  import { noOptions } from "../../../../core/validation.js";
3
3
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
4
- import { resolveGraphicBounds } from "../../../../layout/canvas.js";
5
4
  import { unionConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
6
5
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
+ import { formatVisibleText } from "../../../../core/textMetrics.js";
7
+ import {
8
+ assertLegendBoundsInsideCanvas,
9
+ formatContinuousValues,
10
+ resolveContinuousBounds,
11
+ resolveLegendBackgroundFromBounds,
12
+ resolveLegendTextBounds,
13
+ sampleContinuousValues
14
+ } from "../continuous/common.js";
7
15
  import {
8
16
  alignLegendStart,
9
17
  measureLegendSymbolHeight,
10
18
  measureLegendTextWidth,
11
19
  resolveLegendGrid
12
20
  } from "../../../../layout/legend.js";
13
- import { findCanvasGraphic } from
14
- "../../../../materialization/graphicHierarchy.js";
15
21
 
16
22
  export function activeConfig(program) {
17
- const entries = ["series", "color"]
18
- .filter(kind => program.guideConfigs.legend?.[kind] !== undefined)
19
- .map(kind => [kind, program.guideConfigs.legend[kind]]);
20
- if (entries.length !== 1) {
23
+ const kinds = ["series", "color"]
24
+ .filter(kind => program.guideConfigs.legend?.[kind] !== undefined);
25
+ if (kinds.length !== 1) {
21
26
  throw new Error("Legend component requires one categorical legend config.");
22
27
  }
23
- return { kind: entries[0][0], config: entries[0][1] };
28
+ const kind = kinds[0];
29
+ return { kind, config: program.guideConfigs.legend[kind] };
24
30
  }
25
31
 
26
32
  function prefix(config) {
@@ -55,38 +61,43 @@ function leftGuideBoundary(program, bounds) {
55
61
  return occupied === undefined ? bounds.x : Math.min(bounds.x, occupied.left);
56
62
  }
57
63
 
64
+ function textBounds(x, y, value, style, textAlign = "left") {
65
+ return resolveLegendTextBounds(
66
+ { x, y, align: textAlign },
67
+ formatVisibleText(value),
68
+ style
69
+ );
70
+ }
71
+
58
72
  function resolveLeftSizeMetrics(program, sizeConfig) {
59
73
  if (sizeConfig === undefined) return undefined;
60
74
  const scale = program.resolvedScales[sizeConfig.scale];
61
75
  if (scale?.type !== "linear") {
62
76
  throw new Error(`Legend requires resolved linear scale "${sizeConfig.scale}".`);
63
77
  }
64
- const values = Array.from(
65
- { length: sizeConfig.count },
66
- (_, index) => scale.domain[0] +
67
- index / (sizeConfig.count - 1) * (scale.domain[1] - scale.domain[0])
68
- );
69
- const labels = values.map(value => String(+value.toPrecision(3)));
78
+ const values = sampleContinuousValues(scale.domain, sizeConfig.count);
79
+ const labels = formatContinuousValues(values, scale.domain, "quantitative");
70
80
  const maximumRadius = Math.sqrt(Math.max(...scale.range) / Math.PI);
71
- return { scale, values, labels, maximumRadius };
81
+ return { values, labels, maximumRadius };
72
82
  }
73
83
 
74
84
  function resolveLeftLayout(program, bounds, canvas, config, width, count) {
75
85
  const sizeConfig = program.guideConfigs.legend?.size;
76
86
  const size = resolveLeftSizeMetrics(program, sizeConfig);
77
87
  const padding = config.border === false ? 0 : config.border.padding;
78
- const categoricalWidth = Math.max(
79
- config.titleVisible === false ? 0 : String(config.title).length * 6,
80
- ...config.domain.map(value =>
81
- width + config.labels.offset + measureLegendTextWidth(value)
82
- )
83
- );
88
+ const categoricalWidth = config.domain.reduce((maximum, value) => Math.max(
89
+ maximum,
90
+ width + config.labels.offset +
91
+ measureLegendTextWidth(value, config.labels)
92
+ ), config.titleVisible === false
93
+ ? 0
94
+ : measureLegendTextWidth(config.title, config.titleStyle));
84
95
  const sizeWidth = size === undefined
85
96
  ? 0
86
- : Math.max(
87
- String(sizeConfig.title).length * 6,
88
- ...size.labels.map(label => 44 + measureLegendTextWidth(label))
89
- );
97
+ : size.labels.reduce((maximum, label) => Math.max(
98
+ maximum,
99
+ 44 + measureLegendTextWidth(label, config.labels)
100
+ ), measureLegendTextWidth(sizeConfig.title, config.titleStyle));
90
101
  const contentWidth = Math.max(categoricalWidth, sizeWidth);
91
102
  const occupiedRight = bounds.x - config.offset;
92
103
  const contentRight = occupiedRight - padding;
@@ -122,7 +133,7 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
122
133
  Math.max(config.labels.fontSize, measureLegendSymbolHeight(config)) / 2;
123
134
  const blockTop = Math.min(categoricalTop, sizeTitleY - config.titleStyle.fontSize / 2);
124
135
  const blockBottom = Math.max(categoricalBottom, sizeBottom);
125
- if (blockTop < 0 || blockBottom > canvas.properties.height) {
136
+ if (blockTop < 0 || blockBottom > canvas.height) {
126
137
  throw new Error("Legend layout requires more vertical Canvas space.");
127
138
  }
128
139
  let background;
@@ -139,8 +150,8 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
139
150
  };
140
151
  if (
141
152
  background.x < 0 || background.y < 0 ||
142
- background.x + background.width > canvas.properties.width ||
143
- background.y + background.height > canvas.properties.height
153
+ background.x + background.width > canvas.width ||
154
+ background.y + background.height > canvas.height
144
155
  ) {
145
156
  throw new Error("Legend background requires more left-margin space.");
146
157
  }
@@ -155,7 +166,6 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
155
166
  blockTop,
156
167
  blockBottom,
157
168
  size: size === undefined ? undefined : {
158
- ...size,
159
169
  titleX: start,
160
170
  titleY: sizeTitleY,
161
171
  itemY: sizeItemY,
@@ -165,122 +175,65 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
165
175
  };
166
176
  }
167
177
 
168
- function resolveTopLayout(program, bounds, canvas, config, width, count) {
178
+ function resolveGridLayout(program, bounds, canvas, config, width, count, top) {
169
179
  const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
170
180
  resolveLegendGrid(config, width, count);
171
- const titleWidth = config.title.length * 7;
172
- const titleGap = config.titlePosition === "left" ? 20 : 12;
173
- const totalWidth = config.titlePosition === "left"
181
+ const inlineTitle = config.titlePosition === "left";
182
+ const titleWidth = config.titleVisible === false
183
+ ? 0
184
+ : measureLegendTextWidth(config.title, config.titleStyle);
185
+ const titleGap = inlineTitle ? 20 : 12;
186
+ const totalWidth = inlineTitle
174
187
  ? titleWidth + titleGap + gridWidth
175
188
  : Math.max(titleWidth, gridWidth);
176
189
  const start = alignLegendStart(bounds, totalWidth, config.align);
177
- if (start < 0 || start + totalWidth > canvas.properties.width) {
190
+ if (start < 0 || start + totalWidth > canvas.width) {
178
191
  throw new Error("Legend layout requires more horizontal Canvas space.");
179
192
  }
180
- const blockBottom = bounds.y - config.offset;
181
- const gridStart = config.titlePosition === "left"
193
+ const edge = top
194
+ ? bounds.y - config.offset
195
+ : bounds.y + bounds.height + config.offset;
196
+ const gridStart = inlineTitle
182
197
  ? start + titleWidth + titleGap
183
198
  : start + (totalWidth - gridWidth) / 2;
184
- const gridTop = blockBottom - gridHeight;
185
- const blockTop = config.titlePosition === "left"
186
- ? Math.min(gridTop, blockBottom / 2 + gridTop / 2 - config.titleStyle.fontSize / 2)
187
- : gridTop - titleGap - config.titleStyle.fontSize;
188
- if (blockTop < 0) {
189
- throw new Error("Legend layout requires more top-margin space.");
190
- }
191
- const titleGraphic = program.graphicSpec.objects.chartSubtitle ??
192
- program.graphicSpec.objects.chartTitle;
193
- if (
194
- titleGraphic?.type === "text" &&
195
- titleGraphic.properties.y + titleGraphic.properties.fontSize / 2 >= blockTop
196
- ) {
197
- throw new Error("Top legend and chart title require more top-margin space.");
198
- }
199
- const columnX = [];
200
- let cursor = gridStart;
201
- for (const columnWidth of columnWidths) {
202
- columnX.push(cursor);
203
- cursor += columnWidth + config.itemGap;
204
- }
205
- const symbolX = cells.map(cell => columnX[cell.column]);
206
- const labelX = symbolX.map(value => value + width + config.labels.offset);
207
- const itemY = cells.map(cell =>
208
- gridTop + rowHeight / 2 + cell.row * (rowHeight + config.itemGap)
209
- );
210
- const titleX = config.titlePosition === "left"
211
- ? start
212
- : start + totalWidth / 2;
213
- const titleY = config.titlePosition === "left"
214
- ? gridTop + gridHeight / 2
215
- : gridTop - titleGap - config.titleStyle.fontSize / 2;
216
- let background;
217
- if (config.border !== false) {
218
- const x = start - config.border.padding;
219
- const y = blockTop - config.border.padding;
220
- const backgroundWidth = totalWidth + config.border.padding * 2;
221
- const height = blockBottom - blockTop + config.border.padding * 2;
222
- if (x < 0 || y < 0 || x + backgroundWidth > canvas.properties.width) {
223
- throw new Error("Legend background requires more top or horizontal space.");
199
+ const gridTop = top
200
+ ? edge - gridHeight
201
+ : inlineTitle ? edge : edge + config.titleStyle.fontSize + titleGap;
202
+ const blockTop = top
203
+ ? inlineTitle
204
+ ? Math.min(
205
+ gridTop,
206
+ edge / 2 + gridTop / 2 - config.titleStyle.fontSize / 2
207
+ )
208
+ : gridTop - titleGap - config.titleStyle.fontSize
209
+ : edge;
210
+ const blockBottom = top ? edge : gridTop + gridHeight;
211
+ if (top) {
212
+ if (blockTop < 0) {
213
+ throw new Error("Legend layout requires more top-margin space.");
214
+ }
215
+ const titleGraphic = program.graphicSpec.objects.chartSubtitle ??
216
+ program.graphicSpec.objects.chartTitle;
217
+ if (titleGraphic?.type === "text" &&
218
+ titleGraphic.properties.y + titleGraphic.properties.fontSize / 2 >= blockTop) {
219
+ throw new Error("Top legend and chart title require more top-margin space.");
220
+ }
221
+ } else {
222
+ const occupiedTop = config.border === false
223
+ ? blockTop
224
+ : blockTop - config.border.padding;
225
+ const axisTitle = program.graphicSpec.objects.xAxisTitle;
226
+ if (axisTitle?.type === "text" &&
227
+ axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop) {
228
+ throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
229
+ }
230
+ const occupiedBottom = config.border === false
231
+ ? blockBottom
232
+ : blockBottom + config.border.padding;
233
+ if (occupiedTop < 0 || occupiedBottom > canvas.height) {
234
+ throw new Error("Legend layout requires more bottom-margin space.");
224
235
  }
225
- background = { x, y, width: backgroundWidth, height };
226
- }
227
- return {
228
- symbolX,
229
- itemY,
230
- labelX,
231
- titleX,
232
- titleY,
233
- background,
234
- blockTop,
235
- blockBottom
236
- };
237
- }
238
-
239
- function resolveBottomLayout(program, bounds, canvas, config, width, count) {
240
- const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
241
- resolveLegendGrid(config, width, count);
242
- const titleWidth = config.title.length * 7;
243
- const titleGap = config.titlePosition === "left" ? 20 : 12;
244
- const totalWidth = config.titlePosition === "left"
245
- ? titleWidth + titleGap + gridWidth
246
- : Math.max(titleWidth, gridWidth);
247
- const start = alignLegendStart(bounds, totalWidth, config.align);
248
- if (start < 0 || start + totalWidth > canvas.properties.width) {
249
- throw new Error("Legend layout requires more horizontal Canvas space.");
250
- }
251
-
252
- const blockTop = bounds.y + bounds.height + config.offset;
253
- const gridStart = config.titlePosition === "left"
254
- ? start + titleWidth + titleGap
255
- : start + (totalWidth - gridWidth) / 2;
256
- const gridTop = config.titlePosition === "left"
257
- ? blockTop
258
- : blockTop + config.titleStyle.fontSize + titleGap;
259
- const blockBottom = gridTop + gridHeight;
260
- const titleX = config.titlePosition === "left"
261
- ? start
262
- : start + totalWidth / 2;
263
- const titleY = config.titlePosition === "left"
264
- ? gridTop + gridHeight / 2
265
- : blockTop + config.titleStyle.fontSize / 2;
266
-
267
- const axisTitle = program.graphicSpec.objects.xAxisTitle;
268
- const occupiedTop = config.border === false
269
- ? blockTop
270
- : blockTop - config.border.padding;
271
- if (
272
- axisTitle?.type === "text" &&
273
- axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop
274
- ) {
275
- throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
276
- }
277
- const occupiedBottom = config.border === false
278
- ? blockBottom
279
- : blockBottom + config.border.padding;
280
- if (occupiedTop < 0 || occupiedBottom > canvas.properties.height) {
281
- throw new Error("Legend layout requires more bottom-margin space.");
282
236
  }
283
-
284
237
  const columnX = [];
285
238
  let cursor = gridStart;
286
239
  for (const columnWidth of columnWidths) {
@@ -292,6 +245,12 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
292
245
  const itemY = cells.map(cell =>
293
246
  gridTop + rowHeight / 2 + cell.row * (rowHeight + config.itemGap)
294
247
  );
248
+ const titleX = inlineTitle ? start : start + totalWidth / 2;
249
+ const titleY = inlineTitle
250
+ ? gridTop + gridHeight / 2
251
+ : top
252
+ ? gridTop - titleGap - config.titleStyle.fontSize / 2
253
+ : blockTop + config.titleStyle.fontSize / 2;
295
254
  let background;
296
255
  if (config.border !== false) {
297
256
  background = {
@@ -300,13 +259,10 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
300
259
  width: totalWidth + config.border.padding * 2,
301
260
  height: blockBottom - blockTop + config.border.padding * 2
302
261
  };
303
- if (
304
- background.x < 0 ||
305
- background.y < 0 ||
306
- background.x + background.width > canvas.properties.width ||
307
- background.y + background.height > canvas.properties.height
308
- ) {
309
- throw new Error("Legend background requires more bottom or horizontal space.");
262
+ if (background.x < 0 || background.y < 0 ||
263
+ background.x + background.width > canvas.width ||
264
+ (!top && background.y + background.height > canvas.height)) {
265
+ throw new Error(`Legend background requires more ${top ? "top" : "bottom"} or horizontal space.`);
310
266
  }
311
267
  }
312
268
  return {
@@ -322,16 +278,17 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
322
278
  }
323
279
 
324
280
  function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
325
- const labels = config.domain.map(String);
281
+ const labels = config.domain.map(formatVisibleText);
326
282
  const itemWidths = labels.map(
327
- label => width + config.labels.offset + label.length * 7
283
+ label => width + config.labels.offset +
284
+ measureLegendTextWidth(label, config.labels)
328
285
  );
329
286
  const totalWidth = itemWidths.reduce((sum, value) => sum + value, 0) +
330
287
  config.itemGap * Math.max(0, count - 1);
331
288
  const start = config.align === "center"
332
- ? (canvas.properties.width - totalWidth) / 2
289
+ ? (canvas.width - totalWidth) / 2
333
290
  : alignLegendStart(bounds, totalWidth, config.align);
334
- if (start < 0 || start + totalWidth > canvas.properties.width) {
291
+ if (start < 0 || start + totalWidth > canvas.width) {
335
292
  throw new Error("Legend layout requires more horizontal Canvas space.");
336
293
  }
337
294
  const symbolX = [];
@@ -342,9 +299,9 @@ function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
342
299
  labelX.push(cursor + width + config.labels.offset);
343
300
  cursor += itemWidths[index] + config.itemGap;
344
301
  }
345
- const itemY = Array(count).fill(canvas.properties.height - 28);
302
+ const itemY = Array(count).fill(canvas.height - 28);
346
303
  const titleX = start + totalWidth / 2;
347
- const titleY = canvas.properties.height - 52;
304
+ const titleY = canvas.height - 52;
348
305
  if (titleY <= bounds.y + bounds.height) {
349
306
  throw new Error("Legend layout requires more bottom-margin space.");
350
307
  }
@@ -362,22 +319,24 @@ function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
362
319
  width: totalWidth + config.border.padding * 2,
363
320
  height: itemY[0] + maxHeight / 2 + config.border.padding - y
364
321
  };
322
+ if (
323
+ background.x < 0 || background.y < 0 ||
324
+ background.x + background.width > canvas.width ||
325
+ background.y + background.height > canvas.height
326
+ ) {
327
+ throw new Error("Legend background requires more bottom or horizontal space.");
328
+ }
365
329
  }
366
330
  return { symbolX, itemY, labelX, titleX, titleY, background };
367
331
  }
368
332
 
369
333
  export function resolveLayout(program, config) {
370
- const bounds = resolveGraphicBounds(program);
371
- const canvas = findCanvasGraphic(program);
372
- if (
373
- bounds === undefined ||
374
- ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
375
- canvas?.type !== "canvas" ||
376
- !Number.isFinite(canvas.properties.width) ||
377
- !Number.isFinite(canvas.properties.height)
378
- ) {
379
- throw new Error("Legend layout requires Canvas bounds, width, and height.");
380
- }
334
+ const resolved = resolveContinuousBounds(
335
+ program,
336
+ "Legend layout requires Canvas bounds, width, and height."
337
+ );
338
+ const bounds = resolved.plot;
339
+ const canvas = resolved.canvas;
381
340
 
382
341
  const count = config.domain.length;
383
342
  const width = symbolWidth(config);
@@ -390,23 +349,37 @@ export function resolveLayout(program, config) {
390
349
  (_, index) => bounds.y + 52 + index * config.itemGap
391
350
  );
392
351
  const labelX = symbolX.map(value => value + width + config.labels.offset);
393
- if (labelX.some(value => value >= canvas.properties.width)) {
394
- throw new Error("Legend layout requires more right-margin space.");
395
- }
396
352
  const titleX = symbolX[0];
397
353
  const titleY = bounds.y + 20;
398
- let background;
399
- if (config.border !== false) {
400
- const x = symbolX[0] - config.border.padding;
401
- const y = bounds.y + 8;
402
- const backgroundWidth =
403
- canvas.properties.width - x - config.border.padding;
404
- const height = itemY.at(-1) - y + config.border.padding;
405
- if (backgroundWidth <= 0 || height <= 0) {
406
- throw new Error("Legend background requires positive width and height.");
407
- }
408
- background = { x, y, width: backgroundWidth, height };
409
- }
354
+ const labelBounds = config.domain.map((value, index) => textBounds(
355
+ labelX[index], itemY[index], value, config.labels
356
+ ));
357
+ const titleBounds = config.titleVisible === false
358
+ ? undefined
359
+ : textBounds(titleX, titleY, config.title, config.titleStyle);
360
+ const symbolHeight = measureLegendSymbolHeight(config);
361
+ const symbolBounds = itemY.map(y => ({
362
+ left: symbolX[0],
363
+ right: symbolX[0] + width,
364
+ top: y - symbolHeight / 2,
365
+ bottom: y + symbolHeight / 2
366
+ }));
367
+ const contentBounds = [
368
+ ...labelBounds,
369
+ ...symbolBounds,
370
+ ...(titleBounds === undefined ? [] : [titleBounds])
371
+ ];
372
+ assertLegendBoundsInsideCanvas(
373
+ contentBounds,
374
+ canvas,
375
+ "Right legend layout"
376
+ );
377
+ const background = resolveLegendBackgroundFromBounds(
378
+ contentBounds,
379
+ config.border,
380
+ canvas,
381
+ "Right legend"
382
+ );
410
383
  return { symbolX, itemY, labelX, titleX, titleY, background };
411
384
  }
412
385
 
@@ -415,13 +388,13 @@ export function resolveLayout(program, config) {
415
388
  }
416
389
 
417
390
  if (config.position === "top") {
418
- return resolveTopLayout(program, bounds, canvas, config, width, count);
391
+ return resolveGridLayout(program, bounds, canvas, config, width, count, true);
419
392
  }
420
393
 
421
394
  if (config.bottomGrid !== true) {
422
395
  return resolveCompactBottomLayout(bounds, canvas, config, width, count);
423
396
  }
424
- return resolveBottomLayout(program, bounds, canvas, config, width, count);
397
+ return resolveGridLayout(program, bounds, canvas, config, width, count, false);
425
398
  }
426
399
 
427
400
  export function resolveAppearance(program, config) {
@@ -99,6 +99,11 @@ export function normalizeRecipe(symbol, kind) {
99
99
  if (!Array.isArray(symbol.layers) || symbol.layers.length === 0) {
100
100
  throw new TypeError("Legend symbol layers must be a non-empty array.");
101
101
  }
102
+ if (symbol.layers.length > 3) {
103
+ throw new Error(
104
+ "Legend symbol recipe supports at most one layer per type."
105
+ );
106
+ }
102
107
  layers = symbol.layers.map(normalizeLayer);
103
108
  } else if (kind === "series") {
104
109
  validateKeys(symbol, ["length", "lineWidth"], "createLegend.symbol");
@@ -1,5 +1,8 @@
1
1
  import { validateUserId } from "../../../../core/identifiers.js";
2
- import { sameOrderedValues } from "../../../../core/validation.js";
2
+ import {
3
+ sameOrderedValues,
4
+ validateGeneratedItemLimit
5
+ } from "../../../../core/validation.js";
3
6
  import { CHANNELS } from "./options.js";
4
7
  import { nonEmptyString } from "./validation.js";
5
8
  import { findLayer } from "../../../../selectors/layers.js";
@@ -59,6 +62,10 @@ function resolveOrdinalScales(program, scaleIds) {
59
62
  if (!Array.isArray(concrete.domain) || concrete.domain.length === 0) {
60
63
  throw new Error(`Legend scale "${id}" requires a non-empty domain.`);
61
64
  }
65
+ validateGeneratedItemLimit(
66
+ concrete.domain.length,
67
+ "Categorical legend item count"
68
+ );
62
69
  return concrete;
63
70
  });
64
71
  if (scales.slice(1).some(scale => !sameValues(scale.domain, scales[0].domain))) {