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,11 +1,15 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
2
  import { validateBoxTransform } from "./boxPlot.js";
3
+ import { validateBinTransform } from "./bin.js";
3
4
  import {
4
5
  requestedBin2DTransform,
5
6
  validateBin2DTransform
6
7
  } from "./bin2d.js";
7
8
  import { validateDensityTransform } from "./density.js";
9
+ import { validateECDFTransform } from "./ecdf.js";
10
+ import { validateComputedTransform } from "./computed.js";
8
11
  import { validateFilterTransform } from "./filter.js";
12
+ import { validateFoldTransform } from "./fold.js";
9
13
  import {
10
14
  requestedGradientProfileTransform,
11
15
  validateGradientProfileTransform
@@ -19,6 +23,8 @@ import { validateMarkFilterTransform } from "./markFilter.js";
19
23
  import { validateRegressionTransform } from "./regression/index.js";
20
24
  import { validateWindowTransform } from "./window.js";
21
25
  import { validateTimeUnitTransform } from "./timeUnit.js";
26
+ import { validateSummaryTransform } from "./summary.js";
27
+ import { validateStackTransform } from "./stack.js";
22
28
  import { findTransformTopology } from "./transformTopology.js";
23
29
 
24
30
  function requestedDensityTransform(transform) {
@@ -44,12 +50,22 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
44
50
  }
45
51
 
46
52
  const TRANSFORM_POLICIES = Object.freeze({
53
+ bin: Object.freeze({
54
+ ...findTransformTopology("bin"),
55
+ validate: validateBinTransform,
56
+ materializeOp: "materializeBinData"
57
+ }),
47
58
  bin2d: Object.freeze({
48
59
  ...findTransformTopology("bin2d"),
49
60
  validate: validateBin2DTransform,
50
61
  materializeOp: "materializeBin2DData",
51
62
  replayTransform: requestedBin2DTransform
52
63
  }),
64
+ computed: Object.freeze({
65
+ ...findTransformTopology("computed"),
66
+ validate: validateComputedTransform,
67
+ materializeOp: "materializeComputedData"
68
+ }),
53
69
  boxOutlier: Object.freeze({
54
70
  ...findTransformTopology("boxOutlier"),
55
71
  validate: validateBoxTransform,
@@ -66,11 +82,21 @@ const TRANSFORM_POLICIES = Object.freeze({
66
82
  materializeOp: "materializeDensityData",
67
83
  replayTransform: requestedDensityTransform
68
84
  }),
85
+ ecdf: Object.freeze({
86
+ ...findTransformTopology("ecdf"),
87
+ validate: validateECDFTransform,
88
+ materializeOp: "materializeECDFData"
89
+ }),
69
90
  filter: Object.freeze({
70
91
  ...findTransformTopology("filter"),
71
92
  validate: validateFilterTransform,
72
93
  materializeOp: "materializeFilteredData"
73
94
  }),
95
+ fold: Object.freeze({
96
+ ...findTransformTopology("fold"),
97
+ validate: validateFoldTransform,
98
+ materializeOp: "materializeFoldData"
99
+ }),
74
100
  gradientProfile: Object.freeze({
75
101
  ...findTransformTopology("gradientProfile"),
76
102
  validate: validateGradientProfileTransform,
@@ -99,6 +125,16 @@ const TRANSFORM_POLICIES = Object.freeze({
99
125
  validate: validateRegressionTransform,
100
126
  materializeOp: "materializeRegressionData"
101
127
  }),
128
+ summary: Object.freeze({
129
+ ...findTransformTopology("summary"),
130
+ validate: validateSummaryTransform,
131
+ materializeOp: "materializeSummaryData"
132
+ }),
133
+ stack: Object.freeze({
134
+ ...findTransformTopology("stack"),
135
+ validate: validateStackTransform,
136
+ materializeOp: "materializeStackData"
137
+ }),
102
138
  timeUnit: Object.freeze({
103
139
  ...findTransformTopology("timeUnit"),
104
140
  validate: validateTimeUnitTransform,
@@ -0,0 +1,112 @@
1
+ const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
2
+ const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
3
+ const MONTH_NAMES = Object.freeze([
4
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
5
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
6
+ ]);
7
+
8
+ export function isUtcValueFormat(format) {
9
+ if (typeof format !== "string") return false;
10
+ let directives = 0;
11
+ for (let index = 0; index < format.length; index += 1) {
12
+ if (format[index] !== "%") continue;
13
+ const directive = format[index + 1];
14
+ if (!TIME_DIRECTIVES.has(directive)) return false;
15
+ if (directive !== "%") directives += 1;
16
+ index += 1;
17
+ }
18
+ return directives > 0;
19
+ }
20
+
21
+ export function validateValueFormat(format, label = "Label format", {
22
+ allowDecimalsObject = false
23
+ } = {}) {
24
+ if (format === undefined || format === "auto") return format ?? "auto";
25
+ if (
26
+ format !== null &&
27
+ typeof format === "object" &&
28
+ Number.isInteger(format.decimals) &&
29
+ format.decimals >= 0 &&
30
+ format.decimals <= 100 &&
31
+ Object.keys(format).length === 1
32
+ ) {
33
+ if (allowDecimalsObject) return format;
34
+ }
35
+ if (typeof format === "string" && (
36
+ NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
37
+ )) {
38
+ return format;
39
+ }
40
+ const numericCandidate = typeof format === "string"
41
+ ? format.match(/^\.(\d+)(f|%|e)$/)
42
+ : undefined;
43
+ if (numericCandidate && Number(numericCandidate[1]) > 12) {
44
+ throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
45
+ }
46
+ throw new TypeError(
47
+ `${label} must be auto${allowDecimalsObject ? ", { decimals }," : ","} or a supported format string (numeric or UTC).`
48
+ );
49
+ }
50
+
51
+ function pad(value, length = 2) {
52
+ return String(value).padStart(length, "0");
53
+ }
54
+
55
+ function formatUtcValue(value, format, label) {
56
+ const date = new Date(value);
57
+ if (!Number.isFinite(date.getTime())) {
58
+ throw new TypeError(`${label} requires a valid date or timestamp.`);
59
+ }
60
+ const values = {
61
+ Y: pad(date.getUTCFullYear(), 4),
62
+ m: pad(date.getUTCMonth() + 1),
63
+ d: pad(date.getUTCDate()),
64
+ b: MONTH_NAMES[date.getUTCMonth()]
65
+ };
66
+ return format.replace(/%([Ymdb%])/g, (_, directive) =>
67
+ directive === "%" ? "%" : values[directive]
68
+ );
69
+ }
70
+
71
+ export function formatValue(value, {
72
+ format = "auto",
73
+ valueType,
74
+ autoFormatter = current => String(current),
75
+ label = "Label format",
76
+ allowDecimalsObject = false
77
+ } = {}) {
78
+ const resolved = validateValueFormat(format, label, {
79
+ allowDecimalsObject
80
+ });
81
+ if (value === undefined || value === null) return undefined;
82
+ if (resolved === "auto") return autoFormatter(value);
83
+ if (["nominal", "ordinal", "discrete"].includes(valueType)) {
84
+ throw new Error('Discrete labels require format "auto".');
85
+ }
86
+ const utc = isUtcValueFormat(resolved);
87
+ if (valueType === "temporal") {
88
+ if (!utc) throw new Error("Temporal labels require a supported UTC format string.");
89
+ return formatUtcValue(value, resolved, label);
90
+ }
91
+ if (utc) {
92
+ if (valueType === "quantitative") {
93
+ throw new Error("Quantitative labels cannot use a UTC format string.");
94
+ }
95
+ return formatUtcValue(value, resolved, label);
96
+ }
97
+ if (!Number.isFinite(value)) {
98
+ throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
99
+ }
100
+ if (typeof resolved === "object") return value.toFixed(resolved.decimals);
101
+ const match = resolved.match(NUMERIC_FORMAT);
102
+ const precision = Number(match[1]);
103
+ if (match[2] === "f") return value.toFixed(precision);
104
+ if (match[2] === "%") {
105
+ const percentage = value * 100;
106
+ if (!Number.isFinite(percentage)) {
107
+ throw new RangeError(`${label} percent format overflow.`);
108
+ }
109
+ return `${percentage.toFixed(precision)}%`;
110
+ }
111
+ return value.toExponential(precision);
112
+ }
@@ -336,10 +336,16 @@ function stableWindowOutput(operation, calculate) {
336
336
  }
337
337
  }
338
338
 
339
+ function setWindowOutput(entry, field, value) {
340
+ Object.defineProperty(entry.row, field, {
341
+ value, enumerable: true, writable: true, configurable: true
342
+ });
343
+ }
344
+
339
345
  function applyOperation(partition, operation, sortBy) {
340
346
  if (operation.op === "rowNumber") {
341
347
  partition.forEach((entry, index) => {
342
- entry.row[operation.as] = index + 1;
348
+ setWindowOutput(entry, operation.as, index + 1);
343
349
  });
344
350
  return;
345
351
  }
@@ -354,7 +360,7 @@ function applyOperation(partition, operation, sortBy) {
354
360
  rank = index + 1;
355
361
  denseRank += 1;
356
362
  }
357
- entry.row[operation.as] = operation.op === "rank" ? rank : denseRank;
363
+ setWindowOutput(entry, operation.as, operation.op === "rank" ? rank : denseRank);
358
364
  });
359
365
  return;
360
366
  }
@@ -364,7 +370,7 @@ function applyOperation(partition, operation, sortBy) {
364
370
  stableFinitePrefixSums(values, label)
365
371
  );
366
372
  partition.forEach((entry, index) => {
367
- entry.row[operation.as] = totals[index];
373
+ setWindowOutput(entry, operation.as, totals[index]);
368
374
  });
369
375
  return;
370
376
  }
@@ -379,18 +385,18 @@ function applyOperation(partition, operation, sortBy) {
379
385
  index + operation.frame.following
380
386
  );
381
387
  const values = operationValues(partition, operation, first, last);
382
- entry.row[operation.as] = stableWindowOutput(operation, label =>
388
+ setWindowOutput(entry, operation.as, stableWindowOutput(operation, label =>
383
389
  calculate(values, label)
384
- );
390
+ ));
385
391
  });
386
392
  return;
387
393
  }
388
394
  const direction = operation.op === "lag" ? -1 : 1;
389
395
  partition.forEach((entry, index) => {
390
396
  const peer = partition[index + direction * operation.offset];
391
- entry.row[operation.as] = peer === undefined
397
+ setWindowOutput(entry, operation.as, peer === undefined
392
398
  ? operation.default
393
- : peer.row[operation.field];
399
+ : peer.row[operation.field]);
394
400
  });
395
401
  }
396
402
 
@@ -23,6 +23,29 @@ function resolveColumns(columns, count) {
23
23
  return Math.min(value, count);
24
24
  }
25
25
 
26
+ function resolveGridCoordinates(children, columns) {
27
+ const requested = children.some(child => child.row !== undefined || child.column !== undefined);
28
+ if (!requested) return undefined;
29
+ if (children.some(child => !(
30
+ Number.isInteger(child.row) && child.row >= 0 &&
31
+ Number.isInteger(child.column) && child.column >= 0
32
+ ))) {
33
+ throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");
34
+ }
35
+ const keys = children.map(child => `${child.row}:${child.column}`);
36
+ if (new Set(keys).size !== keys.length) {
37
+ throw new Error("Facet grid children must occupy unique row and column coordinates.");
38
+ }
39
+ const requiredColumns = Math.max(...children.map(child => child.column)) + 1;
40
+ if (columns !== undefined && columns !== requiredColumns) {
41
+ throw new RangeError("Facet grid columns must match the declared column domain.");
42
+ }
43
+ return {
44
+ columns: requiredColumns,
45
+ rows: Math.max(...children.map(child => child.row)) + 1
46
+ };
47
+ }
48
+
26
49
  function alignedOffset(remaining, align) {
27
50
  if (align === "start") return 0;
28
51
  if (align === "end") return remaining;
@@ -43,10 +66,12 @@ export function resolveFacetLayout({
43
66
  sharedLegendPosition = "right"
44
67
  } = {}) {
45
68
  const values = children?.map(child => child?.value);
69
+ const coordinates = resolveGridCoordinates(children ?? [], columns);
46
70
  const resolvedChildren = normalizeCompositionChildren(children?.map(
47
- ({ value: _value, ...child }) => child
71
+ ({ value: _value, row: _row, column: _column, ...child }) => child
48
72
  ));
49
- const resolvedColumns = resolveColumns(columns, resolvedChildren.length);
73
+ const resolvedColumns = coordinates?.columns ??
74
+ resolveColumns(columns, resolvedChildren.length);
50
75
  const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
51
76
  const resolvedAlign = normalizeCompositionAlign(align);
52
77
  const resolvedPadding = normalizeCompositionPadding(padding);
@@ -79,12 +104,17 @@ export function resolveFacetLayout({
79
104
  const legendLane = sharedLegend
80
105
  ? legendGap + (sideLegend ? legendWidth : legendHeight)
81
106
  : 0;
82
- const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
107
+ const rowCount = coordinates?.rows ??
108
+ Math.ceil(resolvedChildren.length / resolvedColumns);
83
109
  const columnWidths = Array(resolvedColumns).fill(-Infinity);
84
110
  const rowHeights = Array(rowCount).fill(-Infinity);
85
111
  resolvedChildren.forEach((child, index) => {
86
- const column = index % resolvedColumns;
87
- const row = Math.floor(index / resolvedColumns);
112
+ const column = coordinates === undefined
113
+ ? index % resolvedColumns
114
+ : children[index].column;
115
+ const row = coordinates === undefined
116
+ ? Math.floor(index / resolvedColumns)
117
+ : children[index].row;
88
118
  columnWidths[column] = Math.max(columnWidths[column], child.width);
89
119
  rowHeights[row] = Math.max(rowHeights[row], child.height);
90
120
  });
@@ -97,8 +127,12 @@ export function resolveFacetLayout({
97
127
  .reduce((sum, height) => sum + height, 0) + resolvedGap * row
98
128
  );
99
129
  const placements = resolvedChildren.map((child, index) => {
100
- const column = index % resolvedColumns;
101
- const row = Math.floor(index / resolvedColumns);
130
+ const column = coordinates === undefined
131
+ ? index % resolvedColumns
132
+ : children[index].column;
133
+ const row = coordinates === undefined
134
+ ? Math.floor(index / resolvedColumns)
135
+ : children[index].row;
102
136
  return {
103
137
  id: child.id,
104
138
  value: values[index],
@@ -0,0 +1,12 @@
1
+ export function assertGuideCollisionBlocks(blocks) {
2
+ for (let index = 0; index < blocks.length; index += 1) {
3
+ const a = blocks[index];
4
+ for (const b of blocks.slice(index + 1)) {
5
+ if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
6
+ if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
7
+ a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
8
+ throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
9
+ }
10
+ }
11
+ }
12
+ }
@@ -3,19 +3,11 @@ import {
3
3
  measureTextWidth
4
4
  } from "../core/textMetrics.js";
5
5
 
6
- export function measureLegendSymbolHeight(config) {
7
- return Math.max(...config.symbol.layers.map(layer => {
8
- if (layer.type === "swatch") return layer.height;
9
- if (layer.type === "point") return layer.size * 2;
10
- return layer.lineWidth;
11
- }));
12
- }
13
-
14
6
  export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
15
7
  return measureTextWidth(formatVisibleText(value), style);
16
8
  }
17
9
 
18
- export function resolveLegendGrid(config, width, count) {
10
+ export function resolveLegendGrid(config, width, count, symbolHeight) {
19
11
  const labels = config.domain.map(formatVisibleText);
20
12
  const itemWidths = labels.map(
21
13
  label => width + config.labels.offset +
@@ -39,7 +31,7 @@ export function resolveLegendGrid(config, width, count) {
39
31
  config.itemGap * Math.max(0, actualColumns - 1);
40
32
  const rowHeight = Math.max(
41
33
  config.labels.fontSize,
42
- measureLegendSymbolHeight(config)
34
+ symbolHeight
43
35
  );
44
36
  const gridHeight = rowHeight * rowCount +
45
37
  config.itemGap * Math.max(0, rowCount - 1);
@@ -0,0 +1,102 @@
1
+ import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
2
+ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
3
+
4
+ // Measure item content before choosing its edge. Family owners supply the
5
+ // sample dimensions and formatted labels; this module does not inspect scales.
6
+ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
7
+ const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
8
+ ? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
9
+ const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
10
+ left: -strokes[index] / 2, right: symbol.width + strokes[index] / 2,
11
+ top: -symbol.height / 2 - strokes[index] / 2,
12
+ bottom: symbol.height / 2 + strokes[index] / 2
13
+ });
14
+ // Retain a family's minimum slot while reserving its complete painted extent.
15
+ const sampleLeft = Math.min(0, ...sampleBounds.map(bounds => bounds.left));
16
+ const sampleRight = Math.max(symbol.width, ...sampleBounds.map(bounds => bounds.right));
17
+ const sampleWidth = sampleRight - sampleLeft;
18
+ const sampleHeight = Math.max(symbol.height,
19
+ ...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
20
+ const side = ["left", "right"].includes(config.position);
21
+ const titleVisible = config.titleVisible !== false;
22
+ const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
23
+ const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
24
+ const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
25
+ text => measureTextWidth(text, config.labels)
26
+ ));
27
+ let symbolX;
28
+ let labelX;
29
+ let itemY;
30
+ let title;
31
+ if (config.layout === "legacy-bottom") {
32
+ const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
33
+ const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
34
+ const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
35
+ let cursor = x;
36
+ symbolX = widths.map(width => {
37
+ const start = cursor - sampleLeft;
38
+ cursor += width + config.itemGap;
39
+ return start;
40
+ });
41
+ labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
42
+ itemY = labels.map(() => canvas.height - 28);
43
+ title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
44
+ } else if (side) {
45
+ const width = Math.max(itemWidth, titleWidth);
46
+ const x = config.position === "right" ? plot.x + plot.width + config.offset
47
+ : plot.x - config.offset - width;
48
+ const itemHeight = Math.max(sampleHeight, config.labels.fontSize);
49
+ const pitch = Math.max(config.itemGap, itemHeight);
50
+ symbolX = labels.map(() => x - sampleLeft);
51
+ labelX = labels.map(() => x + sampleWidth + config.labels.offset);
52
+ const firstY = Math.max(plot.y + 52, titleVisible
53
+ ? plot.y + 20 + titleHeight / 2 + 12 + itemHeight / 2 : plot.y + 52);
54
+ itemY = labels.map((_, index) => firstY + index * pitch);
55
+ title = { x, y: plot.y + 20, align: "left" };
56
+ } else {
57
+ const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
58
+ const inline = config.titlePosition === "left" && titleVisible;
59
+ const prefix = inline ? titleWidth + 20 : 0;
60
+ const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
61
+ const top = !inline && titleVisible ? titleHeight + 12 : 0;
62
+ const height = inline ? Math.max(titleHeight, grid.gridHeight) : top + grid.gridHeight;
63
+ const x = alignLegendStart(plot, width, config.align);
64
+ const y = config.position === "top" ? plot.y - config.offset - height
65
+ : plot.y + plot.height + config.offset;
66
+ const gridX = inline ? x + prefix : x + (width - grid.gridWidth) / 2;
67
+ const gridY = inline ? y + (height - grid.gridHeight) / 2 : y + top;
68
+ let cursor = 0;
69
+ const columns = grid.columnWidths.map(width => {
70
+ const start = cursor;
71
+ cursor += width + config.itemGap;
72
+ return start;
73
+ });
74
+ symbolX = grid.cells.map(cell => gridX + columns[cell.column] - sampleLeft);
75
+ labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
76
+ itemY = grid.cells.map(cell => gridY + grid.rowHeight / 2 + cell.row * (grid.rowHeight + config.itemGap));
77
+ title = inline ? { x, y: y + height / 2, align: "left" }
78
+ : { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
79
+ }
80
+ const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
81
+ x, y, text, ...style, textAlign: align, textBaseline: "middle"
82
+ });
83
+ const bounds = [
84
+ ...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
85
+ ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
86
+ ...symbolX.map((x, index) => {
87
+ const local = sampleBounds[index];
88
+ return { left: x + local.left, right: x + local.right,
89
+ top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
90
+ })
91
+ ];
92
+ if (config.layout === "legacy-bottom") {
93
+ const itemBounds = titleVisible ? bounds.slice(1) : bounds;
94
+ if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
95
+ throw new Error("Legacy bottom legend requires more space between its fixed title and item rows.");
96
+ }
97
+ if (bounds.some(bound => bound.top <= plot.y + plot.height)) {
98
+ throw new Error("Legacy bottom legend requires more bottom-margin space.");
99
+ }
100
+ }
101
+ return { symbolX, labelX, itemY, title, bounds };
102
+ }
@@ -4,6 +4,28 @@ export const SIDE_LEGEND_SYMBOL_CENTER = 16;
4
4
  export const SIDE_LEGEND_LABEL_START = 44;
5
5
  export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
6
6
 
7
+ export function isHorizontalEdgeLegend(config) {
8
+ return config !== undefined &&
9
+ ["top", "bottom"].includes(config.position) &&
10
+ config.layout !== "legacy-bottom";
11
+ }
12
+
13
+ export function resolveSingleHorizontalLegendPlacement({ plot, canvas, config, bounds }) {
14
+ if (!isHorizontalEdgeLegend(config)) {
15
+ throw new Error("Single legend placement requires a horizontal edge layout.");
16
+ }
17
+ const dx = config.align === "left" ? plot.x - bounds.left
18
+ : config.align === "right" ? plot.x + plot.width - bounds.right
19
+ : plot.x + plot.width / 2 - midpoint(bounds.left, bounds.right);
20
+ const dy = config.position === "top" ? plot.y - config.offset - bounds.bottom
21
+ : plot.y + plot.height + config.offset - bounds.top;
22
+ const occupied = translateBounds(bounds, dx, dy);
23
+ if (occupied.left < 0 || occupied.right > canvas.width || occupied.top < 0 || occupied.bottom > canvas.height) {
24
+ throw new Error(`Legend layout requires more ${config.position}-margin or Canvas space.`);
25
+ }
26
+ return { dx, dy, occupied };
27
+ }
28
+
7
29
  function midpoint(start, end) {
8
30
  const sum = start + end;
9
31
  const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
@@ -25,24 +47,25 @@ function decoration(border) {
25
47
  : border.padding + border.lineWidth / 2;
26
48
  }
27
49
 
28
- function horizontalExtent(blocks) {
50
+ function horizontalExtent(blocks, labelStart) {
29
51
  let left = 0;
30
52
  let right = 0;
31
53
  for (const block of blocks) {
32
- if (block.title !== undefined) right = Math.max(right, block.title.width);
54
+ const inset = block.inset ?? 0;
55
+ if (block.title !== undefined) right = Math.max(right, block.title.width + inset);
33
56
  left = Math.min(left,
34
- SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX
57
+ SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX - inset
35
58
  );
36
59
  right = Math.max(
37
60
  right,
38
- SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX,
39
- SIDE_LEGEND_LABEL_START + block.labels.width
61
+ SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX + inset,
62
+ labelStart + block.labels.width + inset
40
63
  );
41
64
  }
42
65
  return { left, right };
43
66
  }
44
67
 
45
- function placeBlock(block, cursor) {
68
+ function placeBlock(block, cursor, labelStart) {
46
69
  let dy;
47
70
  if (block.title === undefined) {
48
71
  dy = cursor - block.bounds.top;
@@ -65,7 +88,7 @@ function placeBlock(block, cursor) {
65
88
  ? undefined
66
89
  : -block.title.x,
67
90
  symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
68
- labelDx: SIDE_LEGEND_LABEL_START - block.labels.x,
91
+ labelDx: labelStart - block.labels.x,
69
92
  bounds
70
93
  };
71
94
  }
@@ -87,7 +110,10 @@ export function resolveSideLegendLane({
87
110
  }
88
111
  const blocks = groups.flatMap(group => group.blocks);
89
112
  if (blocks.length < 2) return undefined;
90
- const laneExtent = horizontalExtent(blocks);
113
+ const labelStart = Math.max(SIDE_LEGEND_LABEL_START, ...blocks.map(
114
+ block => SIDE_LEGEND_SYMBOL_CENTER + Math.abs(block.labels.x - block.symbol.centerX)
115
+ ));
116
+ const laneExtent = horizontalExtent(blocks, labelStart);
91
117
  const offset = blocks.reduce(
92
118
  (maximum, block) => Math.max(maximum, block.offset),
93
119
  -Infinity
@@ -96,7 +122,7 @@ export function resolveSideLegendLane({
96
122
  ? plot.x + plot.width + offset
97
123
  : plot.x - offset - laneExtent.right;
98
124
  const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
99
- const labelStartX = titleStartX + SIDE_LEGEND_LABEL_START;
125
+ const labelStartX = titleStartX + labelStart;
100
126
  const first = blocks[0];
101
127
  let cursor = first.title === undefined
102
128
  ? plot.y + 12
@@ -110,7 +136,7 @@ export function resolveSideLegendLane({
110
136
  if (occupiedGroups.length > 0) cursor += inset;
111
137
  const groupPlacements = [];
112
138
  for (const block of group.blocks) {
113
- const placement = placeBlock(block, cursor);
139
+ const placement = placeBlock(block, cursor, labelStart);
114
140
  groupPlacements.push(placement);
115
141
  placements.push({
116
142
  ...placement,
@@ -123,7 +149,7 @@ export function resolveSideLegendLane({
123
149
  cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
124
150
  }
125
151
  const vertical = unionBounds(groupPlacements.map(item => item.bounds));
126
- const groupExtent = horizontalExtent(group.blocks);
152
+ const groupExtent = horizontalExtent(group.blocks, labelStart);
127
153
  const foreground = {
128
154
  left: titleStartX + groupExtent.left,
129
155
  right: titleStartX + groupExtent.right,
@@ -232,6 +258,13 @@ function normalizeHorizontalRow(entries) {
232
258
  )
233
259
  : commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
234
260
  return entries.map(({ group, dx }) => {
261
+ if (group.atomic) {
262
+ const contentDy = elementAnchor - group.element.top;
263
+ const foreground = translateBounds(group.content, dx, contentDy);
264
+ return { id: group.id, dx, titleDy: contentDy, contentDy, foreground,
265
+ occupied: expandBounds(foreground, group.inset), padding: group.padding,
266
+ backgroundId: group.backgroundId };
267
+ }
235
268
  const contentDy = commonTitleY === undefined
236
269
  ? elementAnchor - midpoint(
237
270
  group.element.top,
@@ -350,3 +383,59 @@ export function resolveHorizontalLegendLane({
350
383
  rowCount: packed.length
351
384
  };
352
385
  }
386
+
387
+ // Compose independently measured content blocks before the outer edge lane
388
+ // treats the whole group as one indivisible item.
389
+ export function resolveHorizontalLegendGroup({ edge, plot, canvas, groups,
390
+ align, offset, border, backgroundId, collisionBounds = [] }) {
391
+ const inset = decoration(border);
392
+ const innerPlot = { ...plot, x: plot.x + inset, width: plot.width - inset * 2 };
393
+ const packed = packHorizontalRows(groups, innerPlot);
394
+ const rows = packed.map(normalizeHorizontalRow);
395
+ let cursor = 0;
396
+ const placements = [];
397
+ for (const row of rows) {
398
+ const bounds = unionBounds(row.map(item => item.occupied));
399
+ const dy = edge === "top" ? cursor - bounds.bottom : cursor - bounds.top;
400
+ const translated = row.map(item => translateHorizontalPlacement(item, dy));
401
+ placements.push(...translated);
402
+ const occupied = unionBounds(translated.map(item => item.occupied));
403
+ cursor = edge === "top" ? occupied.top - HORIZONTAL_LEGEND_BLOCK_GAP
404
+ : occupied.bottom + HORIZONTAL_LEGEND_BLOCK_GAP;
405
+ }
406
+ const foreground = unionBounds(placements.map(item => item.occupied));
407
+ const occupied = expandBounds(foreground, inset);
408
+ const width = occupied.right - occupied.left;
409
+ const x = align === "left" ? plot.x : align === "right" ? plot.x + plot.width - width
410
+ : plot.x + (plot.width - width) / 2;
411
+ const dx = x - occupied.left;
412
+ const dy = edge === "top" ? plot.y - offset - occupied.bottom
413
+ : plot.y + plot.height + offset - occupied.top;
414
+ const finalBounds = translateBounds(occupied, dx, dy);
415
+ if (finalBounds.left < 0 || finalBounds.right > canvas.width ||
416
+ finalBounds.top < 0 || finalBounds.bottom > canvas.height) {
417
+ throw new Error(`Combined legend requires more ${edge}-margin or Canvas space.`);
418
+ }
419
+ if (collisionBounds.some(bounds => overlap(finalBounds, bounds))) {
420
+ throw new Error(`Combined ${edge} legend and ${edge === "top" ? "chart titles" : "x-axis guides"} require more margin space.`);
421
+ }
422
+ return {
423
+ placements: placements.map(item => ({ ...item, dx: item.dx + dx,
424
+ titleDy: item.titleDy + dy, contentDy: item.contentDy + dy,
425
+ ...(item.backgroundId === undefined ? {} : { background: {
426
+ id: item.backgroundId,
427
+ x: item.foreground.left + dx - item.padding,
428
+ y: item.foreground.top + dy - item.padding,
429
+ width: item.foreground.right - item.foreground.left + item.padding * 2,
430
+ height: item.foreground.bottom - item.foreground.top + item.padding * 2
431
+ } }) })),
432
+ occupied: finalBounds,
433
+ background: backgroundId === undefined ? undefined : {
434
+ id: backgroundId,
435
+ x: foreground.left + dx - border.padding,
436
+ y: foreground.top + dy - border.padding,
437
+ width: foreground.right - foreground.left + border.padding * 2,
438
+ height: foreground.bottom - foreground.top + border.padding * 2
439
+ }
440
+ };
441
+ }