ggaction 0.0.17 → 0.0.18

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 (263) hide show
  1. package/CHANGELOG.md +46 -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/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -1,110 +1 @@
1
- import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import {
3
- validateNonEmptyString,
4
- validateNonNegativeFinite,
5
- validatePositiveFinite
6
- } from "../core/validation.js";
7
-
8
- const MARGIN_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
9
-
10
- export const DEFAULT_MARGIN = cloneAndFreeze({
11
- top: 30,
12
- right: 30,
13
- bottom: 60,
14
- left: 70
15
- });
16
-
17
- export const DEFAULT_CANVAS = cloneAndFreeze({
18
- width: 640,
19
- height: 400,
20
- background: "white",
21
- margin: DEFAULT_MARGIN
22
- });
23
-
24
- function validateMarginValue(value, key) {
25
- validateNonNegativeFinite(value, `Canvas margin.${key}`);
26
- }
27
-
28
- export function normalizeMargin(margin, base = DEFAULT_MARGIN) {
29
- if (Number.isFinite(margin)) {
30
- if (margin < 0) {
31
- throw new RangeError("Canvas margin must not be negative.");
32
- }
33
-
34
- return cloneAndFreeze({
35
- top: margin,
36
- right: margin,
37
- bottom: margin,
38
- left: margin
39
- });
40
- }
41
-
42
- if (!isPlainObject(margin)) {
43
- throw new TypeError("Canvas margin must be a number or a plain object.");
44
- }
45
-
46
- const keys = Object.keys(margin);
47
-
48
- if (keys.length === 0) {
49
- throw new TypeError("Canvas margin object must contain at least one side.");
50
- }
51
-
52
- for (const key of keys) {
53
- if (!MARGIN_KEYS.includes(key)) {
54
- throw new Error(`Unknown canvas margin option "${key}".`);
55
- }
56
-
57
- validateMarginValue(margin[key], key);
58
- }
59
-
60
- return cloneAndFreeze({
61
- ...base,
62
- ...margin
63
- });
64
- }
65
-
66
- export function validateCanvasState({ width, height, background, margin }) {
67
- validatePositiveFinite(width, "Canvas width");
68
- validatePositiveFinite(height, "Canvas height");
69
-
70
- validateNonEmptyString(background, "Canvas background");
71
-
72
- for (const key of MARGIN_KEYS) {
73
- validateMarginValue(margin[key], key);
74
- }
75
-
76
- if (margin.left + margin.right >= width) {
77
- throw new RangeError("Canvas horizontal margins must be smaller than width.");
78
- }
79
-
80
- if (margin.top + margin.bottom >= height) {
81
- throw new RangeError("Canvas vertical margins must be smaller than height.");
82
- }
83
- }
84
-
85
- export function createGraphicBounds({ width, height, margin }) {
86
- return cloneAndFreeze({
87
- x: margin.left,
88
- y: margin.top,
89
- width: width - margin.left - margin.right,
90
- height: height - margin.top - margin.bottom
91
- });
92
- }
93
-
94
- export function resolveGraphicBounds(program) {
95
- const canvas = program.graphicSpec.objects.canvas;
96
- const margin = program.materializationConfigs.canvas?.margin;
97
- if (
98
- canvas?.type !== "canvas" ||
99
- !Number.isFinite(canvas.properties.width) ||
100
- !Number.isFinite(canvas.properties.height) ||
101
- margin === undefined
102
- ) {
103
- throw new Error("Graphical layout requires Canvas dimensions and margin.");
104
- }
105
- return createGraphicBounds({
106
- width: canvas.properties.width,
107
- height: canvas.properties.height,
108
- margin
109
- });
110
- }
1
+ import{annotateError as d}from"../core/diagnostics.js";import{cloneAndFreeze as n,isPlainObject as v}from"../core/immutable.js";import{validateNonEmptyString as h,validateNonNegativeFinite as u,validatePositiveFinite as a,validateOptionObject as l}from"../core/validation.js";const f=Object.freeze(["top","right","bottom","left"]),c=n({top:30,right:30,bottom:60,left:70}),x=n({width:640,height:400,background:"white",margin:c});function p(e,r){u(e,`Canvas margin.${r}`)}function E(e,r=c){if(Number.isFinite(e)){if(e<0)throw new RangeError("Canvas margin must not be negative.");return n({top:e,right:e,bottom:e,left:e})}if(!v(e))throw new TypeError("Canvas margin must be a number or a plain object.");const o=Object.keys(e);if(o.length===0)throw new TypeError("Canvas margin object must contain at least one side.");for(const t of o){if(!f.includes(t))throw new Error(`Unknown canvas margin option "${t}".`);p(e[t],t)}return n({...r,...e})}function O(e){if(e===!1)return!1;l(e,["color","lineWidth"],"Canvas plotBorder");const r={color:"#ddd",lineWidth:1,...e};return h(r.color,"Canvas plotBorder color"),a(r.lineWidth,"Canvas plotBorder lineWidth"),n(r)}function y({width:e,height:r,background:o,margin:t,plotBorder:i=!1}){a(e,"Canvas width"),a(r,"Canvas height"),h(o,"Canvas background");for(const s of f)p(t[s],s);if(t.left+t.right>=e)throw new RangeError("Canvas horizontal margins must be smaller than width.");if(t.top+t.bottom>=r)throw new RangeError("Canvas vertical margins must be smaller than height.");if(i!==!1&&i.lineWidth>=Math.min(e-t.left-t.right,r-t.top-t.bottom))throw new RangeError("Canvas plotBorder lineWidth must be smaller than both plot dimensions.")}function w({width:e,height:r,margin:o}){return n({x:o.left,y:o.top,width:e-o.left-o.right,height:r-o.top-o.bottom})}function j(e){const r=e.graphicSpec.objects.canvas,o=e.materializationConfigs.canvas?.margin;if(r?.type!=="canvas"||!Number.isFinite(r.properties.width)||!Number.isFinite(r.properties.height)||o===void 0)throw new Error("Graphical layout requires Canvas dimensions and margin.");return w({width:r.properties.width,height:r.properties.height,margin:o})}function b(e,r){if(!r||![r.width,r.height].every(t=>Number.isFinite(t)&&t>0)||!Array.isArray(e)||e.some(t=>!t||![t.left,t.right,t.top,t.bottom].every(Number.isFinite)||t.left>t.right||t.top>t.bottom))return;const o={top:0,right:0,bottom:0,left:0};for(const t of e)o.top=Math.max(o.top,-t.top),o.right=Math.max(o.right,t.right-r.width),o.bottom=Math.max(o.bottom,t.bottom-r.height),o.left=Math.max(o.left,-t.left);return Object.values(o).some(t=>t>1e-9)?n(o):void 0}function N(e,r,o){const t=new Error(e),i=b(r,o);return i===void 0?t:d(t,{reason:"canvas-overflow",canvasOverflow:i})}function F(e,r){const o=Object.hasOwn(e,"plot")?e.plot:r;if(!(o===void 0||o===!1)){if(l(o,["width","height"],"Canvas plot"),a(o.width,"Canvas plot width"),a(o.height,"Canvas plot height"),Object.hasOwn(e,"width")||Object.hasOwn(e,"height"))throw new Error("Canvas plot dimensions cannot be combined with fixed outer dimensions.");return n(o)}}export{x as DEFAULT_CANVAS,c as DEFAULT_MARGIN,N as canvasOverflowError,w as createGraphicBounds,E as normalizeMargin,O as normalizePlotBorder,b as resolveCanvasOverflow,F as resolveCanvasPlot,j as resolveGraphicBounds,y as validateCanvasState};
@@ -1,244 +1 @@
1
- import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import {
3
- DEFAULT_COMPOSITION_LAYOUT,
4
- normalizeCompositionAlign,
5
- normalizeCompositionChildren,
6
- normalizeCompositionPadding,
7
- validateCompositionSpacing
8
- } from "./composition.js";
9
-
10
- export const DEFAULT_FACET_LEGEND_GAP = 18;
11
- export const DEFAULT_FACET_LEGEND_WIDTH = 132;
12
- export const DEFAULT_FACET_LEGEND_HEIGHT = 0;
13
-
14
- const FACET_LEGEND_POSITIONS = Object.freeze([
15
- "right", "left", "top", "bottom"
16
- ]);
17
-
18
- function resolveColumns(columns, count) {
19
- const value = columns ?? count;
20
- if (!Number.isInteger(value) || value <= 0) {
21
- throw new RangeError("Facet columns must be a positive integer.");
22
- }
23
- return Math.min(value, count);
24
- }
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
-
49
- function alignedOffset(remaining, align) {
50
- if (align === "start") return 0;
51
- if (align === "end") return remaining;
52
- return remaining / 2;
53
- }
54
-
55
- const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
56
-
57
- function normalizeHeaderLayout(value, rows) {
58
- if (value === undefined) {
59
- return {
60
- outer: ZERO_SIDES,
61
- cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
62
- };
63
- }
64
- if (!isPlainObject(value) || Object.keys(value).some(
65
- key => !["outer", "cellRows"].includes(key)
66
- )) {
67
- throw new TypeError("Facet headerLayout must contain outer and cellRows.");
68
- }
69
- const outer = { ...ZERO_SIDES, ...(value.outer ?? {}) };
70
- if (!isPlainObject(value.outer ?? {}) || Object.keys(outer).some(
71
- key => !Object.hasOwn(ZERO_SIDES, key)
72
- )) {
73
- throw new TypeError("Facet headerLayout.outer must contain side reservations.");
74
- }
75
- for (const [side, amount] of Object.entries(outer)) {
76
- validateCompositionSpacing(amount, `Facet headerLayout.outer.${side}`);
77
- }
78
- const sourceRows = value.cellRows ?? {};
79
- if (!isPlainObject(sourceRows) || Object.keys(sourceRows).some(
80
- key => !["top", "bottom"].includes(key)
81
- )) {
82
- throw new TypeError("Facet headerLayout.cellRows must contain top and bottom arrays.");
83
- }
84
- const cellRows = Object.fromEntries(["top", "bottom"].map(side => {
85
- const amounts = sourceRows[side] ?? Array(rows).fill(0);
86
- if (!Array.isArray(amounts) || amounts.length !== rows) {
87
- throw new RangeError(`Facet headerLayout.cellRows.${side} must match row count.`);
88
- }
89
- amounts.forEach((amount, index) => validateCompositionSpacing(
90
- amount,
91
- `Facet headerLayout.cellRows.${side}[${index}]`
92
- ));
93
- return [side, [...amounts]];
94
- }));
95
- return { outer, cellRows };
96
- }
97
-
98
- export function resolveFacetLayout({
99
- children,
100
- columns,
101
- gap = DEFAULT_COMPOSITION_LAYOUT.gap,
102
- align = DEFAULT_COMPOSITION_LAYOUT.align,
103
- padding = DEFAULT_COMPOSITION_LAYOUT.padding,
104
- titleHeight = 0,
105
- sharedLegend = false,
106
- sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
107
- sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
108
- sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
109
- sharedLegendPosition = "right",
110
- headerLayout
111
- } = {}) {
112
- const values = children?.map(child => child?.value);
113
- const coordinates = resolveGridCoordinates(children ?? [], columns);
114
- const resolvedChildren = normalizeCompositionChildren(children?.map(
115
- ({ value: _value, row: _row, column: _column, ...child }) => child
116
- ));
117
- const resolvedColumns = coordinates?.columns ??
118
- resolveColumns(columns, resolvedChildren.length);
119
- const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
120
- const resolvedAlign = normalizeCompositionAlign(align);
121
- const resolvedPadding = normalizeCompositionPadding(padding);
122
- const resolvedTitleHeight = validateCompositionSpacing(
123
- titleHeight,
124
- "Facet title height"
125
- );
126
- if (typeof sharedLegend !== "boolean") {
127
- throw new TypeError("Facet sharedLegend must be a boolean.");
128
- }
129
- const legendGap = validateCompositionSpacing(
130
- sharedLegendGap,
131
- "Facet shared legend gap"
132
- );
133
- const legendWidth = validateCompositionSpacing(
134
- sharedLegendWidth,
135
- "Facet shared legend width"
136
- );
137
- const legendHeight = validateCompositionSpacing(
138
- sharedLegendHeight,
139
- "Facet shared legend height"
140
- );
141
- if (!FACET_LEGEND_POSITIONS.includes(sharedLegendPosition)) {
142
- throw new Error(
143
- `Unknown facet shared legend position "${sharedLegendPosition}".`
144
- );
145
- }
146
- const sideLegend = ["right", "left"].includes(sharedLegendPosition);
147
- const beforeGrid = ["left", "top"].includes(sharedLegendPosition);
148
- const legendLane = sharedLegend
149
- ? legendGap + (sideLegend ? legendWidth : legendHeight)
150
- : 0;
151
- const rowCount = coordinates?.rows ??
152
- Math.ceil(resolvedChildren.length / resolvedColumns);
153
- const headers = normalizeHeaderLayout(headerLayout, rowCount);
154
- const columnWidths = Array(resolvedColumns).fill(-Infinity);
155
- const rowHeights = Array(rowCount).fill(-Infinity);
156
- resolvedChildren.forEach((child, index) => {
157
- const column = coordinates === undefined
158
- ? index % resolvedColumns
159
- : children[index].column;
160
- const row = coordinates === undefined
161
- ? Math.floor(index / resolvedColumns)
162
- : children[index].row;
163
- columnWidths[column] = Math.max(columnWidths[column], child.width);
164
- rowHeights[row] = Math.max(rowHeights[row], child.height);
165
- });
166
- const columnStarts = columnWidths.map((_, column) =>
167
- resolvedPadding.left + headers.outer.left + columnWidths.slice(0, column)
168
- .reduce((sum, width) => sum + width, 0) + resolvedGap * column
169
- );
170
- const rowStarts = rowHeights.map((_, row) =>
171
- resolvedTitleHeight + resolvedPadding.top + headers.outer.top +
172
- rowHeights.slice(0, row).reduce((sum, height, index) =>
173
- sum + headers.cellRows.top[index] + height + headers.cellRows.bottom[index], 0) +
174
- resolvedGap * row + headers.cellRows.top[row]
175
- );
176
- const placements = resolvedChildren.map((child, index) => {
177
- const column = coordinates === undefined
178
- ? index % resolvedColumns
179
- : children[index].column;
180
- const row = coordinates === undefined
181
- ? Math.floor(index / resolvedColumns)
182
- : children[index].row;
183
- return {
184
- id: child.id,
185
- value: values[index],
186
- column,
187
- row,
188
- x: columnStarts[column] + (beforeGrid && sideLegend ? legendLane : 0) +
189
- alignedOffset(
190
- columnWidths[column] - child.width,
191
- resolvedAlign
192
- ),
193
- y: rowStarts[row] + (beforeGrid && !sideLegend ? legendLane : 0) +
194
- alignedOffset(
195
- rowHeights[row] - child.height,
196
- resolvedAlign
197
- ),
198
- width: child.width,
199
- height: child.height
200
- };
201
- });
202
- const gridWidth = resolvedPadding.left +
203
- headers.outer.left +
204
- columnWidths.reduce((sum, width) => sum + width, 0) +
205
- resolvedGap * Math.max(0, resolvedColumns - 1) +
206
- headers.outer.right +
207
- resolvedPadding.right;
208
- const gridHeight = resolvedTitleHeight + resolvedPadding.top +
209
- headers.outer.top +
210
- rowHeights.reduce((sum, height) => sum + height, 0) +
211
- headers.cellRows.top.reduce((sum, amount) => sum + amount, 0) +
212
- headers.cellRows.bottom.reduce((sum, amount) => sum + amount, 0) +
213
- resolvedGap * Math.max(0, rowCount - 1) +
214
- headers.outer.bottom +
215
- resolvedPadding.bottom;
216
- return cloneAndFreeze({
217
- columns: resolvedColumns,
218
- rows: rowCount,
219
- gap: resolvedGap,
220
- align: resolvedAlign,
221
- padding: resolvedPadding,
222
- titleHeight: resolvedTitleHeight,
223
- headerLayout: headers,
224
- gridWidth,
225
- width: gridWidth + (sideLegend ? legendLane : 0),
226
- height: gridHeight + (sideLegend ? 0 : legendLane),
227
- children: placements,
228
- ...(sharedLegend ? {
229
- legend: {
230
- position: sharedLegendPosition,
231
- x: sharedLegendPosition === "right"
232
- ? gridWidth + legendGap
233
- : 0,
234
- y: sharedLegendPosition === "bottom"
235
- ? gridHeight + legendGap
236
- : sharedLegendPosition === "top"
237
- ? resolvedTitleHeight
238
- : resolvedTitleHeight + resolvedPadding.top + 30,
239
- width: legendWidth,
240
- height: legendHeight
241
- }
242
- } : {})
243
- });
244
- }
1
+ import{cloneAndFreeze as V,isPlainObject as k}from"../core/immutable.js";import{DEFAULT_COMPOSITION_LAYOUT as q,normalizeCompositionAlign as X,normalizeCompositionChildren as tt,normalizeCompositionPadding as et,validateCompositionSpacing as y}from"./composition.js";const ot=18,rt=132,nt=0,lt=Object.freeze(["right","left","top","bottom"]);function it(r,i){const f=r??i;if(!Number.isInteger(f)||f<=0)throw new RangeError("Facet columns must be a positive integer.");return Math.min(f,i)}function at(r,i){if(!r.some(o=>o.row!==void 0||o.column!==void 0))return;if(r.some(o=>!(Number.isInteger(o.row)&&o.row>=0&&Number.isInteger(o.column)&&o.column>=0)))throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");const d=r.map(o=>`${o.row}:${o.column}`);if(new Set(d).size!==d.length)throw new Error("Facet grid children must occupy unique row and column coordinates.");const g=Math.max(...r.map(o=>o.column))+1;if(i!==void 0&&i!==g)throw new RangeError("Facet grid columns must match the declared column domain.");return{columns:g,rows:Math.max(...r.map(o=>o.row))+1}}function B(r,i){return i==="start"?0:i==="end"?r:r/2}const $=Object.freeze({top:0,right:0,bottom:0,left:0});function st(r,i,f){if(r===void 0)return{outer:$,cellRows:{top:Array(i).fill(0),bottom:Array(i).fill(0)}};if(!k(r)||Object.keys(r).some(o=>!["outer","cellRows","cellColumns"].includes(o)))throw new TypeError("Facet headerLayout must contain outer and cellRows.");const d={...$,...r.outer??{}};if(!k(r.outer??{})||Object.keys(d).some(o=>!Object.hasOwn($,o)))throw new TypeError("Facet headerLayout.outer must contain side reservations.");for(const[o,u]of Object.entries(d))y(u,`Facet headerLayout.outer.${o}`);const g={outer:d};for(const[o,u,I,D]of[["cellRows",["top","bottom"],i,"row"],["cellColumns",["left","right"],f,"column"]]){if(o==="cellColumns"&&r[o]===void 0)continue;const _=o==="cellRows"?r[o]??{}:r[o];if(!k(_)||Object.keys(_).some(a=>!u.includes(a)))throw new TypeError(`Facet headerLayout.${o} must contain ${u[0]} and ${u[1]} arrays.`);g[o]=Object.fromEntries(u.map(a=>{const b=_[a]??Array(I).fill(0);if(!Array.isArray(b)||b.length!==I)throw new RangeError(`Facet headerLayout.${o}.${a} must match ${D} count.`);return b.forEach((m,E)=>y(m,`Facet headerLayout.${o}.${a}[${E}]`)),[a,[...b]]}))}return g}function ht({children:r,columns:i,gap:f=q.gap,align:d=q.align,padding:g=q.padding,titleHeight:o=0,sharedLegend:u=!1,sharedLegendGap:I=ot,sharedLegendWidth:D=rt,sharedLegendHeight:_=nt,sharedLegendPosition:a="right",headerLayout:b,spacing:m="canvas",plots:E}={}){if(!["canvas","plot"].includes(m))throw new Error(`Unknown facet spacing "${m}".`);const G=new Map((E??[]).map(e=>[e.id,e])),U=r;let h=$;if(m==="plot"){if(!Array.isArray(E)||E.length!==r?.length||G.size!==E.length)throw new Error("Plot spacing requires one plot bounds record per child.");h={...$},r=r.map(e=>{const t=G.get(e.id);if(!t||![t.x,t.y,t.width,t.height].every(Number.isFinite)||t.x<0||t.y<0||t.width<=0||t.height<=0||t.x+t.width>e.width+1e-9||t.y+t.height>e.height+1e-9)throw new Error("Plot spacing requires valid contained child plot bounds.");return h.left=Math.max(h.left,t.x),h.top=Math.max(h.top,t.y),h.right=Math.max(h.right,e.width-t.x-t.width),h.bottom=Math.max(h.bottom,e.height-t.y-t.height),{...e,width:t.width,height:t.height}})}const J=r?.map(e=>e?.value),F=at(r??[],i),L=tt(r?.map(({value:e,row:t,column:l,...s})=>s)),c=F?.columns??it(i,L.length),A=y(f,"Facet gap"),H=X(d),N=et(g),w=Object.fromEntries(Object.keys(N).map(e=>[e,N[e]+h[e]])),T=y(o,"Facet title height");if(typeof u!="boolean")throw new TypeError("Facet sharedLegend must be a boolean.");const z=y(I,"Facet shared legend gap"),W=y(D,"Facet shared legend width"),P=y(_,"Facet shared legend height");if(!lt.includes(a))throw new Error(`Unknown facet shared legend position "${a}".`);const M=["right","left"].includes(a),Y=["left","top"].includes(a),j=u?z+(M?W:P):0,C=F?.rows??Math.ceil(L.length/c);let n=st(b,C,c);if(m==="plot"){for(const[e,t]of[[n.cellRows.bottom,n.cellRows.top],[n.cellColumns?.right??[],n.cellColumns?.left??[]]])for(let l=1;l<e.length;l+=1)if(e[l-1]+t[l]>A)throw new Error("Plot spacing gap is too small for the facet headers.");n={outer:{top:n.outer.top+n.cellRows.top[0],bottom:n.outer.bottom+n.cellRows.bottom.at(-1),left:n.outer.left+(n.cellColumns?.left[0]??0),right:n.outer.right+(n.cellColumns?.right.at(-1)??0)},cellRows:{top:Array(C).fill(0),bottom:Array(C).fill(0)}}}const v=n.cellColumns??{left:Array(c).fill(0),right:Array(c).fill(0)},O=Array(c).fill(-1/0),R=Array(C).fill(-1/0);L.forEach((e,t)=>{const l=F===void 0?t%c:r[t].column,s=F===void 0?Math.floor(t/c):r[t].row;O[l]=Math.max(O[l],e.width),R[s]=Math.max(R[s],e.height)});const K=O.map((e,t)=>w.left+n.outer.left+O.slice(0,t).reduce((l,s,x)=>l+v.left[x]+s+v.right[x],0)+A*t+v.left[t]),Q=R.map((e,t)=>T+w.top+n.outer.top+R.slice(0,t).reduce((l,s,x)=>l+n.cellRows.top[x]+s+n.cellRows.bottom[x],0)+A*t+n.cellRows.top[t]),p=L.map((e,t)=>{const l=F===void 0?t%c:r[t].column,s=F===void 0?Math.floor(t/c):r[t].row;return{id:e.id,value:J[t],column:l,row:s,x:K[l]+(Y&&M?j:0)+B(O[l]-e.width,H),y:Q[s]+(Y&&!M?j:0)+B(R[s]-e.height,H),width:e.width,height:e.height}}),S=w.left+n.outer.left+O.reduce((e,t)=>e+t,0)+v.left.reduce((e,t)=>e+t,0)+v.right.reduce((e,t)=>e+t,0)+A*Math.max(0,c-1)+n.outer.right+w.right,Z=T+w.top+n.outer.top+R.reduce((e,t)=>e+t,0)+n.cellRows.top.reduce((e,t)=>e+t,0)+n.cellRows.bottom.reduce((e,t)=>e+t,0)+A*Math.max(0,C-1)+n.outer.bottom+w.bottom;if(m==="plot")for(let e=0;e<p.length;e+=1){const t=G.get(p[e].id);p[e].x-=t.x,p[e].y-=t.y,p[e].width=U[e].width,p[e].height=U[e].height}return V({...m==="plot"?{spacing:m}:{},columns:c,rows:C,gap:A,align:H,padding:N,titleHeight:T,headerLayout:n,gridWidth:S,width:S+(M?j:0),height:Z+(M?0:j),children:p,...u?{legend:{position:a,x:a==="right"?S+z:0,y:a==="bottom"?Z+z:a==="top"?T:T+w.top+30,width:W,height:P}}:{}})}export{ot as DEFAULT_FACET_LEGEND_GAP,nt as DEFAULT_FACET_LEGEND_HEIGHT,rt as DEFAULT_FACET_LEGEND_WIDTH,ht as resolveFacetLayout};
@@ -507,6 +507,7 @@ function textBounds(item, offset = { x: 0, y: 0 }, profile) {
507
507
  x: item.x + offset.x,
508
508
  y: item.y + offset.y,
509
509
  text: item.text,
510
+ lines: item.lines,
510
511
  fontSize: item.fontSize,
511
512
  fontFamily: item.fontFamily,
512
513
  fontWeight: item.fontWeight,
@@ -1,3 +1,4 @@
1
+ import { PLOT_LEGEND_POSITIONS } from "../core/vocabulary.js";
1
2
  import {
2
3
  formatVisibleText,
3
4
  measureTextWidth
@@ -7,6 +8,11 @@ export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile)
7
8
  return measureTextWidth(formatVisibleText(value), style, profile);
8
9
  }
9
10
 
11
+ // A conservative line-height reserve, not a font-specific glyph measurement.
12
+ export function legendLabelLineHeight(fontSize) {
13
+ return fontSize * 1.2;
14
+ }
15
+
10
16
  export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
11
17
  const labels = config.domain.map(formatVisibleText);
12
18
  const itemWidths = labels.map(
@@ -43,3 +49,10 @@ export function alignLegendStart(bounds, width, align) {
43
49
  if (align === "right") return bounds.x + bounds.width - width;
44
50
  return bounds.x + (bounds.width - width) / 2;
45
51
  }
52
+
53
+
54
+ export { PLOT_LEGEND_POSITIONS } from "../core/vocabulary.js";
55
+
56
+ export function isPlotLegend(config) {
57
+ return PLOT_LEGEND_POSITIONS.includes(config.position);
58
+ }
@@ -0,0 +1 @@
1
+ import{PLOT_LEGEND_POSITIONS as I}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as M,HORIZONTAL_LEGEND_BLOCK_GAP as N}from"./legendLane.js";function H({blocks:g,group:O,plot:e,canvas:L,allowOverflow:l=!1}){if(g.length===0)return[];const{placement:t,blockLayout:f}=O,u=I.includes(t.position),c=["left","right"].includes(t.position),x=f==="column"||f==="wrap"&&c,y=u||c||t.at!==void 0?M:N,_=e.width-(u?t.offset*2:0);let h=0,p=0,w=0;const d=g.map(i=>{const o=i.bounds.right-i.bounds.left,E=i.bounds.bottom-i.bounds.top;if(f==="wrap"&&!x&&!u&&o>_&&!l)throw new Error("Horizontal legend block requires more plot width.");!x&&f==="wrap"&&h>0&&h+o>_&&(h=0,p+=w+y,w=0);const G={id:i.id,x:h,y:p,width:o,height:E};return x?p+=E+y:(h+=o+y,w=Math.max(w,E)),G}),r=Math.max(...d.map(i=>i.x+i.width)),a=Math.max(...d.map(i=>i.y+i.height));if(f==="wrap"&&t.position==="top"){const i=new Map;for(const o of d)i.set(o.y,Math.max(i.get(o.y)??0,o.height));for(const o of d)o.y=a-o.y-i.get(o.y)}let n,s;if(t.at!==void 0)n=e.x+t.at.x,s=e.y+t.at.y;else if(u){if(r+2*t.offset>e.width||a+2*t.offset>e.height)throw new Error("In-plot legend group does not fit the plot inset.");n=t.position.endsWith("left")?e.x+t.offset:e.x+e.width-t.offset-r,s=t.position.startsWith("top")?e.y+t.offset:e.y+e.height-t.offset-a}else c?(n=t.position==="right"?e.x+e.width+t.offset:e.x-t.offset-r,s=e.y):(n=t.align==="left"?e.x:t.align==="right"?e.x+e.width-r:e.x+(e.width-r)/2,s=t.position==="top"?e.y-t.offset-a:e.y+e.height+t.offset);const m={left:n,top:s,right:n+r,bottom:s+a};if(m.left<0||m.top<0||m.right>L.width||m.bottom>L.height)throw new Error("Legend group requires more Canvas space.");return d.map((i,o)=>({id:i.id,dx:n+i.x-g[o].bounds.left,dy:s+i.y-g[o].bounds.top}))}export{H as resolveLegendGroupPlacement};
@@ -1,5 +1,6 @@
1
1
  import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
2
- import { alignLegendStart, resolveLegendGrid } from "./legend.js";
2
+ import { alignLegendStart, resolveLegendGrid, isPlotLegend, legendLabelLineHeight } from "./legend.js";
3
+ import { alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
3
4
 
4
5
  // Measure item content before choosing its edge. Family owners supply the
5
6
  // sample dimensions and formatted labels; this module does not inspect scales.
@@ -17,13 +18,13 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
17
18
  const sampleWidth = sampleRight - sampleLeft;
18
19
  const sampleHeight = Math.max(symbol.height,
19
20
  ...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
20
- const side = ["left", "right"].includes(config.position);
21
+ const inside = isPlotLegend(config);
22
+ const side = inside || ["left", "right"].includes(config.position);
21
23
  const titleVisible = config.titleVisible !== false;
22
- const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 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, profile)
26
- ));
24
+ const titleText = titleVisible ? resolveLegendTitleText(config.title, config.titleStyle, profile)
25
+ : { width: 0, height: 0 };
26
+ const titleWidth = titleVisible ? titleText.width : 0;
27
+ const titleHeight = titleVisible ? titleText.height : 0;
27
28
  let symbolX;
28
29
  let labelX;
29
30
  let itemY;
@@ -42,16 +43,36 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
42
43
  itemY = labels.map(() => canvas.height - 28);
43
44
  title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
44
45
  } else if (side) {
45
- const width = Math.max(itemWidth, titleWidth);
46
+ const grid = resolveLegendGrid({
47
+ ...config, columns: config.columns ?? 1, domain: labels
48
+ }, sampleWidth, labels.length, sampleHeight, profile);
49
+ const width = Math.max(grid.gridWidth, titleWidth);
46
50
  const x = config.position === "right" ? plot.x + plot.width + config.offset
47
51
  : plot.x - config.offset - width;
48
- const itemHeight = Math.max(sampleHeight, config.labels.fontSize);
52
+ const itemHeight = Math.max(sampleHeight, legendLabelLineHeight(config.labels.fontSize));
49
53
  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
54
+ let cursor = x;
55
+ const columnStarts = grid.columnWidths.map(columnWidth => {
56
+ const start = cursor;
57
+ cursor += columnWidth + config.itemGap;
58
+ return start;
59
+ });
60
+ symbolX = grid.cells.map(cell => columnStarts[cell.column] - sampleLeft);
61
+ labelX = grid.cells.map(cell => columnStarts[cell.column] + sampleWidth + config.labels.offset);
62
+ let firstY = Math.max(plot.y + 52, titleVisible
53
63
  ? plot.y + 20 + titleHeight / 2 + 12 + itemHeight / 2 : plot.y + 52);
54
- itemY = labels.map((_, index) => firstY + index * pitch);
64
+ if ((inside || config.groupCorner === true) && titleVisible) {
65
+ const titleBounds = resolveTextBounds({ x: x, y: plot.y + 20,
66
+ text: config.title, ...config.titleStyle, lines: titleText.lines,
67
+ textAlign: "left", textBaseline: "middle" }, profile);
68
+ const labelTops = labels.map(text => resolveTextBounds({
69
+ x: 0, y: 0, text, ...config.labels,
70
+ textAlign: "left", textBaseline: "middle"
71
+ }, profile).top);
72
+ const itemTop = Math.min(...sampleBounds.map(bounds => bounds.top), ...labelTops);
73
+ firstY = titleBounds.bottom + 12 - itemTop;
74
+ }
75
+ itemY = grid.cells.map(cell => firstY + cell.row * pitch);
55
76
  title = { x, y: plot.y + 20, align: "left" };
56
77
  } else {
57
78
  const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
@@ -80,8 +101,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
80
101
  const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
81
102
  x, y, text, ...style, textAlign: align, textBaseline: "middle"
82
103
  }, profile);
83
- const bounds = [
84
- ...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
104
+ const bodyBounds = [
85
105
  ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
86
106
  ...symbolX.map((x, index) => {
87
107
  const local = sampleBounds[index];
@@ -89,6 +109,13 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
89
109
  top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
90
110
  })
91
111
  ];
112
+ if (titleVisible) title = alignLegendTitle(title, config.title, config.titleStyle, bodyBounds, profile);
113
+ const bounds = [
114
+ ...(titleVisible ? [resolveTextBounds({ x: title.x, y: title.y,
115
+ text: config.title, ...config.titleStyle, lines: titleText.lines,
116
+ textAlign: title.align, textBaseline: "middle" }, profile)] : []),
117
+ ...bodyBounds
118
+ ];
92
119
  if (config.layout === "legacy-bottom") {
93
120
  const itemBounds = titleVisible ? bounds.slice(1) : bounds;
94
121
  if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
@@ -98,5 +125,22 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
98
125
  throw new Error("Legacy bottom legend requires more bottom-margin space.");
99
126
  }
100
127
  }
128
+ if (inside) {
129
+ const left = Math.min(...bounds.map(b => b.left));
130
+ const right = Math.max(...bounds.map(b => b.right));
131
+ const top = Math.min(...bounds.map(b => b.top));
132
+ const bottom = Math.max(...bounds.map(b => b.bottom));
133
+ const inset = config.offset + (config.border === false ? 0 : config.border.padding + config.border.lineWidth / 2);
134
+ if (right - left + 2 * inset > plot.width || bottom - top + 2 * inset > plot.height) {
135
+ throw new Error("In-plot legend does not fit the plot; reduce its content or dimensions.");
136
+ }
137
+ const dx = config.position.endsWith("left") ? plot.x + inset - left
138
+ : plot.x + plot.width - inset - right;
139
+ const dy = config.position.startsWith("top") ? plot.y + inset - top
140
+ : plot.y + plot.height - inset - bottom;
141
+ return { symbolX: symbolX.map(x => x + dx), labelX: labelX.map(x => x + dx),
142
+ itemY: itemY.map(y => y + dy), title: { ...title, x: title.x + dx, y: title.y + dy },
143
+ bounds: bounds.map(b => ({ left: b.left + dx, right: b.right + dx, top: b.top + dy, bottom: b.bottom + dy })) };
144
+ }
101
145
  return { symbolX, labelX, itemY, title, bounds };
102
146
  }