ggaction 0.0.20 → 0.0.22

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 (254) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +3 -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/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/interval.js +4 -3
  165. package/src/grammar/jitter.js +2 -2
  166. package/src/grammar/legendSpacing.js +45 -0
  167. package/src/grammar/lineMissing.js +90 -0
  168. package/src/grammar/lineSeries.js +1 -376
  169. package/src/grammar/lookup.js +97 -0
  170. package/src/grammar/markFilter.js +4 -2
  171. package/src/grammar/markLabels.js +8 -3
  172. package/src/grammar/numeric.js +1 -1
  173. package/src/grammar/pathCommands.js +1 -1
  174. package/src/grammar/pathSeries.js +1 -109
  175. package/src/grammar/pointPolygon.js +81 -0
  176. package/src/grammar/pointShapes.js +15 -15
  177. package/src/grammar/rects.js +1 -1
  178. package/src/grammar/regression/derive.js +1 -195
  179. package/src/grammar/scales/appearance.js +1 -1
  180. package/src/grammar/scales/color.js +1 -1
  181. package/src/grammar/scales/colorConsumers.js +1 -63
  182. package/src/grammar/scales/continuous.js +58 -1
  183. package/src/grammar/scales/definition.js +1 -1
  184. package/src/grammar/scales/ordinal.js +5 -2
  185. package/src/grammar/scales/policies.js +3 -3
  186. package/src/grammar/schemas/graphicBounds.js +40 -12
  187. package/src/grammar/schemas/graphicClip.js +23 -0
  188. package/src/grammar/schemas/graphicTree.js +16 -0
  189. package/src/grammar/statisticalReference.js +22 -0
  190. package/src/grammar/text.js +65 -1
  191. package/src/grammar/ticks.js +92 -1
  192. package/src/grammar/transformTopology.js +3 -1
  193. package/src/grammar/transforms.js +24 -0
  194. package/src/inspection.js +1 -495
  195. package/src/inspectionDescriptors.js +1 -1
  196. package/src/layout/canvas.js +1 -1
  197. package/src/layout/facetContentFlow.js +101 -0
  198. package/src/layout/facetHeaderTitles.js +1 -0
  199. package/src/layout/facetTopLane.js +29 -0
  200. package/src/layout/facets.js +1 -1
  201. package/src/layout/legendGroup.js +1 -1
  202. package/src/layout/legendItems.js +26 -10
  203. package/src/layout/legendSpacing.js +66 -0
  204. package/src/layout/plotDrawOrder.js +17 -0
  205. package/src/materialization/bars/histogram.js +8 -2
  206. package/src/materialization/bars/resolve.js +1 -1
  207. package/src/materialization/composition.js +1 -0
  208. package/src/materialization/compositionSnapshot.js +22 -2
  209. package/src/materialization/dataProvenance.js +1 -1
  210. package/src/materialization/facetGuides/layout.js +1 -1
  211. package/src/materialization/facetGuides/placement.js +23 -11
  212. package/src/materialization/facetGuides/preparation.js +43 -24
  213. package/src/materialization/facetHeaders.js +1 -1
  214. package/src/materialization/facetTopLane.js +25 -0
  215. package/src/materialization/facets.js +1 -1
  216. package/src/materialization/graphicHierarchy.js +5 -0
  217. package/src/materialization/graphicTranslation.js +2 -0
  218. package/src/materialization/guides/layout.js +1 -1
  219. package/src/materialization/guides/resources.js +1 -1
  220. package/src/materialization/legends.js +1 -1
  221. package/src/materialization/markClip.js +26 -0
  222. package/src/materialization/marks/capabilities.js +3 -2
  223. package/src/materialization/marks/grouping.js +7 -5
  224. package/src/materialization/marks/index.js +4 -2
  225. package/src/materialization/marks/policies.js +4 -0
  226. package/src/materialization/plotDrawOrder.js +44 -0
  227. package/src/materialization/rect.js +11 -3
  228. package/src/materialization/scaleGuideDependencies.js +13 -5
  229. package/src/materialization/selection/items/common.js +1 -1
  230. package/src/materialization/selection/items/path.js +1 -0
  231. package/src/materialization/selection/policies/index.js +2 -1
  232. package/src/materialization/text.js +10 -5
  233. package/src/mcp/cli.js +0 -0
  234. package/src/persistence/validation.js +108 -18
  235. package/src/persistence.js +4 -2
  236. package/src/renderers/canvas/index.js +16 -0
  237. package/src/renderers/canvas/isolated.js +11 -1
  238. package/src/renderers/canvas/native.js +7 -0
  239. package/src/renderers/canvas/scene.js +66 -0
  240. package/src/renderers/canvas/text.js +3 -2
  241. package/src/renderers/svg.js +1 -617
  242. package/src/renderers/svgSerialization.js +643 -0
  243. package/src/renderers/transitionScene.js +67 -0
  244. package/src/scene.js +89 -0
  245. package/src/selectors/axes.js +1 -1
  246. package/src/selectors/datasets.js +1 -0
  247. package/src/selectors/grids.js +9 -0
  248. package/src/selectors/markClip.js +28 -0
  249. package/src/selectors/raincloud.js +7 -0
  250. package/src/version.js +1 -1
  251. package/types/basic.d.ts +7 -0
  252. package/types/index.d.ts +8 -0
  253. package/types/program.d.ts +270 -60
  254. package/types/scene.d.ts +35 -0
@@ -1 +1 @@
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
+ import{annotateError as p}from"../core/diagnostics.js";import{cloneAndFreeze as n,isPlainObject as u}from"../core/immutable.js";import{validateNonEmptyString as l,validateNonNegativeFinite as v,validatePositiveFinite as a,validateOptionObject as h,validateBoolean as w}from"../core/validation.js";const f=Object.freeze(["top","right","bottom","left"]),d=n({top:30,right:30,bottom:60,left:70}),x=n({width:640,height:400,background:"white",margin:d});function c(e,r){v(e,`Canvas margin.${r}`)}function E(e,r=d){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(!u(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}".`);c(e[t],t)}return n({...r,...e})}function O(e){if(e===!1)return!1;h(e,["color","lineWidth","underlay"],"Canvas plotBorder"),e.underlay!==void 0&&w(e.underlay,"Canvas plotBorder underlay");const{underlay:r,...o}=e,t={color:"#ddd",lineWidth:1,...o,...r===!0?{underlay:!0}:{}};return l(t.color,"Canvas plotBorder color"),a(t.lineWidth,"Canvas plotBorder lineWidth"),n(t)}function j({width:e,height:r,background:o,margin:t,plotBorder:i=!1}){a(e,"Canvas width"),a(r,"Canvas height"),l(o,"Canvas background");for(const s of f)c(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 b({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 N(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 b({width:r.properties.width,height:r.properties.height,margin:o})}function m(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 F(e,r,o){const t=new Error(e),i=m(r,o);return i===void 0?t:p(t,{reason:"canvas-overflow",canvasOverflow:i})}function A(e,r){const o=Object.hasOwn(e,"plot")?e.plot:r;if(!(o===void 0||o===!1)){if(h(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,d as DEFAULT_MARGIN,F as canvasOverflowError,b as createGraphicBounds,E as normalizeMargin,O as normalizePlotBorder,m as resolveCanvasOverflow,A as resolveCanvasPlot,N as resolveGraphicBounds,j as validateCanvasState};
@@ -0,0 +1,101 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validateCompositionSpacing } from "./composition.js";
3
+
4
+ /** Pack ordered full content bounds without imposing common plot origins. */
5
+ export function resolveFacetContentFlow({ children, columns, gap = 16 } = {}) {
6
+ if (!Array.isArray(children) || children.length === 0) {
7
+ throw new TypeError("Facet content flow requires non-empty children.");
8
+ }
9
+ columns ??= children.length;
10
+ if (!Number.isInteger(columns) || columns <= 0) {
11
+ throw new RangeError("Facet content flow columns must be a positive integer.");
12
+ }
13
+ columns = Math.min(columns, children.length);
14
+ validateCompositionSpacing(gap, "Facet content flow gap");
15
+ const ids = new Set();
16
+ const rightExtents = Array(columns).fill(0);
17
+ const bottomExtents = Array(Math.ceil(children.length / columns)).fill(0);
18
+ const cells = children.map((child, index) => {
19
+ if (!isPlainObject(child) || typeof child.id !== "string" ||
20
+ child.id.length === 0 || ids.has(child.id)) {
21
+ throw new TypeError("Facet content flow requires unique non-empty child IDs.");
22
+ }
23
+ const bounds = child.bounds;
24
+ if (!isPlainObject(bounds) ||
25
+ !["left", "right", "top", "bottom"].every(key => Number.isFinite(bounds[key])) ||
26
+ bounds.left > bounds.right || bounds.top > bounds.bottom) {
27
+ throw new RangeError("Facet content flow requires finite ordered content bounds.");
28
+ }
29
+ ids.add(child.id);
30
+ const column = index % columns, row = Math.floor(index / columns);
31
+ rightExtents[column] = Math.max(rightExtents[column], Math.ceil(bounds.right));
32
+ bottomExtents[row] = Math.max(bottomExtents[row], Math.ceil(bounds.bottom));
33
+ return { id: child.id, column, row, bounds: { ...bounds } };
34
+ });
35
+ const columnOrigins = Array(columns).fill(0);
36
+ let x = 0;
37
+ const placed = cells.map(cell => {
38
+ x = cell.column === 0 ? 0 : x + rightExtents[cell.column - 1] + gap +
39
+ Math.max(0, Math.ceil(-cell.bounds.left));
40
+ if (cell.row > 0) {
41
+ columnOrigins[cell.column] += bottomExtents[cell.row - 1] + gap +
42
+ Math.max(0, Math.ceil(-cell.bounds.top));
43
+ }
44
+ const y = columnOrigins[cell.column];
45
+ if (![x, y, x + cell.bounds.left, x + cell.bounds.right,
46
+ y + cell.bounds.top, y + cell.bounds.bottom].every(Number.isFinite)) {
47
+ throw new RangeError("Facet content flow exceeds finite layout bounds.");
48
+ }
49
+ return {
50
+ id: cell.id, column: cell.column, row: cell.row, x, y,
51
+ bounds: {
52
+ left: x + cell.bounds.left, right: x + cell.bounds.right,
53
+ top: y + cell.bounds.top, bottom: y + cell.bounds.bottom
54
+ }
55
+ };
56
+ });
57
+ const bounds = { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity };
58
+ for (const child of placed) {
59
+ bounds.left = Math.min(bounds.left, child.bounds.left);
60
+ bounds.right = Math.max(bounds.right, child.bounds.right);
61
+ bounds.top = Math.min(bounds.top, child.bounds.top);
62
+ bounds.bottom = Math.max(bounds.bottom, child.bounds.bottom);
63
+ }
64
+ return cloneAndFreeze({ columns, gap, children: placed, bounds });
65
+ }
66
+
67
+ /** Place one shared footer per occupied column beneath the complete content. */
68
+ export function resolveFacetContentFooters({ flow, footers, offset = 0 } = {}) {
69
+ if (!isPlainObject(flow) || !Array.isArray(flow.children) || flow.children.length === 0 ||
70
+ !Number.isFinite(flow.bounds?.bottom)) {
71
+ throw new TypeError("Facet content footers require a resolved content flow.");
72
+ }
73
+ if (!Array.isArray(footers) || !Number.isFinite(offset)) {
74
+ throw new TypeError("Facet content footers require an array and finite offset.");
75
+ }
76
+ const lastByColumn = new Map(), occupied = new Set(), ownerIds = new Set();
77
+ for (const child of flow.children) {
78
+ if (!isPlainObject(child) || typeof child.id !== "string" || child.id.length === 0 || ownerIds.has(child.id) ||
79
+ !Number.isInteger(child.column) || child.column < 0 ||
80
+ !Number.isInteger(child.row) || child.row < 0 || !Number.isFinite(child.x)) {
81
+ throw new RangeError("Facet content footer owners require finite occupied cells.");
82
+ }
83
+ const slot = `${child.row}:${child.column}`;
84
+ if (occupied.has(slot)) throw new Error("Facet content footer column has duplicate cells.");
85
+ occupied.add(slot); ownerIds.add(child.id);
86
+ const previous = lastByColumn.get(child.column);
87
+ if (previous === undefined || child.row > previous.row) lastByColumn.set(child.column, child);
88
+ }
89
+ const columns = new Set(), ids = new Set(), y = Math.ceil(flow.bounds.bottom) + offset;
90
+ if (!Number.isFinite(y)) throw new RangeError("Facet content footer exceeds finite layout bounds.");
91
+ return cloneAndFreeze(footers.map(footer => {
92
+ if (!isPlainObject(footer) || typeof footer.id !== "string" || footer.id.length === 0 ||
93
+ ids.has(footer.id) || !Number.isInteger(footer.column) ||
94
+ columns.has(footer.column) || !lastByColumn.has(footer.column)) {
95
+ throw new Error("Facet content footer requires one unique ID and occupied column.");
96
+ }
97
+ ids.add(footer.id); columns.add(footer.column);
98
+ const owner = lastByColumn.get(footer.column);
99
+ return { id: footer.id, column: footer.column, ownerId: owner.id, x: owner.x, y };
100
+ }));
101
+ }
@@ -0,0 +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};
@@ -0,0 +1,29 @@
1
+ import { DEFAULT_FACET_TOP_LANE, FACET_TOP_LANE_MODES } from "../core/vocabulary.js";
2
+ import { textBoundsFitCanvas, textBoundsIntersect } from "../core/textMetrics.js";
3
+
4
+ export function normalizeFacetTopLane(value, spacing) {
5
+ const mode = value === undefined ? DEFAULT_FACET_TOP_LANE : value;
6
+ if (!FACET_TOP_LANE_MODES.includes(mode)) {
7
+ throw new Error('Facet topLane must be "stacked" or "shared".');
8
+ }
9
+ if (mode !== DEFAULT_FACET_TOP_LANE && spacing !== "plot") {
10
+ throw new Error('Shared facet topLane requires spacing "plot".');
11
+ }
12
+ return mode === DEFAULT_FACET_TOP_LANE ? undefined : mode;
13
+ }
14
+
15
+ export function assertFacetTopLaneBounds({ canvas, titles, headers, retained, plots }) {
16
+ const owned = [...titles, ...headers];
17
+ if (owned.some(bounds => !textBoundsFitCanvas(bounds, canvas))) {
18
+ throw new Error("Shared facet top lane text does not fit the final Canvas.");
19
+ }
20
+ if (titles.some(title => headers.some(header => textBoundsIntersect(title, header)))) {
21
+ throw new Error("Shared facet top lane title overlaps a facet header.");
22
+ }
23
+ if (owned.some(bounds => plots.some(plot => textBoundsIntersect(bounds, plot)))) {
24
+ throw new Error("Shared facet top lane text overlaps a child plot.");
25
+ }
26
+ if (owned.some(bounds => retained.some(other => textBoundsIntersect(bounds, other)))) {
27
+ throw new Error("Shared facet top lane text overlaps retained chart text.");
28
+ }
29
+ }
@@ -1 +1 @@
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};
1
+ import{cloneAndFreeze as et,isPlainObject as U}from"../core/immutable.js";import{normalizeFacetTopLane as ot}from"./facetTopLane.js";import{annotateError as rt}from"../core/diagnostics.js";import{DEFAULT_COMPOSITION_LAYOUT as P,normalizeCompositionAlign as nt,normalizeCompositionChildren as lt,normalizeCompositionPadding as it,validateCompositionSpacing as b}from"./composition.js";const at=18,st=132,ct=0,ut=Object.freeze(["right","left","top","bottom"]);function mt(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 ht(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 Q(r,i){return i==="start"?0:i==="end"?r:r/2}const M=Object.freeze({top:0,right:0,bottom:0,left:0});function ft(r,i,f){if(r===void 0)return{outer:M,cellRows:{top:Array(i).fill(0),bottom:Array(i).fill(0)}};if(!U(r)||Object.keys(r).some(o=>!["outer","cellRows","cellColumns"].includes(o)))throw new TypeError("Facet headerLayout must contain outer and cellRows.");const d={...M,...r.outer??{}};if(!U(r.outer??{})||Object.keys(d).some(o=>!Object.hasOwn(M,o)))throw new TypeError("Facet headerLayout.outer must contain side reservations.");for(const[o,u]of Object.entries(d))b(u,`Facet headerLayout.outer.${o}`);const g={outer:d};for(const[o,u,x,z]of[["cellRows",["top","bottom"],i,"row"],["cellColumns",["left","right"],f,"column"]]){if(o==="cellColumns"&&r[o]===void 0)continue;const v=o==="cellRows"?r[o]??{}:r[o];if(!U(v)||Object.keys(v).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 E=v[a]??Array(x).fill(0);if(!Array.isArray(E)||E.length!==x)throw new RangeError(`Facet headerLayout.${o}.${a} must match ${z} count.`);return E.forEach((m,D)=>b(m,`Facet headerLayout.${o}.${a}[${D}]`)),[a,[...E]]}))}return g}function yt({children:r,columns:i,gap:f=P.gap,align:d=P.align,padding:g=P.padding,titleHeight:o=0,sharedLegend:u=!1,sharedLegendGap:x=at,sharedLegendWidth:z=st,sharedLegendHeight:v=ct,sharedLegendPosition:a="right",headerLayout:E,spacing:m="canvas",topLane:D,plots:L}={}){if(!["canvas","plot"].includes(m))throw new Error(`Unknown facet spacing "${m}".`);const $=ot(D,m),H=new Map((L??[]).map(e=>[e.id,e])),W=r;let h=M;if(m==="plot"){if(!Array.isArray(L)||L.length!==r?.length||H.size!==L.length)throw new Error("Plot spacing requires one plot bounds record per child.");h={...M},r=r.map(e=>{const t=H.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 V=r?.map(e=>e?.value),F=ht(r??[],i),I=lt(r?.map(({value:e,row:t,column:s,...l})=>l)),c=F?.columns??mt(i,I.length),w=b(f,"Facet gap"),N=nt(d),S=it(g),p=Object.fromEntries(Object.keys(S).map(e=>[e,S[e]+h[e]])),j=b(o,"Facet title height"),Y=$===void 0?j:0;if(typeof u!="boolean")throw new TypeError("Facet sharedLegend must be a boolean.");const k=b(x,"Facet shared legend gap"),Z=b(z,"Facet shared legend width"),B=b(v,"Facet shared legend height");if(!ut.includes(a))throw new Error(`Unknown facet shared legend position "${a}".`);const O=["right","left"].includes(a),J=["left","top"].includes(a),G=u?k+(O?Z:B):0,A=F?.rows??Math.ceil(I.length/c);let n=ft(E,A,c);if(m==="plot"){let e=0;for(const[t,s]of[[n.cellRows.bottom,n.cellRows.top],[n.cellColumns?.right??[],n.cellColumns?.left??[]]])for(let l=1;l<t.length;l+=1)e=Math.max(e,t[l-1]+s[l]);if(e>w)throw rt(new Error("Plot spacing gap is too small for the facet headers."),{code:"invalid-value",optionPath:"gap",actual:w,limit:e});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(A).fill(0),bottom:Array(A).fill(0)}},$!==void 0&&(n.outer.top=0)}const T=n.cellColumns??{left:Array(c).fill(0),right:Array(c).fill(0)},C=Array(c).fill(-1/0),R=Array(A).fill(-1/0);I.forEach((e,t)=>{const s=F===void 0?t%c:r[t].column,l=F===void 0?Math.floor(t/c):r[t].row;C[s]=Math.max(C[s],e.width),R[l]=Math.max(R[l],e.height)});const X=C.map((e,t)=>p.left+n.outer.left+C.slice(0,t).reduce((s,l,_)=>s+T.left[_]+l+T.right[_],0)+w*t+T.left[t]),tt=R.map((e,t)=>Y+p.top+n.outer.top+R.slice(0,t).reduce((s,l,_)=>s+n.cellRows.top[_]+l+n.cellRows.bottom[_],0)+w*t+n.cellRows.top[t]),y=I.map((e,t)=>{const s=F===void 0?t%c:r[t].column,l=F===void 0?Math.floor(t/c):r[t].row;return{id:e.id,value:V[t],column:s,row:l,x:X[s]+(J&&O?G:0)+Q(C[s]-e.width,N),y:tt[l]+(J&&!O?G:0)+Q(R[l]-e.height,N),width:e.width,height:e.height}}),q=p.left+n.outer.left+C.reduce((e,t)=>e+t,0)+T.left.reduce((e,t)=>e+t,0)+T.right.reduce((e,t)=>e+t,0)+w*Math.max(0,c-1)+n.outer.right+p.right,K=Y+p.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)+w*Math.max(0,A-1)+n.outer.bottom+p.bottom;if(m==="plot")for(let e=0;e<y.length;e+=1){const t=H.get(y[e].id);y[e].x-=t.x,y[e].y-=t.y,y[e].width=W[e].width,y[e].height=W[e].height}return et({...m==="plot"?{spacing:m}:{},...$===void 0?{}:{topLane:$},columns:c,rows:A,gap:w,align:N,padding:S,titleHeight:j,headerLayout:n,gridWidth:q,width:q+(O?G:0),height:K+(O?0:G),children:y,...u?{legend:{position:a,x:a==="right"?q+k:0,y:a==="bottom"?K+k:a==="top"?j:j+p.top+30,width:Z,height:B}}:{}})}export{at as DEFAULT_FACET_LEGEND_GAP,ct as DEFAULT_FACET_LEGEND_HEIGHT,st as DEFAULT_FACET_LEGEND_WIDTH,yt as resolveFacetLayout};
@@ -1 +1 @@
1
- import{PLOT_LEGEND_POSITIONS as M}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as N,HORIZONTAL_LEGEND_BLOCK_GAP as P}from"./legendLane.js";function A({blocks:g,group:l,plot:e,canvas:L,allowOverflow:O=!1,deferCanvasFit:G=!1}){if(g.length===0)return[];const{placement:t,blockLayout:f}=l,u=M.includes(t.position),c=["left","right"].includes(t.position),x=f==="column"||f==="wrap"&&c,y=u||c||t.at!==void 0?N:P,_=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>_&&!O)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 I={id:i.id,x:h,y:p,width:o,height:E};return x?p+=E+y:(h+=o+y,w=Math.max(w,E)),I}),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(!G&&(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{A as resolveLegendGroupPlacement};
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,6 +1,7 @@
1
1
  import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
2
2
  import { alignLegendStart, resolveLegendGrid, isPlotLegend, legendLabelLineHeight } from "./legend.js";
3
3
  import { alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
4
+ import {resolveLegendContentSpacing} from "./legendSpacing.js";
4
5
 
5
6
  // Measure item content before choosing its edge. Family owners supply the
6
7
  // sample dimensions and formatted labels; this module does not inspect scales.
@@ -28,6 +29,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
28
29
  let symbolX;
29
30
  let labelX;
30
31
  let itemY;
32
+ let itemRows;
31
33
  let title;
32
34
  if (config.layout === "legacy-bottom") {
33
35
  const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
@@ -41,6 +43,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
41
43
  });
42
44
  labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
43
45
  itemY = labels.map(() => canvas.height - 28);
46
+ itemRows = labels.map(() => 0);
44
47
  title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
45
48
  } else if (side) {
46
49
  const grid = resolveLegendGrid({
@@ -73,6 +76,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
73
76
  firstY = titleBounds.bottom + 12 - itemTop;
74
77
  }
75
78
  itemY = grid.cells.map(cell => firstY + cell.row * pitch);
79
+ itemRows = grid.cells.map(cell => cell.row);
76
80
  title = { x, y: plot.y + 20, align: "left" };
77
81
  } else {
78
82
  const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
@@ -95,27 +99,39 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
95
99
  symbolX = grid.cells.map(cell => gridX + columns[cell.column] - sampleLeft);
96
100
  labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
97
101
  itemY = grid.cells.map(cell => gridY + grid.rowHeight / 2 + cell.row * (grid.rowHeight + config.itemGap));
102
+ itemRows = grid.cells.map(cell => cell.row);
98
103
  title = inline ? { x, y: y + height / 2, align: "left" }
99
104
  : { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
100
105
  }
101
106
  const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
102
107
  x, y, text, ...style, textAlign: align, textBaseline: "middle"
103
108
  }, profile);
104
- const bodyBounds = [
105
- ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
106
- ...symbolX.map((x, index) => {
109
+ const labelBounds = labels.map((label,index) => textBounds(labelX[index],itemY[index],label,config.labels));
110
+ const symbolItemBounds = symbolX.map((x, index) => {
107
111
  const local = sampleBounds[index];
108
112
  return { left: x + local.left, right: x + local.right,
109
113
  top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
110
- })
111
- ];
114
+ });
115
+ const spacingItems = config.rowPadding === undefined && config.titlePadding === undefined ? undefined
116
+ : labels.map((label,index) => ({row:itemRows[index],x:symbolX[index],y:itemY[index],bounds:[
117
+ ...(label === "" ? [] : [labelBounds[index]]),
118
+ ...(symbol.itemBounds !== undefined && index >= symbol.itemBounds.length ? [] : [symbolItemBounds[index]])
119
+ ]}));
120
+ const bodyBounds = spacingItems === undefined
121
+ ? [...labelBounds,...symbolItemBounds] : spacingItems.flatMap(item => item.bounds);
112
122
  if (titleVisible) title = alignLegendTitle(title, config.title, config.titleStyle, bodyBounds, profile);
113
- const bounds = [
114
- ...(titleVisible ? [resolveTextBounds({ x: title.x, y: title.y,
123
+ const titleBounds = titleVisible ? resolveTextBounds({ x: title.x, y: title.y,
115
124
  text: config.title, ...config.titleStyle, lines: titleText.lines,
116
- textAlign: title.align, textBaseline: "middle" }, profile)] : []),
117
- ...bodyBounds
118
- ];
125
+ textAlign: title.align, textBaseline: "middle" }, profile) : undefined;
126
+ let bounds = [...(titleVisible ? [titleBounds] : []),...bodyBounds];
127
+ const spacing = spacingItems === undefined ? undefined : resolveLegendContentSpacing(config,spacingItems,titleBounds);
128
+ if (spacing !== undefined) {
129
+ symbolX = symbolX.map((x,index) => x+spacing.offsets[index].dx);
130
+ labelX = labelX.map((x,index) => x+spacing.offsets[index].dx);
131
+ itemY = itemY.map((y,index) => y+spacing.offsets[index].dy);
132
+ title = {...title,x:title.x+spacing.titleOffset.dx,y:title.y+spacing.titleOffset.dy};
133
+ bounds = spacing.bounds;
134
+ }
119
135
  if (config.layout === "legacy-bottom") {
120
136
  const itemBounds = titleVisible ? bounds.slice(1) : bounds;
121
137
  if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
@@ -0,0 +1,66 @@
1
+ const shift = (bounds, {dx, dy}) => ({
2
+ left:bounds.left + dx, right:bounds.right + dx,
3
+ top:bounds.top + dy, bottom:bounds.bottom + dy
4
+ });
5
+ const union = bounds => ({
6
+ left:Math.min(...bounds.map(b => b.left)), right:Math.max(...bounds.map(b => b.right)),
7
+ top:Math.min(...bounds.map(b => b.top)), bottom:Math.max(...bounds.map(b => b.bottom))
8
+ });
9
+
10
+ // Family owners supply existing concrete text/sample bounds and logical row
11
+ // identities. This pure function neither reads scales nor changes a recipe.
12
+ export function resolveLegendContentSpacing(config, items, titleBounds) {
13
+ if (config.rowPadding === undefined && config.titlePadding === undefined) return undefined;
14
+ if (items.length === 0) throw new Error("Legend content spacing requires a body item.");
15
+ const offsets = items.map(() => ({dx:0,dy:0}));
16
+ const titleOffset = {dx:0,dy:0};
17
+ const boundsFor = (item, index) => item.bounds.length > 0 ? item.bounds
18
+ : [{left:item.x,right:item.x,top:item.y,bottom:item.y}];
19
+ const originalBody = items.flatMap(boundsFor);
20
+ const original = union([...originalBody, ...(titleBounds === undefined ? [] : [titleBounds])]);
21
+ if (config.rowPadding !== undefined) {
22
+ const rows = new Map();
23
+ items.forEach((item,index) => {
24
+ const row = rows.get(item.row) ?? {indices:[],bounds:[]};
25
+ row.indices.push(index);row.bounds.push(...boundsFor(item,index));rows.set(item.row,row);
26
+ });
27
+ let previousBottom;
28
+ for (const [,row] of [...rows.entries()].sort(([a],[b]) => a-b)) {
29
+ const occupied = union(row.bounds);
30
+ const dy = previousBottom === undefined ? 0 : previousBottom + config.rowPadding - occupied.top;
31
+ for (const index of row.indices) offsets[index].dy = dy;
32
+ previousBottom = occupied.bottom + dy;
33
+ }
34
+ }
35
+ const body = () => items.flatMap((item,index) => boundsFor(item,index).map(b => shift(b,offsets[index])));
36
+ if (titleBounds !== undefined && config.titlePadding !== undefined) {
37
+ const occupied = union(body());
38
+ if (config.titlePosition === "left") {
39
+ const dx = titleBounds.right + config.titlePadding - occupied.left;
40
+ for (const offset of offsets) offset.dx += dx;
41
+ } else if (config.titlePosition === "bottom") {
42
+ titleOffset.dy = occupied.bottom + config.titlePadding - titleBounds.top;
43
+ } else {
44
+ const dy = titleBounds.bottom + config.titlePadding - occupied.top;
45
+ for (const offset of offsets) offset.dy += dy;
46
+ }
47
+ }
48
+ const occupied = [...body(), ...(titleBounds === undefined ? [] : [shift(titleBounds,titleOffset)])];
49
+ if (occupied.some(bounds => Object.values(bounds).some(value => !Number.isFinite(value)))) {
50
+ throw new RangeError("Legend content spacing requires finite occupied bounds.");
51
+ }
52
+ const beforeAnchor = union(occupied);
53
+ const right = config.position === "left" || config.position?.endsWith("right");
54
+ const horizontal = ["top","bottom"].includes(config.position);
55
+ const align = horizontal ? config.align ?? "center" : right ? "right" : "left";
56
+ const coordinate = (bounds, side) => side === "center" ? (bounds.left+bounds.right)/2 : bounds[side];
57
+ const dx = coordinate(original,align) - coordinate(beforeAnchor,align);
58
+ const bottom = config.position === "top" || config.position?.startsWith("bottom-");
59
+ const dy = bottom ? original.bottom - beforeAnchor.bottom : original.top - beforeAnchor.top;
60
+ for (const offset of offsets) {offset.dx += dx;offset.dy += dy;}
61
+ titleOffset.dx += dx;titleOffset.dy += dy;
62
+ if ([...offsets,titleOffset].some(offset => !Number.isFinite(offset.dx) || !Number.isFinite(offset.dy))) {
63
+ throw new RangeError("Legend content spacing requires finite offsets.");
64
+ }
65
+ return {offsets,titleOffset,bounds:[...body(),...(titleBounds === undefined ? [] : [shift(titleBounds,titleOffset)])]};
66
+ }
@@ -0,0 +1,17 @@
1
+ export function resolvePlotDrawOrder({ children, grids, border, axes }) {
2
+ const present = new Set(children);
3
+ const gridOrder = grids.filter(grid => present.has(grid.id)).sort((a, b) =>
4
+ (a.zIndex ?? 0) - (b.zIndex ?? 0) || a.index - b.index);
5
+ const axisOrder = axes.map(axis => ({ ...axis, ids: children.filter(id => axis.ids.includes(id)) }));
6
+ const borderId = present.has(border?.id) ? [border.id] : [];
7
+ const owned = new Set([...gridOrder.map(grid => grid.id), ...borderId, ...axisOrder.flatMap(axis => axis.ids)]);
8
+ return [
9
+ ...gridOrder.filter(grid => (grid.zIndex ?? 0) <= 0).map(grid => grid.id),
10
+ ...(border?.underlay ? borderId : []),
11
+ ...axisOrder.filter(axis => axis.underlay).flatMap(axis => axis.ids),
12
+ ...children.filter(id => !owned.has(id)),
13
+ ...gridOrder.filter(grid => (grid.zIndex ?? 0) > 0).map(grid => grid.id),
14
+ ...(border?.underlay ? [] : borderId),
15
+ ...axisOrder.filter(axis => !axis.underlay).flatMap(axis => axis.ids)
16
+ ];
17
+ }
@@ -1,3 +1,4 @@
1
+ import { aggregateRows } from "../../grammar/aggregate.js";
1
2
  import { resolveBarCellOpacities } from "./resolve.js";
2
3
  import { readSeriesIdentity } from "../../grammar/pathSeries.js";
3
4
  import {
@@ -25,7 +26,7 @@ import {
25
26
  validateWeightedNumericFields
26
27
  } from "../../grammar/weightedStatistics.js";
27
28
 
28
- export function resolveHistogramInput(dataset, xEncoding) {
29
+ export function resolveHistogramInput(dataset, xEncoding, yEncoding) {
29
30
  const allValues = readQuantitativeField(dataset.values, xEncoding.field);
30
31
  if (xEncoding.weight === undefined) {
31
32
  return {
@@ -33,6 +34,11 @@ export function resolveHistogramInput(dataset, xEncoding) {
33
34
  values: allValues,
34
35
  entries: dataset.values.map((row, index) => ({ row, index })),
35
36
  mass(entries) {
37
+ if (yEncoding?.aggregate !== undefined && yEncoding.aggregate !== "count") {
38
+ // Empty/insufficient samples have no visible segment, just like zero
39
+ // count bins. Use the same native aggregate owner as category bars.
40
+ return aggregateRows(entries.map(entry => entry.row), yEncoding.field, yEncoding.aggregate) ?? 0;
41
+ }
36
42
  return entries.length;
37
43
  }
38
44
  };
@@ -65,7 +71,7 @@ export function deriveHistogramSegments({
65
71
  xScale,
66
72
  resolvedScales
67
73
  }) {
68
- const input = resolveHistogramInput(dataset, xEncoding);
74
+ const input = resolveHistogramInput(dataset, xEncoding, layer.encoding.y);
69
75
  const { rows, values: xValues, entries } = input;
70
76
  const bins = resolveHistogramBins({
71
77
  values: xValues,
@@ -82,7 +82,7 @@ export function requireCompleteBar(program, id) {
82
82
  grain === undefined
83
83
  ) {
84
84
  throw new Error(
85
- `Bar mark "${id}" requires binned x/count y or ordinal x/aggregate y encodings.`
85
+ `Bar mark "${id}" requires binned x/aggregate y or ordinal x/aggregate y encodings.`
86
86
  );
87
87
  }
88
88
  const xScale = findSemanticScale(program, xEncoding.scale);
@@ -103,6 +103,7 @@ export function attachSnapshotObject(program, snapshot, id, parent) {
103
103
  value: itemDefinitions(object)
104
104
  });
105
105
  }
106
+ if (object.clip !== undefined) next = next.editGraphics({target: id, property: "clip", value: object.clip});
106
107
  for (const childId of object.children ?? []) {
107
108
  next = attachSnapshotObject(next, snapshot, childId, id);
108
109
  }
@@ -1,4 +1,4 @@
1
- import { cloneAndFreeze, freezeOwned, isPlainObject } from "../core/immutable.js";
1
+ import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "../core/immutable.js";
2
2
  import { namespaceGraphicId } from "../selectors/graphicCapacity.js";
3
3
  export { namespaceGraphicId } from "../selectors/graphicCapacity.js";
4
4
  import {
@@ -6,6 +6,17 @@ import {
6
6
  walkGraphicTreeEvents
7
7
  } from "../grammar/schemas/graphicTree.js";
8
8
 
9
+ const snapshots = new WeakMap();
10
+
11
+ function placeSnapshot(snapshot, placement) {
12
+ const root = snapshot.order[0], canvas = snapshot.objects[root];
13
+ if (canvas.properties.x === placement.x && canvas.properties.y === placement.y) return snapshot;
14
+ return freezeOwned({ order: snapshot.order, objects: freezeOwned({
15
+ ...snapshot.objects,
16
+ [root]: freezeOwned({ ...canvas, properties: freezeOwned({ ...canvas.properties, ...placement }) })
17
+ }) });
18
+ }
19
+
9
20
  function validateNamespace(namespace) {
10
21
  if (typeof namespace !== "string" || namespace.length === 0) {
11
22
  throw new TypeError("Graphic snapshot namespace must be a non-empty string.");
@@ -84,6 +95,8 @@ export function namespaceGraphicSnapshot(
84
95
  ) {
85
96
  throw new TypeError("Graphic snapshot requires objects and order.");
86
97
  }
98
+ const cached = snapshots.get(graphicSpec)?.get(resolvedNamespace);
99
+ if (cached) return placeSnapshot(cached, placement);
87
100
  walkGraphicTreeEvents(graphicSpec, {});
88
101
  const { id: canvasId, object: canvas } =
89
102
  requireSingleOrderedGraphicByType(graphicSpec, "canvas");
@@ -102,8 +115,15 @@ export function namespaceGraphicSnapshot(
102
115
  ]
103
116
  ));
104
117
  const rootId = namespaceGraphicId(resolvedNamespace, canvasId);
105
- return freezeOwned({
118
+ const snapshot = freezeOwned({
106
119
  objects: freezeOwned(objects),
107
120
  order: freezeOwned([rootId])
108
121
  });
122
+ if (isOwned(graphicSpec)) {
123
+ if (!snapshots.has(graphicSpec)) snapshots.set(graphicSpec, new Map());
124
+ const cache = snapshots.get(graphicSpec);
125
+ cache.set(resolvedNamespace, snapshot);
126
+ if (cache.size > 16) cache.delete(cache.keys().next().value);
127
+ }
128
+ return snapshot;
109
129
  }
@@ -59,7 +59,7 @@ export function findUpstreamTransform(program, dataset, type) {
59
59
  visited.add(current.id);
60
60
  if (current.transform?.length !== 1) return undefined;
61
61
  const transform = current.transform[0];
62
- if (transform.type === type) return transform;
62
+ if (Array.isArray(type) ? type.includes(transform.type) : transform.type === type) return transform;
63
63
  if (findTransformTopology(transform.type)?.provenanceTransparent !== true) {
64
64
  return undefined;
65
65
  }
@@ -1 +1 @@
1
- import{resolveAuthoredFacetLegendLayout as p}from"./ownerLayout.js";import{cartesianAxisOwners as B}from"../../selectors/axes.js";import{PLOT_LEGEND_POSITIONS as F}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as G}from"../../grammar/schemas/graphicBounds.js";import{resolveFacetOuterSides as I}from"../../grammar/facets/guides.js";import{resolveTextBounds as P,textBoundsIntersect as C}from"../../core/textMetrics.js";import{axisGraphicIds as E}from"../guides/resources.js";import{resolveFacetHeaderItems as T}from"../facetHeaders.js";function b(o,t,e){return{left:o.left+t,right:o.right+t,top:o.top+e,bottom:o.bottom+e}}function M(o,t,e,f){const r=o.materializationConfigs.textMetrics,h=I({placements:t.children}),i=[];for(const s of t.children){const m=o.children[s.id];for(const a of B(m))for(const[g,w]of[[2,"labels"],[3,"title"]]){const n=a.config?.[w]?.position;if(o.compositionSpec.facet.guides.axes==="outer"&&!h[n]?.includes(s.id))continue;const u=E(a.channel,a.id)[g];if(m.graphicSpec.objects[u]===void 0)continue;const x=G(m.graphicSpec,u,r);x!==void 0&&i.push(b(x,s.x,s.y))}}const{items:d}=T(t,e,f);for(const s of d)s.properties.text!==""&&i.push(P(s.properties,r));return i}function O(o,t,e,f,r){const h=o.right-o.left,i=o.bottom-o.top,d=e==="right"?t.x+t.width+r:e==="left"?t.x-r-h:f==="left"?t.x:f==="right"?t.x+t.width-h:t.x+(t.width-h)/2,s=["left","right"].includes(e)?t.y:e==="top"?t.y-r-i:t.y+t.height+r;return{left:d,right:d+h,top:s,bottom:s+i}}function q(o,{layout:t,plot:e,plots:f,title:r,preparedHeaders:h,prepared:i}){if(i?.mode==="authored")return p(o,{layout:t,plot:e,title:r,prepared:i,guides:M(o,t,f,h)});if(i?.mode!=="promoted")return{layout:t,plot:e};const d=i.occupiedGroup;if(d?.placement.at!==void 0||F.includes(d?.placement.position))return{layout:t,plot:e};const s=i.reservation.position,m=i.reservation.align,a=i.kinds.map(c=>i.source.guideConfigs.legend[c]),g=i.reservation.gap,w=d===void 0?a.every(c=>c.inferredOffset===!0):d.inferredOffset===!0;let n=O(i.bounds,e,s,m,g);const u=M(o,t,f,h),x=u.map(c=>s==="right"?c.right-(e.x+e.width):s==="left"?e.x-c.left:s==="top"?e.y-c.top:c.bottom-(e.y+e.height));if(w)n=O(i.bounds,e,s,m,Math.max(g,...x.map(c=>c+g)));else if(u.some(c=>C(n,c)))throw new Error("Facet shared legend overlaps retained axes or headers.");const l=Math.max(0,t.padding.left-n.left),v=Math.max(0,r.height+t.padding.top-n.top),L=t.children.map(c=>({...c,x:c.x+l,y:c.y+v})),S={...e,x:e.x+l,y:e.y+v};return n=b(n,l,v),{plot:S,layout:{...t,children:L,width:Math.ceil(Math.max(t.width+l,n.right+t.padding.right)),height:Math.ceil(Math.max(t.height+v,n.bottom+t.padding.bottom)),legend:{position:s,x:n.left,y:n.top,width:n.right-n.left,height:n.bottom-n.top,translation:{x:n.left-i.bounds.left,y:n.top-i.bounds.top}}}}}export{q as resolvePromotedFacetLegendLayout};
1
+ import{resolveAuthoredFacetLegendLayout as G}from"./ownerLayout.js";import{PLOT_LEGEND_POSITIONS as I}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as O}from"../../grammar/schemas/graphicBounds.js";import{resolveTextBounds as P,textBoundsIntersect as y}from"../../core/textMetrics.js";import{resolveFacetHeaderItems as C,retainedFacetGuideIds as E}from"../facetHeaders.js";function w(n,t,e){return{left:n.left+t,right:n.right+t,top:n.top+e,bottom:n.bottom+e}}function M(n,t,e,f){const d=n.materializationConfigs.textMetrics,c=[];for(const r of t.children){const h=n.children[r.id];for(const m of E(n,r.id)){const g=O(h.graphicSpec,m,d);g!==void 0&&c.push(w(g,r.x,r.y))}}const{items:i}=C(t,e,f);for(const r of i)r.properties.text!==""&&c.push(P(r.properties,d));return c}function b(n,t,e,f,d){const c=n.right-n.left,i=n.bottom-n.top,r=e==="right"?t.x+t.width+d:e==="left"?t.x-d-c:f==="left"?t.x:f==="right"?t.x+t.width-c:t.x+(t.width-c)/2,h=["left","right"].includes(e)?t.y:e==="top"?t.y-d-i:t.y+t.height+d;return{left:r,right:r+c,top:h,bottom:h+i}}function k(n,{layout:t,plot:e,plots:f,title:d,preparedHeaders:c,prepared:i}){if(i?.mode==="authored")return G(n,{layout:t,plot:e,title:d,prepared:i,guides:M(n,t,f,c)});if(i?.mode!=="promoted")return{layout:t,plot:e};const r=i.occupiedGroup;if(r?.placement.at!==void 0||I.includes(r?.placement.position))return{layout:t,plot:e};const h=i.reservation.position,m=i.reservation.align,g=i.kinds.map(s=>i.source.guideConfigs.legend[s]),u=i.reservation.gap,l=r===void 0?g.every(s=>s.inferredOffset===!0):r.inferredOffset===!0;let o=b(i.bounds,e,h,m,u);const v=M(n,t,f,c),L=v.map(s=>h==="right"?s.right-(e.x+e.width):h==="left"?e.x-s.left:h==="top"?e.y-s.top:s.bottom-(e.y+e.height));if(l)o=b(i.bounds,e,h,m,Math.max(u,...L.map(s=>s+u)));else if(v.some(s=>y(o,s)))throw new Error("Facet shared legend overlaps retained axes or headers.");const a=Math.max(0,t.padding.left-o.left),x=Math.max(0,d.height+t.padding.top-o.top),B=t.children.map(s=>({...s,x:s.x+a,y:s.y+x})),F={...e,x:e.x+a,y:e.y+x};return o=w(o,a,x),{plot:F,layout:{...t,children:B,width:Math.ceil(Math.max(t.width+a,o.right+t.padding.right)),height:Math.ceil(Math.max(t.height+x,o.bottom+t.padding.bottom)),legend:{position:h,x:o.left,y:o.top,width:o.right-o.left,height:o.bottom-o.top,translation:{x:o.left-i.bounds.left,y:o.top-i.bounds.top}}}}}export{k as resolvePromotedFacetLegendLayout};
@@ -145,15 +145,8 @@ function attachAuthoredParentLegend(program, prepared, layout, synchronizeColorS
145
145
  compositionSpec: { ...spec, facet: { ...spec.facet, guides: { ...spec.facet.guides, legend: prepared.intent } } } });
146
146
  }
147
147
 
148
- export function applyFacetGuideComposition(program, { layout, plot, synchronizeColorSizeMeaning } = {}) {
149
- if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
150
- throw new TypeError("Facet guide composition requires a resolved layout.");
151
- }
152
- if (!isPlainObject(plot)) {
153
- throw new TypeError("Facet guide composition requires resolved plot bounds.");
154
- }
155
- const prepared = prepareSharedFacetLegend(program);
156
- const children = layout.children.map(cell => {
148
+ export function prepareFacetGuideOwnership(program, placements, prepared) {
149
+ const children = placements.map(cell => {
157
150
  const child = program.children[cell.id];
158
151
  return {
159
152
  id: cell.id,
@@ -164,17 +157,36 @@ export function applyFacetGuideComposition(program, { layout, plot, synchronizeC
164
157
  legendKinds: legendKinds(child)
165
158
  };
166
159
  });
167
- const ownership = planFacetGuideOwnership({
168
- placements: layout.children,
160
+ return planFacetGuideOwnership({
161
+ placements,
169
162
  children,
170
163
  axes: program.compositionSpec.facet.guides.axes,
171
164
  legend: program.compositionSpec.facet.guides.legend,
172
165
  ...(prepared === undefined ? {} : { sharedLegends: prepared.compatibility })
173
166
  });
167
+ }
168
+
169
+ export function applyFacetGuideComposition(program, { layout, plot, synchronizeColorSizeMeaning } = {}) {
170
+ if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
171
+ throw new TypeError("Facet guide composition requires a resolved layout.");
172
+ }
173
+ if (!isPlainObject(plot)) {
174
+ throw new TypeError("Facet guide composition requires resolved plot bounds.");
175
+ }
176
+ const prepared = prepareSharedFacetLegend(program);
177
+ const ownership = prepareFacetGuideOwnership(program, layout.children, prepared);
174
178
  let next = program;
175
179
  for (const cell of layout.children) {
176
180
  const namespace = `${program.compositionSpec.id}-${cell.id}`;
177
181
  const childPlan = ownership.children[cell.id];
182
+ // Density guides belong to the gradient owner rather than a color channel.
183
+ // Facets currently support no shared density guide; the parent's legend
184
+ // policy still removes each local copy while retaining hue legends.
185
+ for (const [owner, config] of Object.entries(program.children[cell.id].markConfigs)) {
186
+ if (!config.gradientPlot) continue;
187
+ next = removeNamespacedGraphics(next, namespace,
188
+ ["DensityLegend", "DensityLegendLabels", "DensityLegendTitle"].map(suffix => `${owner}${suffix}`));
189
+ }
178
190
  const axisOwners = cartesianAxisOwners(program.children[cell.id]);
179
191
  for (const axisId of childPlan.removeAxes) {
180
192
  const owner = axisOwners.find(owner => owner.id === axisId);