ggaction 0.0.24 → 0.0.26

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 (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -82,7 +82,7 @@ export function normalizeCompositionChildren(children) {
82
82
  throw new TypeError(`Composition child ${index} must be a plain object.`);
83
83
  }
84
84
  const unknown = Object.keys(child).find(
85
- key => !["id", "width", "height", "widthMode", "heightMode"].includes(key)
85
+ key => !["id", "width", "height", "widthMode", "heightMode", "plot"].includes(key)
86
86
  );
87
87
  if (unknown !== undefined) {
88
88
  throw new Error(`Unknown composition child property "${unknown}".`);
@@ -112,7 +112,8 @@ export function normalizeCompositionChildren(children) {
112
112
  width: child.width,
113
113
  height: child.height,
114
114
  widthMode: child.widthMode ?? "explicit",
115
- heightMode: child.heightMode ?? "explicit"
115
+ heightMode: child.heightMode ?? "explicit",
116
+ ...(child.plot === undefined ? {} : { plot: validatePlacedBounds(child.plot, `Composition plot ${child.id}`) })
116
117
  });
117
118
  });
118
119
  }
@@ -146,10 +147,11 @@ export function resolveCompositionLayout({
146
147
  const resolvedDirection = normalizeDirection(direction);
147
148
  const resolvedChildren = normalizeCompositionChildren(children);
148
149
  const resolvedGap = validateCompositionSpacing(gap, "Composition gap");
149
- const resolvedAlign = normalizeCompositionAlign(align);
150
+ const resolvedAlign = align === "plot" ? "plot" : normalizeCompositionAlign(align);
150
151
  const resolvedPadding = normalizeCompositionPadding(padding);
151
152
  const horizontal = resolvedDirection === "horizontal";
152
- const sizedChildren = normalizeAutoCrossSize(resolvedChildren, horizontal);
153
+ const sizedChildren = resolvedAlign === "plot" ? resolvedChildren : normalizeAutoCrossSize(resolvedChildren, horizontal);
154
+ if (resolvedAlign === "plot") return resolvePlotAlignedComposition({ direction: resolvedDirection, children: sizedChildren, gap: resolvedGap, padding: resolvedPadding });
153
155
  const contentWidth = horizontal
154
156
  ? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
155
157
  resolvedGap * (sizedChildren.length - 1)
@@ -303,3 +305,27 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
303
305
  height: bottom - top
304
306
  });
305
307
  }
308
+
309
+
310
+ function resolvePlotAlignedComposition({ direction, children, gap, padding }) {
311
+ const horizontal = direction === "horizontal";
312
+ const axis = horizontal ? "y" : "x", dimension = horizontal ? "height" : "width";
313
+ if (children.some(child => !child.plot)) throw new Error("Plot alignment requires a plot in every child.");
314
+ const crossSize = children[0].plot[dimension];
315
+ if (children.some(child => Math.abs(child.plot[dimension] - crossSize) > 1e-6)) {
316
+ throw new Error("Plot alignment requires equal cross-axis plot sizes after normalization.");
317
+ }
318
+ const leading = Math.max(...children.map(child => child.plot[axis]));
319
+ const trailing = Math.max(...children.map(child => child[dimension] - child.plot[axis] - crossSize));
320
+ let cursor = horizontal ? padding.left : padding.top;
321
+ const placements = children.map(child => {
322
+ const result = { id: child.id, width: child.width, height: child.height,
323
+ x: horizontal ? cursor : padding.left + leading - child.plot.x,
324
+ y: horizontal ? padding.top + leading - child.plot.y : cursor };
325
+ cursor += (horizontal ? child.width : child.height) + gap;
326
+ return result;
327
+ });
328
+ return cloneAndFreeze({ direction, gap, align: "plot", padding, children: placements,
329
+ width: horizontal ? cursor - gap + padding.right : padding.left + leading + crossSize + trailing + padding.right,
330
+ height: horizontal ? padding.top + leading + crossSize + trailing + padding.bottom : cursor - gap + padding.bottom });
331
+ }
@@ -0,0 +1,91 @@
1
+ import { textBoundsIntersect } from "../core/textMetrics.js";
2
+ import { SIDE_LEGEND_BLOCK_GAP, HORIZONTAL_LEGEND_BLOCK_GAP } from "./legendLane.js";
3
+ import { resolveLegendGroupPlacement } from "./legendGroup.js";
4
+
5
+ export function compositionLegendEdgeClearance(plot, retained, position, occupied) {
6
+ const side = position === 'left' || position === 'right';
7
+ const relevant = occupied ? retained.filter(value => side
8
+ ? value.top < occupied.bottom && value.bottom > occupied.top
9
+ : value.left < occupied.right && value.right > occupied.left) : retained;
10
+ return Math.max(0, ...relevant.map(value => position === 'right' ? value.right - plot.x - plot.width
11
+ : position === 'left' ? plot.x - value.left : position === 'top' ? plot.y - value.top
12
+ : value.bottom - plot.y - plot.height));
13
+ }
14
+
15
+ // Collected legends retain the native plot-relative anchor. Reuse available
16
+ // child margins; only occupied graphics can require an additional guide lane.
17
+ export function layoutCompositionLegend(base, plot, legend, title = {}, retained = []) {
18
+ if (!legend) return { layout: base };
19
+ const { bounds, placement } = legend;
20
+ const anchorBounds = legend.anchorBounds ?? bounds;
21
+ const width = bounds.right - bounds.left, height = bounds.bottom - bounds.top;
22
+ const { position = 'right', align = 'center', offset = 8, alongOffset = 0, at } = placement;
23
+ const inferred = placement.inferredOffset ?? legend.config.inferredOffset;
24
+ const outside = !at && ['left', 'right', 'top', 'bottom'].includes(position);
25
+ const group = { blockLayout: 'column', inferredOffset: inferred,
26
+ placement: at ? { at } : { position, align, offset, alongOffset } };
27
+ const place = (group, measured = bounds) => {
28
+ const [value] = resolveLegendGroupPlacement({ blocks: [{ id: 'shared', bounds: anchorBounds }],
29
+ group, plot, canvas: base, allowOverflow: true, deferCanvasFit: true });
30
+ return { left: measured.left + value.dx, right: measured.right + value.dx,
31
+ top: measured.top + value.dy, bottom: measured.bottom + value.dy };
32
+ };
33
+ let occupied = place(group);
34
+ if (outside && inferred) {
35
+ const clearance = compositionLegendEdgeClearance(plot, retained, position, place(group, anchorBounds));
36
+ occupied = place({ ...group, placement: { ...group.placement, offset: offset + clearance } });
37
+ } else if (outside && retained.some(value => textBoundsIntersect(occupied, value))) {
38
+ throw new Error('Shared composition legend overlaps retained chart content. Increase its offset.');
39
+ }
40
+ const titleSpace = side => title.config?.position === side ? title.reservation + 8 : 0;
41
+ const dx = Math.max(0, 8 + titleSpace('left') - occupied.left);
42
+ const dy = Math.max(0, 8 + titleSpace('top') - occupied.top);
43
+ const x = occupied.left + dx, y = occupied.top + dy;
44
+ const layout = { ...base,
45
+ width: Math.max(base.width + dx, x + width + 8 + titleSpace('right')),
46
+ height: Math.max(base.height + dy, y + height + 8 + titleSpace('bottom')),
47
+ children: base.children.map(child => ({ ...child, x: child.x + dx, y: child.y + dy })) };
48
+ return { layout, frame: { x, y, width, height } };
49
+ }
50
+
51
+ // Every displayed block uses the same parent frame. Compatible shared color
52
+ // explanations are deduplicated upstream; independent blocks remain distinct.
53
+ // Pack overlapping outside blocks along their edge, preserving authored gaps.
54
+ export function layoutCompositionLegends(base, plot, legends, title = {}, retained = []) {
55
+ let layout = base, currentPlot = plot, occupiedContent = retained;
56
+ const frames = new Map(), placed = [];
57
+ for (const legend of legends) {
58
+ let candidate = legend;
59
+ let result = layoutCompositionLegend(layout, currentPlot, candidate, title, occupiedContent);
60
+ const position = legend.placement.position ?? 'right';
61
+ const outside = !legend.placement.at && ['left', 'right', 'top', 'bottom'].includes(position);
62
+ if (outside) {
63
+ const side = ['left', 'right'].includes(position);
64
+ for (let attempt = 0; attempt < placed.length; attempt++) {
65
+ const dx = result.layout.children[0].x - layout.children[0].x;
66
+ const dy = result.layout.children[0].y - layout.children[0].y;
67
+ const f = result.frame;
68
+ const overlaps = placed.filter(previous => previous.position === position).map(previous => frames.get(previous.id))
69
+ .filter(other => textBoundsIntersect({ left: f.x, right: f.x + f.width, top: f.y, bottom: f.y + f.height },
70
+ { left: other.x + dx, right: other.x + dx + other.width, top: other.y + dy, bottom: other.y + dy + other.height }));
71
+ if (!overlaps.length) break;
72
+ const shift = side ? Math.max(...overlaps.map(other => other.y + dy + other.height)) + SIDE_LEGEND_BLOCK_GAP - f.y
73
+ : Math.max(...overlaps.map(other => other.x + dx + other.width)) + HORIZONTAL_LEGEND_BLOCK_GAP - f.x;
74
+ candidate = { ...candidate, placement: { ...candidate.placement,
75
+ alongOffset: (candidate.placement.alongOffset ?? 0) + shift } };
76
+ result = layoutCompositionLegend(layout, currentPlot, candidate, title, occupiedContent);
77
+ }
78
+ }
79
+ const dx = result.layout.children[0].x - layout.children[0].x;
80
+ const dy = result.layout.children[0].y - layout.children[0].y;
81
+ if (dx || dy) {
82
+ for (const [id, frame] of frames) frames.set(id, { ...frame, x: frame.x + dx, y: frame.y + dy });
83
+ occupiedContent = occupiedContent.map(b => ({ left: b.left + dx, right: b.right + dx, top: b.top + dy, bottom: b.bottom + dy }));
84
+ currentPlot = { ...currentPlot, x: currentPlot.x + dx, y: currentPlot.y + dy };
85
+ }
86
+ frames.set(legend.id, result.frame);
87
+ placed.push({ id: legend.id, position });
88
+ layout = result.layout;
89
+ }
90
+ return { layout, frames };
91
+ }
@@ -1 +1 @@
1
- import{resolveTextBounds as m}from"../core/textMetrics.js";const p=Object.freeze(["fontSize","fontFamily","fontWeight","color"]),b=Object.freeze({left:-Math.PI/2,right:Math.PI/2,top:0,bottom:0});function u(r,o,n,l){const i=m({...o,text:r,x:0,y:0,textAlign:"center",textBaseline:"middle"},l);return["left","right"].includes(n)?i.right-i.left:i.bottom-i.top}function M(r,o){const n=["left","right"].includes(o)?r.cellColumns?.[o]:r.cellRows[o];return n===void 0?0:["left","top"].includes(o)?n[0]:n.at(-1)}function T({roles:r,descriptors:o,space:n,headerLayout:l,profile:i,ids:g}){const a=[];for(const[x,f]of r){if(f.title===void 0)continue;const{side:t}=f,c=f.title,s={...Object.fromEntries(p.map(e=>[e,f[e]])),...c,side:t,align:c.align??"center",rotation:c.rotation??b[t]},d=Math.max(0,...o.filter(e=>e.config.side===t&&e.text!=="").map(e=>u(e.text,e.config,t,i)+e.config.offset+(e.clearance?.[t]??0)));s.offset=c.offset??Math.max(n[t],d)+8;const h=s.offset+u(c.text,s,t,i);l.outer[t]=Math.max(l.outer[t],h-n[t]-M(l,t),0),a.push({topology:"role-title",role:x,ids:g,text:c.text,config:s})}return a}export{T as prepareFacetHeaderTitles};
1
+ import{resolveTextBounds as m,resolveTextLines as b}from"../core/textMetrics.js";const p=Object.freeze(["fontSize","fontFamily","fontWeight","color"]),S=Object.freeze({left:-Math.PI/2,right:Math.PI/2,top:0,bottom:0});function d(i,n,o,c){const s=b(i,{fontSize:n.fontSize,align:"center",baseline:"middle",blockAlign:"middle"}),l=m({...n,text:i,x:0,y:0,...s.length>1?{lines:s}:{},textAlign:"center",textBaseline:"middle"},c);return["left","right"].includes(o)?l.right-l.left:l.bottom-l.top}function T(i,n){const o=["left","right"].includes(n)?i.cellColumns?.[n]:i.cellRows[n];return o===void 0?0:["left","top"].includes(n)?o[0]:o.at(-1)}function M({roles:i,descriptors:n,space:o,headerLayout:c,profile:s,ids:l}){const u=[];for(const[g,f]of i){if(f.title===void 0)continue;const{side:t}=f,r=f.title,a={...Object.fromEntries(p.map(e=>[e,f[e]])),...r,side:t,align:r.align??"center",rotation:r.rotation??S[t]},x=Math.max(0,...n.filter(e=>e.config.side===t&&e.text!=="").map(e=>d(e.text,e.config,t,s)+e.config.offset+(e.clearance?.[t]??0)));a.offset=r.offset??Math.max(o[t],x)+8;const h=a.offset+d(r.text,a,t,s);c.outer[t]=Math.max(c.outer[t],h-o[t]-T(c,t),0),u.push({topology:"role-title",role:g,ids:l,text:r.text,config:a})}return u}export{M as prepareFacetHeaderTitles};
@@ -517,11 +517,45 @@ function textBounds(item, offset = { x: 0, y: 0 }, profile) {
517
517
  }, profile);
518
518
  }
519
519
 
520
- function candidateScore(bounds, boundary, placed, candidate, order) {
520
+ // Query only nearby rectangles. Oversized bounds use the ordered fallback so
521
+ // extreme dimensions cannot allocate an unbounded number of grid cells.
522
+ function labelBoundsIndex() {
523
+ const cells = new Map(), rectangles = [], wide = [];
524
+ const keys = bounds => {
525
+ const x0 = Math.floor(bounds.left / 32), x1 = Math.floor(bounds.right / 32);
526
+ const y0 = Math.floor(bounds.top / 32), y1 = Math.floor(bounds.bottom / 32);
527
+ if (![x0, x1, y0, y1].every(Number.isSafeInteger) || (x1 - x0 + 1) * (y1 - y0 + 1) > 256) return undefined;
528
+ const result = [];
529
+ for (let x = x0; x <= x1; x++) for (let y = y0; y <= y1; y++) result.push(`${x},${y}`);
530
+ return result;
531
+ };
532
+ return {
533
+ add(bounds) {
534
+ const id = rectangles.push(bounds) - 1, slots = keys(bounds);
535
+ if (!slots) { wide.push(id); return; }
536
+ for (const key of slots) {
537
+ if (!cells.has(key)) cells.set(key, []);
538
+ cells.get(key).push(id);
539
+ }
540
+ },
541
+ near(bounds) {
542
+ const slots = keys(bounds);
543
+ if (!slots) return rectangles.map((bounds, id) => ({ bounds, id }));
544
+ const ids = new Set(wide);
545
+ for (const key of slots) for (const id of cells.get(key) ?? []) ids.add(id);
546
+ // Preserve stable summation and tie breaking from the ordered scan.
547
+ return [...ids].sort((a, b) => a - b).map(id => ({ bounds: rectangles[id], id }));
548
+ }
549
+ };
550
+ }
551
+
552
+ function candidateScore(bounds, boundary, placed, candidate, order, account) {
553
+ const nearby = placed.near(bounds);
554
+ account(nearby.length + 1);
521
555
  return {
522
556
  outside: overflow(bounds, boundary),
523
- overlap: placed.reduce(
524
- (area, prior) => area + intersectionArea(bounds, prior),
557
+ overlap: nearby.reduce(
558
+ (area, prior) => area + intersectionArea(bounds, prior.bounds),
525
559
  0
526
560
  ),
527
561
  distanceSquared: candidate.distanceSquared,
@@ -537,21 +571,15 @@ function compareScore(first, second) {
537
571
  first.order - second.order;
538
572
  }
539
573
 
540
- function overlapPairs(items) {
541
- const pairs = [];
542
- for (let first = 0; first < items.length; first += 1) {
543
- for (let second = first + 1; second < items.length; second += 1) {
544
- if (
545
- intersectionArea(
546
- items[first].collisionBounds,
547
- items[second].collisionBounds
548
- ) > 0
549
- ) {
550
- pairs.push([items[first].id, items[second].id]);
551
- }
552
- }
553
- }
554
- return pairs;
574
+ function overlapPairs(items, account) {
575
+ const pairs = [], index = labelBoundsIndex();
576
+ items.forEach((item, second) => {
577
+ const nearby = index.near(item.collisionBounds);
578
+ account(nearby.length + 1);
579
+ for (const first of nearby) if (intersectionArea(first.bounds, item.collisionBounds) > 0) pairs.push([first.id, second]);
580
+ index.add(item.collisionBounds);
581
+ });
582
+ return pairs.sort((a, b) => a[0] - b[0] || a[1] - b[1]).map(pair => pair.map(id => items[id].id));
555
583
  }
556
584
 
557
585
  export function resolveLabelLayout({ items, bounds, ...options } = {}, profile) {
@@ -563,15 +591,14 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
563
591
  const ids = new Set();
564
592
  for (const item of items) validateItem(item, ids);
565
593
  const candidates = enumerateLabelOffsets(policy);
566
- validateWorkLimit(
567
- items.length ** 2 * candidates.length,
568
- "Label layout work"
569
- );
594
+ let work = 0;
595
+ const account = amount => validateWorkLimit(work += amount, "Label layout work");
596
+ account(items.length);
570
597
  const base = items.map(item => ({
571
598
  id: item.id,
572
599
  collisionBounds: expanded(textBounds(item, undefined, profile), policy.padding)
573
600
  }));
574
- const placed = [];
601
+ const placed = labelBoundsIndex();
575
602
  const resolved = [];
576
603
  for (const item of items) {
577
604
  let best;
@@ -583,7 +610,8 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
583
610
  bounds,
584
611
  placed,
585
612
  candidate,
586
- order
613
+ order,
614
+ account
587
615
  );
588
616
  if (best === undefined || compareScore(score, best.score) < 0) {
589
617
  best = { candidate, bounds: boundsAtCandidate, collisionBounds, score };
@@ -602,11 +630,11 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
602
630
  bounds: best.bounds,
603
631
  collisionBounds: best.collisionBounds
604
632
  };
605
- placed.push(resolvedItem.collisionBounds);
633
+ placed.add(resolvedItem.collisionBounds);
606
634
  resolved.push(resolvedItem);
607
635
  }
608
- const beforePairs = overlapPairs(base);
609
- const afterPairs = overlapPairs(resolved);
636
+ const beforePairs = overlapPairs(base, account);
637
+ const afterPairs = overlapPairs(resolved, account);
610
638
  const outside = resolved
611
639
  .filter(item => overflow(item.collisionBounds, bounds) > 0)
612
640
  .map(item => item.id);
@@ -1,3 +1,4 @@
1
+ import { resolveLegendTitleText } from "./legendText.js";
1
2
  import { PLOT_LEGEND_POSITIONS } from "../core/vocabulary.js";
2
3
  import {
3
4
  formatVisibleText,
@@ -5,7 +6,7 @@ import {
5
6
  } from "../core/textMetrics.js";
6
7
 
7
8
  export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile) {
8
- return measureTextWidth(formatVisibleText(value), style, profile);
9
+ return resolveLegendTitleText(String(value), style, profile).width;
9
10
  }
10
11
 
11
12
  // A conservative line-height reserve, not a font-specific glyph measurement.
@@ -37,7 +38,7 @@ export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
37
38
  config.itemGap * Math.max(0, actualColumns - 1);
38
39
  const rowHeight = Math.max(
39
40
  config.labels.fontSize,
40
- symbolHeight
41
+ symbolHeight, ...labels.map(label => resolveLegendTitleText(label, config.labels, profile).height)
41
42
  );
42
43
  const gridHeight = rowHeight * rowCount +
43
44
  config.itemGap * Math.max(0, rowCount - 1);
@@ -1 +1 @@
1
- import{canvasOverflowError as M}from"./canvas.js";import{PLOT_LEGEND_POSITIONS as N}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as P,HORIZONTAL_LEGEND_BLOCK_GAP as C}from"./legendLane.js";function v({blocks:u,group:O,plot:e,canvas:c,allowOverflow:_=!1,deferCanvasFit:G=!1}){if(u.length===0)return[];const{placement:t,blockLayout:f}=O,w=N.includes(t.position),p=["left","right"].includes(t.position),x=f==="column"||f==="wrap"&&p,y=w||p||t.at!==void 0?P:C,l=e.width-(w?t.offset*2:0);let h=0,E=0,m=0;const d=u.map(i=>{const o=i.bounds.right-i.bounds.left,L=i.bounds.bottom-i.bounds.top;if(f==="wrap"&&!x&&!w&&o>l&&!_)throw new Error("Horizontal legend block requires more plot width.");!x&&f==="wrap"&&h>0&&h+o>l&&(h=0,E+=m+y,m=0);const I={id:i.id,x:h,y:E,width:o,height:L};return x?E+=L+y:(h+=o+y,m=Math.max(m,L)),I}),s=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,r;if(t.at!==void 0)n=e.x+t.at.x,r=e.y+t.at.y;else if(w){if(s+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-s,r=t.position.startsWith("top")?e.y+t.offset:e.y+e.height-t.offset-a}else p?(n=t.position==="right"?e.x+e.width+t.offset:e.x-t.offset-s,r=e.y):(n=t.align==="left"?e.x:t.align==="right"?e.x+e.width-s:e.x+(e.width-s)/2,r=t.position==="top"?e.y-t.offset-a:e.y+e.height+t.offset);const g={left:n,top:r,right:n+s,bottom:r+a};if(!G&&(g.left<0||g.top<0||g.right>c.width||g.bottom>c.height))throw t.at!==void 0||O.inferredOffset!==!0?new Error("Legend group requires more Canvas space."):M("Legend group requires more Canvas space.",[g],c);return d.map((i,o)=>({id:i.id,dx:n+i.x-u[o].bounds.left,dy:r+i.y-u[o].bounds.top}))}export{v as resolveLegendGroupPlacement};
1
+ import{canvasOverflowError as N}from"./canvas.js";import{annotateError as C}from"../core/diagnostics.js";import{PLOT_LEGEND_POSITIONS as D}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as H,HORIZONTAL_LEGEND_BLOCK_GAP as P}from"./legendLane.js";function S({blocks:m,group:l,plot:t,canvas:x,allowOverflow:G=!1,deferCanvasFit:I=!1,deferPlotFit:L=!1}){if(m.length===0)return[];const{placement:e,blockLayout:h}=l,d=D.includes(e.position),w=["left","right"].includes(e.position),y=h==="column"||h==="wrap"&&w,p=d||w||e.at!==void 0?H:P,_=t.width-(d?e.offset*2:0);let a=0,E=0,c=0;const g=m.map(i=>{const o=i.bounds.right-i.bounds.left,O=i.bounds.bottom-i.bounds.top;if(h==="wrap"&&!y&&!d&&o>_&&!G)throw new Error("Horizontal legend block requires more plot width.");!y&&h==="wrap"&&a>0&&a+o>_&&(a=0,E+=c+p,c=0);const M={id:i.id,x:a,y:E,width:o,height:O};return y?E+=O+p:(a+=o+p,c=Math.max(c,O)),M}),s=Math.max(...g.map(i=>i.x+i.width)),u=Math.max(...g.map(i=>i.y+i.height));if(h==="wrap"&&e.position==="top"){const i=new Map;for(const o of g)i.set(o.y,Math.max(i.get(o.y)??0,o.height));for(const o of g)o.y=u-o.y-i.get(o.y)}let r,f;if(e.at!==void 0)r=t.x+e.at.x,f=t.y+e.at.y;else if(d){if(!L&&(s+2*e.offset>t.width||u+2*e.offset>t.height))throw new Error("In-plot legend group does not fit the plot inset.");r=e.position.endsWith("left")?t.x+e.offset:t.x+t.width-e.offset-s,f=e.position.startsWith("top")?t.y+e.offset:t.y+t.height-e.offset-u}else w?(r=e.position==="right"?t.x+t.width+e.offset:t.x-e.offset-s,f=t.y):(r=e.align==="left"?t.x:e.align==="right"?t.x+t.width-s:t.x+(t.width-s)/2,f=e.position==="top"?t.y-e.offset-u:t.y+t.height+e.offset);e.at===void 0&&(w?f+=e.alongOffset??0:r+=e.alongOffset??0);const n={left:r,top:f,right:r+s,bottom:f+u};if(d&&!L&&(n.left<t.x||n.top<t.y||n.right>t.x+t.width||n.bottom>t.y+t.height))throw new Error("In-plot legend group does not fit the plot inset.");if(!I&&(n.left<0||n.top<0||n.right>x.width||n.bottom>x.height)){const i=N("Legend group requires more Canvas space.",[n],x);throw e.at!==void 0||l.inferredOffset!==!0?C(i,{requiresExplicitCanvas:!0}):i}return g.map((i,o)=>({id:i.id,dx:r+i.x-m[o].bounds.left,dy:f+i.y-m[o].bounds.top}))}export{S as resolveLegendGroupPlacement};
@@ -1,6 +1,6 @@
1
1
  import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
2
2
  import { alignLegendStart, resolveLegendGrid, isPlotLegend, legendLabelLineHeight } from "./legend.js";
3
- import { alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
3
+ import { alignLegendLabels, alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
4
4
  import {resolveLegendContentSpacing} from "./legendSpacing.js";
5
5
 
6
6
  // Measure item content before choosing its edge. Family owners supply the
@@ -32,7 +32,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
32
32
  let itemRows;
33
33
  let title;
34
34
  if (config.layout === "legacy-bottom") {
35
- const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
35
+ const widths = labels.map(text => sampleWidth + config.labels.offset + resolveLegendTitleText(text, config.labels, profile).width);
36
36
  const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
37
37
  const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
38
38
  let cursor = x;
@@ -52,7 +52,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
52
52
  const width = Math.max(grid.gridWidth, titleWidth);
53
53
  const x = config.position === "right" ? plot.x + plot.width + config.offset
54
54
  : plot.x - config.offset - width;
55
- const itemHeight = Math.max(sampleHeight, legendLabelLineHeight(config.labels.fontSize));
55
+ const itemHeight = Math.max(sampleHeight, legendLabelLineHeight(config.labels.fontSize), ...labels.map(text => resolveLegendTitleText(text, config.labels, profile).height));
56
56
  const pitch = Math.max(config.itemGap, itemHeight);
57
57
  let cursor = x;
58
58
  const columnStarts = grid.columnWidths.map(columnWidth => {
@@ -69,7 +69,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
69
69
  text: config.title, ...config.titleStyle, lines: titleText.lines,
70
70
  textAlign: "left", textBaseline: "middle" }, profile);
71
71
  const labelTops = labels.map(text => resolveTextBounds({
72
- x: 0, y: 0, text, ...config.labels,
72
+ x: 0, y: 0, text, ...config.labels, lines: resolveLegendTitleText(text, config.labels, profile).lines,
73
73
  textAlign: "left", textBaseline: "middle"
74
74
  }, profile).top);
75
75
  const itemTop = Math.min(...sampleBounds.map(bounds => bounds.top), ...labelTops);
@@ -104,9 +104,10 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
104
104
  : { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
105
105
  }
106
106
  const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
107
- x, y, text, ...style, textAlign: align, textBaseline: "middle"
107
+ x, y, text, ...style, lines: resolveLegendTitleText(text, style, profile).lines, textAlign: align, textBaseline: "middle"
108
108
  }, profile);
109
- const labelBounds = labels.map((label,index) => textBounds(labelX[index],itemY[index],label,config.labels));
109
+ if (config.labels.align !== undefined) labelX = alignLegendLabels(labelX.map((x, index) => ({ x, y: itemY[index], align: "left" })), labels, config.labels, profile).map(position => position.x);
110
+ const labelBounds = labels.map((label,index) => textBounds(labelX[index],itemY[index],label,config.labels,config.labels.align ?? "left"));
110
111
  const symbolItemBounds = symbolX.map((x, index) => {
111
112
  const local = sampleBounds[index];
112
113
  return { left: x + local.left, right: x + local.right,
@@ -1 +1 @@
1
- import{canvasOverflowError as S}from"./canvas.js";import{unionBounds as _}from"../core/textMetrics.js";const N=24,$=40,v=16,X=44,U=12;function K(t){return t!==void 0&&["top","bottom"].includes(t.position)&&t.layout!=="legacy-bottom"}function W({plot:t,canvas:o,config:n,bounds:r}){if(!K(n))throw new Error("Single legend placement requires a horizontal edge layout.");const i=n.align==="left"?t.x-r.left:n.align==="right"?t.x+t.width-r.right:t.x+t.width/2-B(r.left,r.right),e=n.position==="top"?t.y-n.offset-r.bottom:t.y+t.height+n.offset-r.top,s=w(r,i,e);if(s.left<0||s.right>o.width||s.top<0||s.bottom>o.height)throw S(`Legend layout requires more ${n.position}-margin or Canvas space.`,[s],o);return{dx:i,dy:e,occupied:s}}function B(t,o){const n=t+o,r=Number.isFinite(n)?n/2:t/2+o/2;return r===0?0:r}function T(t){return t===!1?0:t.padding+t.lineWidth/2}function A(t,o){let n=0,r=0;for(const i of t){const e=i.inset??0;if(i.preserveGrid){n=Math.min(n,-e),r=Math.max(r,i.bounds.right-i.bounds.left+e);continue}i.title!==void 0&&(r=Math.max(r,i.title.width+e)),n=Math.min(n,v+i.symbol.left-i.symbol.centerX-e),r=Math.max(r,v+i.symbol.right-i.symbol.centerX+e,o+i.labels.width+e)}return{left:n,right:r}}function Y(t,o,n){const r=o-t.bounds.top,i={left:t.bounds.left,right:t.bounds.right,top:t.bounds.top+r,bottom:t.bounds.bottom+r};return{id:t.id,dy:r,titleDx:t.title===void 0?void 0:t.preserveGrid?-t.bounds.left:-t.title.x,symbolDx:t.preserveGrid?-t.bounds.left:v-t.symbol.centerX,labelDx:t.preserveGrid?-t.bounds.left:n-t.labels.x,bounds:i}}function H(t,o){return t.left<o.right&&t.right>o.left&&t.top<o.bottom&&t.bottom>o.top}function j({side:t,plot:o,canvas:n,groups:r,axisBounds:i}){if(!["right","left"].includes(t))throw new Error(`Unsupported side legend lane "${t}".`);const e=r.flatMap(c=>c.blocks);if(e.length<2)return;const s=Math.max(X,...e.filter(c=>!c.preserveGrid).map(c=>v+Math.abs(c.labels.x-c.symbol.centerX))),f=A(e,s),g=e.reduce((c,x)=>Math.max(c,x.offset),-1/0),l=t==="right"?o.x+o.width+g:o.x-g-f.right,h=l+v,d=l+s;let y=o.y;const z=[],E=[],D=[];for(const c of r){const x=T(c.border);D.length>0&&(y+=x);const G=[];for(const C of c.blocks){const I=Y(C,y,s);G.push(I),z.push({...I,titleDx:I.titleDx===void 0?void 0:I.titleDx+l,symbolDx:I.symbolDx+l,labelDx:I.labelDx+l}),y=I.bounds.bottom+N}const L=_(G.map(C=>C.bounds)),u=A(c.blocks,s),b={left:l+u.left,right:l+u.right,top:L.top,bottom:L.bottom},k={left:b.left-x,right:b.right+x,top:b.top-x,bottom:b.bottom+x};D.push(k),c.backgroundId!==void 0&&E.push({id:c.backgroundId,x:b.left-c.border.padding,y:b.top-c.border.padding,width:b.right-b.left+c.border.padding*2,height:b.bottom-b.top+c.border.padding*2}),y=k.bottom+N}const a=_(D),p=o.y-a.top,m=w(a,0,p);if(m.left<0||m.right>n.width||m.top<0||m.bottom>n.height)throw S(`Legend lane requires more ${t}-margin or vertical Canvas space.`,[m],n);if(i!==void 0&&H(m,i))throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and y-axis guides require more margin space.`);return{side:t,titleStartX:l,symbolCenterX:h,labelStartX:d,placements:z.map(c=>({...c,dy:c.dy+p,bounds:w(c.bounds,0,p)})),backgrounds:E.map(c=>({...c,y:c.y+p})),occupied:m}}function J({side:t,plot:o,canvas:n,bounds:r}){if(!["left","right"].includes(t))throw new Error("Single side legend placement requires a left or right edge.");const i=o.y-r.top,e=w(r,0,i);if(e.left<0||e.right>n.width||e.top<0||e.bottom>n.height)throw S(`Legend layout requires more ${t}-margin or Canvas space.`,[e],n);return{dy:i,occupied:e}}function w(t,o,n){return{left:t.left+o,right:t.right+o,top:t.top+n,bottom:t.bottom+n}}function q(t,o){return{left:t.left-o,right:t.right+o,top:t.top-o,bottom:t.bottom+o}}function O(t,o,n=!1){const r=[];let i=[],e=o.x;for(const s of t){const f=q(s.horizontal,s.inset);if(f.right-f.left>o.width&&!n)throw new Error("Horizontal legend block requires more plot width.");let l=e-f.left,h=w(f,l,0);i.length>0&&h.right>o.x+o.width&&(r.push(i),i=[],e=o.x,l=e-f.left,h=w(f,l,0)),i.push({group:s,interval:h,dx:l}),e=h.right+$}return i.length>0&&r.push(i),r}function P(t){const o=t.filter(e=>e.group.title!==void 0&&e.group.inline!==!0),n=o[0]?.group.title.y,r=o.length===0?0:o.reduce((e,s)=>Math.max(e,s.group.title.bounds.bottom-s.group.title.y),-1/0),i=n===void 0?B(t[0].group.element.top,t[0].group.element.bottom):n+r+U;return t.map(({group:e,dx:s})=>{if(e.atomic){const h=i-e.element.top,d=w(e.content,s,h);return{id:e.id,dx:s,titleDy:h,contentDy:h,foreground:d,occupied:q(d,e.inset),padding:e.padding,backgroundId:e.backgroundId}}const f=n===void 0?i-B(e.element.top,e.element.bottom):i-e.element.top,g=e.title===void 0?0:e.inline===!0?f:n-e.title.y,l=_([...e.title===void 0?[]:[w(e.title.bounds,s,g)],w(e.content,s,f)]);return{id:e.id,dx:s,titleDy:g,contentDy:f,foreground:l,occupied:q(l,e.inset),padding:e.padding,backgroundId:e.backgroundId}})}function R(t,o){return{...t,titleDy:t.titleDy+o,contentDy:t.contentDy+o,foreground:w(t.foreground,0,o),occupied:w(t.occupied,0,o)}}function Q({edge:t,plot:o,canvas:n,groups:r,collisionBounds:i=[],allowOverflow:e=!1}){if(!["top","bottom"].includes(t))throw new Error(`Unsupported horizontal legend lane "${t}".`);if(r.length<2)return;const s=O(r,o,e),f=s.map(P),g=[];let l;for(let d=0;d<f.length;d+=1){const y=f[d],z=_(y.map(a=>a.occupied));let E;if(d===0){const a=t==="top"?s[d].reduce((p,m)=>Math.max(p,m.group.horizontal.bottom),-1/0):s[d].reduce((p,m)=>Math.min(p,m.group.horizontal.top),1/0);E=t==="top"?a-z.bottom:a-z.top}else E=t==="bottom"?l.bottom+$-z.top:l.top-$-z.bottom;const D=y.map(a=>R(a,E));g.push(...D),l=_(D.map(a=>a.occupied))}const h=_(g.map(d=>d.occupied));if(g.some(d=>d.occupied.left<0||d.occupied.right>n.width||d.occupied.top<0||d.occupied.bottom>n.height))throw S(`Legend lane requires more ${t}-margin or Canvas space.`,[h],n);if(g.some(d=>i.some(y=>H(d.occupied,y)))){const d=t==="top"?"chart titles":"x-axis guides";throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and ${d} require more margin space.`)}return{edge:t,placements:g.map(d=>({...d,...d.backgroundId===void 0?{}:{background:{id:d.backgroundId,x:d.foreground.left-d.padding,y:d.foreground.top-d.padding,width:d.foreground.right-d.foreground.left+d.padding*2,height:d.foreground.bottom-d.foreground.top+d.padding*2}}})),occupied:h,rowCount:s.length}}function V({edge:t,plot:o,canvas:n,groups:r,align:i,offset:e,border:s,backgroundId:f,collisionBounds:g=[],allowOverflow:l=!1}){const h=T(s),d={...o,x:o.x+h,width:o.width-h*2},z=O(r,d,l).map(P);let E=0;const D=[];for(const u of z){const b=_(u.map(M=>M.occupied)),k=t==="top"?E-b.bottom:E-b.top,C=u.map(M=>R(M,k));D.push(...C);const I=_(C.map(M=>M.occupied));E=t==="top"?I.top-$:I.bottom+$}const a=_(D.map(u=>u.occupied)),p=q(a,h),m=p.right-p.left,x=(i==="left"?o.x:i==="right"?o.x+o.width-m:o.x+(o.width-m)/2)-p.left,G=t==="top"?o.y-e-p.bottom:o.y+o.height+e-p.top,L=w(p,x,G);if(L.left<0||L.right>n.width||L.top<0||L.bottom>n.height)throw S(`Combined legend requires more ${t}-margin or Canvas space.`,[L],n);if(g.some(u=>H(L,u)))throw new Error(`Combined ${t} legend and ${t==="top"?"chart titles":"x-axis guides"} require more margin space.`);return{placements:D.map(u=>({...u,dx:u.dx+x,titleDy:u.titleDy+G,contentDy:u.contentDy+G,...u.backgroundId===void 0?{}:{background:{id:u.backgroundId,x:u.foreground.left+x-u.padding,y:u.foreground.top+G-u.padding,width:u.foreground.right-u.foreground.left+u.padding*2,height:u.foreground.bottom-u.foreground.top+u.padding*2}}})),occupied:L,background:f===void 0?void 0:{id:f,x:a.left+x-s.padding,y:a.top+G-s.padding,width:a.right-a.left+s.padding*2,height:a.bottom-a.top+s.padding*2}}}export{$ as HORIZONTAL_LEGEND_BLOCK_GAP,U as HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP,N as SIDE_LEGEND_BLOCK_GAP,X as SIDE_LEGEND_LABEL_START,v as SIDE_LEGEND_SYMBOL_CENTER,K as isHorizontalEdgeLegend,V as resolveHorizontalLegendGroup,Q as resolveHorizontalLegendLane,j as resolveSideLegendLane,W as resolveSingleHorizontalLegendPlacement,J as resolveSingleSideLegendPlacement};
1
+ import{canvasOverflowError as $}from"./canvas.js";import{unionBounds as z}from"../core/textMetrics.js";const N=24,v=40,k=16,X=44,U=12;function K(t){return t!==void 0&&["top","bottom"].includes(t.position)&&t.layout!=="legacy-bottom"}function W({plot:t,canvas:o,config:n,bounds:r}){if(!K(n))throw new Error("Single legend placement requires a horizontal edge layout.");const i=n.align==="left"?t.x-r.left:n.align==="right"?t.x+t.width-r.right:t.x+t.width/2-B(r.left,r.right),e=n.position==="top"?t.y-n.offset-r.bottom:t.y+t.height+n.offset-r.top,s=w(r,i,e);if(s.left<0||s.right>o.width||s.top<0||s.bottom>o.height)throw $(`Legend layout requires more ${n.position}-margin or Canvas space.`,[s],o);return{dx:i,dy:e,occupied:s}}function B(t,o){const n=t+o,r=Number.isFinite(n)?n/2:t/2+o/2;return r===0?0:r}function T(t){return t===!1?0:t.padding+t.lineWidth/2}function A(t,o){let n=0,r=0;for(const i of t){const e=i.inset??0;if(i.preserveGrid){n=Math.min(n,-e),r=Math.max(r,i.bounds.right-i.bounds.left+e);continue}i.title!==void 0&&(r=Math.max(r,i.title.width+e)),n=Math.min(n,k+i.symbol.left-i.symbol.centerX-e),r=Math.max(r,k+i.symbol.right-i.symbol.centerX+e,o+i.labels.width+e)}return{left:n,right:r}}function Y(t,o,n){const r=o-t.bounds.top,i={left:t.bounds.left,right:t.bounds.right,top:t.bounds.top+r,bottom:t.bounds.bottom+r};return{id:t.id,dy:r,titleDx:t.title===void 0?void 0:t.preserveGrid?-t.bounds.left:-t.title.x,symbolDx:t.preserveGrid?-t.bounds.left:k-t.symbol.centerX,labelDx:t.preserveGrid?-t.bounds.left:n-t.labels.x,bounds:i}}function H(t,o){return t.left<o.right&&t.right>o.left&&t.top<o.bottom&&t.bottom>o.top}function j({side:t,plot:o,canvas:n,groups:r,axisBounds:i,allowOverflow:e=!1}){if(!["right","left"].includes(t))throw new Error(`Unsupported side legend lane "${t}".`);const s=r.flatMap(c=>c.blocks);if(s.length<2)return;const a=Math.max(X,...s.filter(c=>!c.preserveGrid).map(c=>k+Math.abs(c.labels.x-c.symbol.centerX))),p=A(s,a),h=s.reduce((c,m)=>Math.max(c,m.offset),-1/0),u=t==="right"?o.x+o.width+h:o.x-h-p.right,d=u+k,G=u+a;let y=o.y;const D=[],I=[],f=[];for(const c of r){const m=T(c.border);f.length>0&&(y+=m);const L=[];for(const C of c.blocks){const b=Y(C,y,a);L.push(b),D.push({...b,titleDx:b.titleDx===void 0?void 0:b.titleDx+u,symbolDx:b.symbolDx+u,labelDx:b.labelDx+u}),y=b.bounds.bottom+N}const l=z(L.map(C=>C.bounds)),M=A(c.blocks,a),x={left:u+M.left,right:u+M.right,top:l.top,bottom:l.bottom},S={left:x.left-m,right:x.right+m,top:x.top-m,bottom:x.bottom+m};f.push(S),c.backgroundId!==void 0&&I.push({id:c.backgroundId,x:x.left-c.border.padding,y:x.top-c.border.padding,width:x.right-x.left+c.border.padding*2,height:x.bottom-x.top+c.border.padding*2}),y=S.bottom+N}const g=z(f),E=o.y-g.top,_=w(g,0,E);if(!e&&(_.left<0||_.right>n.width||_.top<0||_.bottom>n.height))throw $(`Legend lane requires more ${t}-margin or vertical Canvas space.`,[_],n);if(i!==void 0&&H(_,i))throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and y-axis guides require more margin space.`);return{side:t,titleStartX:u,symbolCenterX:d,labelStartX:G,placements:D.map(c=>({...c,dy:c.dy+E,bounds:w(c.bounds,0,E)})),backgrounds:I.map(c=>({...c,y:c.y+E})),occupied:_}}function J({side:t,plot:o,canvas:n,bounds:r}){if(!["left","right"].includes(t))throw new Error("Single side legend placement requires a left or right edge.");const i=o.y-r.top,e=w(r,0,i);if(e.left<0||e.right>n.width||e.top<0||e.bottom>n.height)throw $(`Legend layout requires more ${t}-margin or Canvas space.`,[e],n);return{dy:i,occupied:e}}function w(t,o,n){return{left:t.left+o,right:t.right+o,top:t.top+n,bottom:t.bottom+n}}function q(t,o){return{left:t.left-o,right:t.right+o,top:t.top-o,bottom:t.bottom+o}}function O(t,o,n=!1){const r=[];let i=[],e=o.x;for(const s of t){const a=q(s.horizontal,s.inset);if(a.right-a.left>o.width&&!n)throw new Error("Horizontal legend block requires more plot width.");let h=e-a.left,u=w(a,h,0);i.length>0&&u.right>o.x+o.width&&(r.push(i),i=[],e=o.x,h=e-a.left,u=w(a,h,0)),i.push({group:s,interval:u,dx:h}),e=u.right+v}return i.length>0&&r.push(i),r}function P(t){const o=t.filter(e=>e.group.title!==void 0&&e.group.inline!==!0),n=o[0]?.group.title.y,r=o.length===0?0:o.reduce((e,s)=>Math.max(e,s.group.title.bounds.bottom-s.group.title.y),-1/0),i=n===void 0?B(t[0].group.element.top,t[0].group.element.bottom):n+r+U;return t.map(({group:e,dx:s})=>{if(e.atomic){const u=i-e.element.top,d=w(e.content,s,u);return{id:e.id,dx:s,titleDy:u,contentDy:u,foreground:d,occupied:q(d,e.inset),padding:e.padding,backgroundId:e.backgroundId}}const a=n===void 0?i-B(e.element.top,e.element.bottom):i-e.element.top,p=e.title===void 0?0:e.inline===!0?a:n-e.title.y,h=z([...e.title===void 0?[]:[w(e.title.bounds,s,p)],w(e.content,s,a)]);return{id:e.id,dx:s,titleDy:p,contentDy:a,foreground:h,occupied:q(h,e.inset),padding:e.padding,backgroundId:e.backgroundId}})}function R(t,o){return{...t,titleDy:t.titleDy+o,contentDy:t.contentDy+o,foreground:w(t.foreground,0,o),occupied:w(t.occupied,0,o)}}function Q({edge:t,plot:o,canvas:n,groups:r,collisionBounds:i=[],allowOverflow:e=!1}){if(!["top","bottom"].includes(t))throw new Error(`Unsupported horizontal legend lane "${t}".`);if(r.length<2)return;const s=O(r,o,e),a=s.map(P),p=[];let h;for(let d=0;d<a.length;d+=1){const G=a[d],y=z(G.map(f=>f.occupied));let D;if(d===0){const f=t==="top"?s[d].reduce((g,E)=>Math.max(g,E.group.horizontal.bottom),-1/0):s[d].reduce((g,E)=>Math.min(g,E.group.horizontal.top),1/0);D=t==="top"?f-y.bottom:f-y.top}else D=t==="bottom"?h.bottom+v-y.top:h.top-v-y.bottom;const I=G.map(f=>R(f,D));p.push(...I),h=z(I.map(f=>f.occupied))}const u=z(p.map(d=>d.occupied));if(p.some(d=>d.occupied.left<0||d.occupied.right>n.width||d.occupied.top<0||d.occupied.bottom>n.height))throw $(`Legend lane requires more ${t}-margin or Canvas space.`,[u],n);if(p.some(d=>i.some(G=>H(d.occupied,G)))){const d=t==="top"?"chart titles":"x-axis guides";throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and ${d} require more margin space.`)}return{edge:t,placements:p.map(d=>({...d,...d.backgroundId===void 0?{}:{background:{id:d.backgroundId,x:d.foreground.left-d.padding,y:d.foreground.top-d.padding,width:d.foreground.right-d.foreground.left+d.padding*2,height:d.foreground.bottom-d.foreground.top+d.padding*2}}})),occupied:u,rowCount:s.length}}function V({edge:t,plot:o,canvas:n,groups:r,align:i,offset:e,border:s,backgroundId:a,collisionBounds:p=[],allowOverflow:h=!1}){const u=T(s),d={...o,x:o.x+u,width:o.width-u*2},y=O(r,d,h).map(P);let D=0;const I=[];for(const l of y){const M=z(l.map(b=>b.occupied)),x=t==="top"?D-M.bottom:D-M.top,S=l.map(b=>R(b,x));I.push(...S);const C=z(S.map(b=>b.occupied));D=t==="top"?C.top-v:C.bottom+v}const f=z(I.map(l=>l.occupied)),g=q(f,u),E=g.right-g.left,c=(i==="left"?o.x:i==="right"?o.x+o.width-E:o.x+(o.width-E)/2)-g.left,m=t==="top"?o.y-e-g.bottom:o.y+o.height+e-g.top,L=w(g,c,m);if(L.left<0||L.right>n.width||L.top<0||L.bottom>n.height)throw $(`Combined legend requires more ${t}-margin or Canvas space.`,[L],n);if(p.some(l=>H(L,l)))throw new Error(`Combined ${t} legend and ${t==="top"?"chart titles":"x-axis guides"} require more margin space.`);return{placements:I.map(l=>({...l,dx:l.dx+c,titleDy:l.titleDy+m,contentDy:l.contentDy+m,...l.backgroundId===void 0?{}:{background:{id:l.backgroundId,x:l.foreground.left+c-l.padding,y:l.foreground.top+m-l.padding,width:l.foreground.right-l.foreground.left+l.padding*2,height:l.foreground.bottom-l.foreground.top+l.padding*2}}})),occupied:L,background:a===void 0?void 0:{id:a,x:f.left+c-s.padding,y:f.top+m-s.padding,width:f.right-f.left+s.padding*2,height:f.bottom-f.top+s.padding*2}}}export{v as HORIZONTAL_LEGEND_BLOCK_GAP,U as HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP,N as SIDE_LEGEND_BLOCK_GAP,X as SIDE_LEGEND_LABEL_START,k as SIDE_LEGEND_SYMBOL_CENTER,K as isHorizontalEdgeLegend,V as resolveHorizontalLegendGroup,Q as resolveHorizontalLegendLane,j as resolveSideLegendLane,W as resolveSingleHorizontalLegendPlacement,J as resolveSingleSideLegendPlacement};
@@ -1 +1 @@
1
- import{resolveTextBounds as f,resolveTextLines as m}from"../core/textMetrics.js";function s(e,t,n){const l=m(String(e),{fontSize:t.fontSize,align:"left",baseline:"middle",blockAlign:"middle"}),o=l.length>1?l:void 0,i=f({x:0,y:0,text:String(e),...t,lines:o,textAlign:"left",textBaseline:"middle"},n);return{lines:o,width:i.right-i.left,height:i.bottom-i.top,extraHeight:i.bottom-i.top-t.fontSize}}function h(e,t,n,l,o){if(n.align===void 0)return e;const i=s(t,n,o),r=f({x:e.x,y:e.y,text:t,...n,lines:i.lines,textAlign:e.align,textBaseline:"middle"},o),a=Math.min(r.left,...l.map(g=>g.left)),d=Math.max(r.right,...l.map(g=>g.right));return{...e,x:n.align==="left"?a:n.align==="right"?d:(a+d)/2,align:n.align}}export{h as alignLegendTitle,s as resolveLegendTitleText};
1
+ import{resolveTextBounds as m,resolveTextLines as x}from"../core/textMetrics.js";function u(n,r,t){const f=x(String(n),{fontSize:r.fontSize,align:"left",baseline:"middle",blockAlign:"middle"}),g=f.length>1?f:void 0,e=m({x:0,y:0,text:String(n),...r,lines:g,textAlign:"left",textBaseline:"middle"},t);return{lines:g,width:e.right-e.left,height:e.bottom-e.top,extraHeight:e.bottom-e.top-r.fontSize}}function p(n,r,t,f,g){if(t.align===void 0)return n;const e=u(r,t,g),i=m({x:n.x,y:n.y,text:r,...t,lines:e.lines,textAlign:n.align,textBaseline:"middle"},g),a=Math.min(i.left,...f.map(o=>o.left)),l=Math.max(i.right,...f.map(o=>o.right));return{...n,x:t.align==="left"?a:t.align==="right"?l:(a+l)/2,align:t.align}}function b(n,r,t,f){if(t.align===void 0)return n;const g=new Map,e=n.map(i=>`${i.x}:${i.align??"left"}`);return n.forEach((i,a)=>{const l=u(r[a],t,f),o=i.align==="right"?l.width:i.align==="center"?l.width/2:0,h=i.x-o,c=h+l.width,d=g.get(e[a]);g.set(e[a],{left:Math.min(d?.left??h,h),right:Math.max(d?.right??c,c)})}),n.map((i,a)=>{const l=g.get(e[a]);return{...i,x:t.align==="left"?l.left:t.align==="right"?l.right:(l.left+l.right)/2,align:t.align}})}export{b as alignLegendLabels,p as alignLegendTitle,u as resolveLegendTitleText};
@@ -0,0 +1,27 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import { normalizeActionTargets } from "../grammar/actionTargets.js";
3
+ import { resolveEligibleLayer } from "../selectors/layers.js";
4
+ import { ownership } from "../selectors/markOwners.js";
5
+
6
+ // Creation and inspection share the same explicit source ownership contract.
7
+ export function resolveAdjacentSource(program, source) {
8
+ const grouped = program.compositionSpec && program.compositionSpec.type !== "facet";
9
+ const scoped = isPlainObject(source);
10
+ if (grouped && !scoped) throw new Error("Adding beside a concat requires source: { childPath, id }.");
11
+ let view = program, id = source;
12
+ if (scoped) {
13
+ const selector = normalizeActionTargets(source)[0];
14
+ if (selector.id === undefined) throw new Error("Adjacent source requires an explicit mark id.");
15
+ id = selector.id;
16
+ for (const child of selector.childPath) {
17
+ if (view.compositionSpec?.type === "facet") throw new Error("Select the facet source view, not a generated facet cell.");
18
+ if (!Object.hasOwn(view.children, child)) throw new Error(`Unknown adjacent source child "${child}".`);
19
+ view = view.children[child];
20
+ }
21
+ }
22
+ if (view.compositionSpec && view.compositionSpec.type !== "facet") throw new Error("Adjacent source must identify a unit or facet view.");
23
+ const owners = ownership(view);
24
+ const layer = resolveEligibleLayer(view, { target: id, targetOption: "source", label: "adjacent chart source",
25
+ predicate: layer => !owners.has(layer.id) && !layer.derivedBindings?.overlay && layer.data !== undefined });
26
+ return { view, layer };
27
+ }