ggaction 0.0.18 → 0.0.19

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 (155) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +6 -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 +1 -1
  7. package/src/actions/canvas/fitting.js +6 -2
  8. package/src/actions/charts/heatmap.js +4 -3
  9. package/src/actions/data/derived.js +11 -1
  10. package/src/actions/data/edit.js +2 -0
  11. package/src/actions/data/index.js +4 -0
  12. package/src/actions/data/regression.js +24 -16
  13. package/src/actions/data/regressionStatistics.js +73 -0
  14. package/src/actions/data/shared.js +5 -2
  15. package/src/actions/distributions/revision.js +24 -18
  16. package/src/actions/encodings/remove.js +11 -6
  17. package/src/actions/encodings/shared.js +26 -24
  18. package/src/actions/facets/actions.js +18 -7
  19. package/src/actions/facets/guides.js +2 -1
  20. package/src/actions/guides/axes/axes.js +30 -11
  21. package/src/actions/guides/axes/axis.js +58 -8
  22. package/src/actions/guides/axes/components.js +13 -13
  23. package/src/actions/guides/axes/edit.js +36 -24
  24. package/src/actions/guides/axes/labels.js +37 -27
  25. package/src/actions/guides/axes/lines.js +22 -20
  26. package/src/actions/guides/axes/remove.js +10 -6
  27. package/src/actions/guides/axes/tickGroups.js +8 -7
  28. package/src/actions/guides/axes/ticks.js +31 -22
  29. package/src/actions/guides/axes/titles.js +53 -60
  30. package/src/actions/guides/facadeAxes.js +33 -24
  31. package/src/actions/guides/facadeLegend.js +3 -3
  32. package/src/actions/guides/grids/grid.js +1 -1
  33. package/src/actions/guides/grids/resolve.js +48 -51
  34. package/src/actions/guides/legends/blocks.js +43 -19
  35. package/src/actions/guides/legends/categorical/actions.js +30 -7
  36. package/src/actions/guides/legends/categorical/components.js +3 -10
  37. package/src/actions/guides/legends/categorical/index.js +2 -0
  38. package/src/actions/guides/legends/categorical/layout.js +9 -13
  39. package/src/actions/guides/legends/categorical/options.js +8 -2
  40. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  41. package/src/actions/guides/legends/categorical/resolve.js +9 -2
  42. package/src/actions/guides/legends/categorical/symbols.js +22 -8
  43. package/src/actions/guides/legends/colorSize.js +110 -0
  44. package/src/actions/guides/legends/combine.js +37 -0
  45. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
  46. package/src/actions/guides/legends/continuous/common.js +1 -1
  47. package/src/actions/guides/legends/continuous/gradient.js +27 -13
  48. package/src/actions/guides/legends/continuous/opacity.js +16 -11
  49. package/src/actions/guides/legends/creation.js +2 -0
  50. package/src/actions/guides/legends/edit.js +1 -1
  51. package/src/actions/guides/legends/focused.js +6 -5
  52. package/src/actions/guides/legends/lane.js +1 -1
  53. package/src/actions/guides/legends/lifecycle.js +2 -1
  54. package/src/actions/guides/legends/remove.js +6 -3
  55. package/src/actions/guides/legends/size.js +16 -6
  56. package/src/actions/guides/legends/strokeWidth.js +2 -0
  57. package/src/actions/guides/legends/target.js +2 -79
  58. package/src/actions/guides/legends/transition.js +24 -5
  59. package/src/actions/guides/tickValues.js +8 -5
  60. package/src/actions/marks/bar/edit.js +23 -12
  61. package/src/actions/marks/bar/materialize.js +6 -1
  62. package/src/actions/marks/point/materialize.js +5 -1
  63. package/src/actions/marks/remove.js +10 -10
  64. package/src/actions/primitives/createGraphics.js +3 -2
  65. package/src/actions/primitives/editGraphics.js +15 -11
  66. package/src/actions/primitives/graphicProperties.js +1 -6
  67. package/src/actions/primitives/semanticAction.js +27 -1
  68. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  69. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  70. package/src/actions/regression/edit.js +15 -3
  71. package/src/actions/scales/channels.js +2 -2
  72. package/src/actions/scales/create.js +3 -3
  73. package/src/actions/scales/definitions.js +1 -1
  74. package/src/actions/scales/edit.js +1 -1
  75. package/src/actions/scales/editPolicy.js +3 -0
  76. package/src/actions/scales/patch.js +1 -1
  77. package/src/actions/scales/quantitativeColor.js +8 -3
  78. package/src/actions/theme/reconcile.js +50 -23
  79. package/src/core/compositionState.js +2 -2
  80. package/src/core/graphicProperties.js +1 -0
  81. package/src/core/programState.js +4 -0
  82. package/src/core/resourceReferences.js +2 -2
  83. package/src/core/validation.js +9 -0
  84. package/src/core/vocabulary.js +3 -0
  85. package/src/grammar/axisTextPlacement.js +1 -0
  86. package/src/grammar/colorSizeLegend.js +39 -0
  87. package/src/grammar/computed.js +23 -1
  88. package/src/grammar/datasetSchema.js +10 -2
  89. package/src/grammar/displayLabels.js +8 -0
  90. package/src/grammar/facets/guides.js +54 -31
  91. package/src/grammar/facets/legendIntent.js +65 -0
  92. package/src/grammar/guideDash.js +1 -0
  93. package/src/grammar/legendBlocks.js +1 -0
  94. package/src/grammar/legendBorder.js +9 -0
  95. package/src/grammar/legendClip.js +19 -0
  96. package/src/grammar/legendCombinations.js +45 -0
  97. package/src/grammar/legendGradient.js +9 -0
  98. package/src/grammar/legendGroups.js +1 -1
  99. package/src/grammar/legendOpacity.js +6 -0
  100. package/src/grammar/pointShapes.js +16 -2
  101. package/src/grammar/regression/derive.js +9 -1
  102. package/src/grammar/regression/models.js +60 -0
  103. package/src/grammar/regression/parameters.js +9 -1
  104. package/src/grammar/regression/statistics.js +64 -0
  105. package/src/grammar/scales/color.js +1 -1
  106. package/src/grammar/scales/colorConsumers.js +1 -1
  107. package/src/grammar/scales/definition.js +1 -1
  108. package/src/grammar/scales/types.js +1 -1
  109. package/src/grammar/schemas/concreteGraphic.js +3 -0
  110. package/src/grammar/schemas/graphic.js +3 -0
  111. package/src/grammar/schemas/graphicBounds.js +31 -7
  112. package/src/grammar/schemas/semanticPath.js +17 -2
  113. package/src/grammar/summary.js +1 -1
  114. package/src/grammar/timeUnit.js +6 -4
  115. package/src/grammar/transformTopology.js +1 -0
  116. package/src/grammar/transforms.js +11 -1
  117. package/src/inspectionDescriptors.js +1 -1
  118. package/src/layout/guideCollisions.js +9 -1
  119. package/src/layout/legendGroup.js +1 -1
  120. package/src/materialization/compositionSnapshot.js +2 -7
  121. package/src/materialization/coordinateBounds.js +17 -4
  122. package/src/materialization/facetGuides/edit.js +30 -0
  123. package/src/materialization/facetGuides/layout.js +1 -1
  124. package/src/materialization/facetGuides/owner.js +125 -0
  125. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  126. package/src/materialization/facetGuides/placement.js +52 -6
  127. package/src/materialization/facetGuides/preparation.js +7 -0
  128. package/src/materialization/facets.js +1 -1
  129. package/src/materialization/graphicHierarchy.js +5 -1
  130. package/src/materialization/graphicTranslation.js +73 -0
  131. package/src/materialization/guides/layout.js +1 -1
  132. package/src/materialization/guides/resources.js +1 -1
  133. package/src/materialization/legendBorder.js +28 -0
  134. package/src/materialization/legendClip.js +76 -0
  135. package/src/materialization/legendGroups.js +1 -1
  136. package/src/materialization/legends.js +1 -1
  137. package/src/materialization/scaleGuideDependencies.js +13 -14
  138. package/src/materialization/scales/map.js +3 -0
  139. package/src/materialization/scales/resolve.js +2 -0
  140. package/src/materialization/typography.js +10 -2
  141. package/src/persistence/validation.js +174 -1
  142. package/src/persistence.js +1 -1
  143. package/src/renderers/canvas/index.js +20 -10
  144. package/src/renderers/canvas/isolated.js +166 -0
  145. package/src/renderers/canvas/limits.js +3 -0
  146. package/src/renderers/canvas/rect.js +1 -1
  147. package/src/renderers/pdf.js +2 -2
  148. package/src/renderers/png.js +2 -2
  149. package/src/renderers/svg.js +10 -3
  150. package/src/selectors/axes.js +1 -0
  151. package/src/selectors/graphicCapacity.js +38 -0
  152. package/src/version.js +1 -1
  153. package/types/basic.d.ts +2 -0
  154. package/types/index.d.ts +5 -0
  155. package/types/program.d.ts +206 -61
@@ -2,7 +2,15 @@ export function assertGuideCollisionBlocks(blocks) {
2
2
  for (let index = 0; index < blocks.length; index += 1) {
3
3
  const a = blocks[index];
4
4
  for (const b of blocks.slice(index + 1)) {
5
- if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
5
+ if (a.kind === "axis" && b.kind === "axis") {
6
+ if (!(a.namedAxis || b.namedAxis) || a.axisId === b.axisId || !a.textBounds || !b.textBounds) continue;
7
+ if (a.textBounds.some(left => left && b.textBounds.some(right => right &&
8
+ left.left < right.right && left.right > right.left && left.top < right.bottom && left.bottom > right.top))) {
9
+ throw new Error(`The ${a.id} and ${b.id} text overlap; independent axes require separate text bounds.`);
10
+ }
11
+ continue;
12
+ }
13
+ if (a.position !== b.position) continue;
6
14
  if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
7
15
  a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
8
16
  throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
@@ -1 +1 @@
1
- import{PLOT_LEGEND_POSITIONS as I}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as M,HORIZONTAL_LEGEND_BLOCK_GAP as N}from"./legendLane.js";function H({blocks:g,group:O,plot:e,canvas:L,allowOverflow:l=!1}){if(g.length===0)return[];const{placement:t,blockLayout:f}=O,u=I.includes(t.position),c=["left","right"].includes(t.position),x=f==="column"||f==="wrap"&&c,y=u||c||t.at!==void 0?M:N,_=e.width-(u?t.offset*2:0);let h=0,p=0,w=0;const d=g.map(i=>{const o=i.bounds.right-i.bounds.left,E=i.bounds.bottom-i.bounds.top;if(f==="wrap"&&!x&&!u&&o>_&&!l)throw new Error("Horizontal legend block requires more plot width.");!x&&f==="wrap"&&h>0&&h+o>_&&(h=0,p+=w+y,w=0);const G={id:i.id,x:h,y:p,width:o,height:E};return x?p+=E+y:(h+=o+y,w=Math.max(w,E)),G}),r=Math.max(...d.map(i=>i.x+i.width)),a=Math.max(...d.map(i=>i.y+i.height));if(f==="wrap"&&t.position==="top"){const i=new Map;for(const o of d)i.set(o.y,Math.max(i.get(o.y)??0,o.height));for(const o of d)o.y=a-o.y-i.get(o.y)}let n,s;if(t.at!==void 0)n=e.x+t.at.x,s=e.y+t.at.y;else if(u){if(r+2*t.offset>e.width||a+2*t.offset>e.height)throw new Error("In-plot legend group does not fit the plot inset.");n=t.position.endsWith("left")?e.x+t.offset:e.x+e.width-t.offset-r,s=t.position.startsWith("top")?e.y+t.offset:e.y+e.height-t.offset-a}else c?(n=t.position==="right"?e.x+e.width+t.offset:e.x-t.offset-r,s=e.y):(n=t.align==="left"?e.x:t.align==="right"?e.x+e.width-r:e.x+(e.width-r)/2,s=t.position==="top"?e.y-t.offset-a:e.y+e.height+t.offset);const m={left:n,top:s,right:n+r,bottom:s+a};if(m.left<0||m.top<0||m.right>L.width||m.bottom>L.height)throw new Error("Legend group requires more Canvas space.");return d.map((i,o)=>({id:i.id,dx:n+i.x-g[o].bounds.left,dy:s+i.y-g[o].bounds.top}))}export{H as resolveLegendGroupPlacement};
1
+ 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,4 +1,6 @@
1
1
  import { cloneAndFreeze, freezeOwned, isPlainObject } from "../core/immutable.js";
2
+ import { namespaceGraphicId } from "../selectors/graphicCapacity.js";
3
+ export { namespaceGraphicId } from "../selectors/graphicCapacity.js";
2
4
  import {
3
5
  requireSingleOrderedGraphicByType,
4
6
  walkGraphicTreeEvents
@@ -28,13 +30,6 @@ function validateCanvas(canvas, id) {
28
30
  }
29
31
  }
30
32
 
31
- export function namespaceGraphicId(namespace, id) {
32
- const encode = value => Array.from(value, character =>
33
- character.codePointAt(0).toString(16).padStart(6, "0")
34
- ).join("");
35
- return `g${encode(namespace)}_${id}`;
36
- }
37
-
38
33
  function rewriteItem(item, namespace) {
39
34
  if (!isPlainObject(item) || typeof item.id !== "string" || item.id.length === 0) {
40
35
  throw new TypeError("Graphic snapshot items require non-empty string ids.");
@@ -1,3 +1,4 @@
1
+ import { cartesianAxisOwners } from "../selectors/axes.js";
1
2
  import { isSourceOwnedText } from "../grammar/text.js";
2
3
  import { resolvePolarFrame } from "../grammar/polar.js";
3
4
  import { resolveEffectiveBounds } from "../layout/aspect.js";
@@ -203,10 +204,22 @@ export function resolveGuideCoordinateBounds(program, {
203
204
  return resolveCoordinateBounds(program, target, { resolvedScales });
204
205
  }
205
206
 
207
+ // Axis consumers use a displaced frame; marks and grids retain the plot.
208
+ export function resolveCartesianAxisFrameBounds(program, channel, position, scale, axisId = channel) {
209
+ const plot = resolveGuideCoordinateBounds(program, {
210
+ coordinate: program.semanticSpec.guides.axis?.[axisId]?.coordinate, scale, channel
211
+ });
212
+ if (plot === undefined) return undefined;
213
+ const offset = program.guideConfigs.axis?.[axisId]?.frame?.offset ?? 0;
214
+ if (offset === 0) return plot;
215
+ const shift = ["bottom", "right"].includes(position) ? offset : -offset;
216
+ return channel === "x" ? { ...plot, y: plot.y + shift } : { ...plot, x: plot.x + shift };
217
+ }
218
+
206
219
  // Cartesian guide bindings are semantic scale consumers without data rows.
207
220
  export function axisScaleBindings(program, id) {
208
- return Object.entries(program.semanticSpec.guides.axis ?? {}).flatMap(([channel, guide]) =>
209
- ["x", "y"].includes(channel) && guide.scale === id && guide.coordinate !== undefined &&
210
- program.materializationConfigs?.guides?.axis?.[channel]?.binding?.standalone === true
211
- ? [{ channel, coordinate: guide.coordinate }] : []);
221
+ return cartesianAxisOwners(program).flatMap(owner =>
222
+ owner.scale === id && owner.coordinate !== undefined &&
223
+ owner.config?.binding?.standalone === true
224
+ ? [{ channel: owner.channel, coordinate: owner.coordinate }] : []);
212
225
  }
@@ -0,0 +1,30 @@
1
+ import { prepareSharedFacetLegend } from "./preparation.js";
2
+ import { facetLegendIntent, intrinsicFacetLegendSource, restoreFacetLegendIntent,
3
+ prepareAuthoredFacetLegend } from "./owner.js";
4
+
5
+ export function editFacetParentLegend(program, operation, args) {
6
+ if (program.compositionSpec?.type !== "facet") {
7
+ throw new Error(`${operation} supports only a unit chart or a facet parent shared legend.`);
8
+ }
9
+ const current = program.compositionSpec.facet.guides.legend;
10
+ if (current === false) throw new Error(`${operation} requires an existing facet parent legend.`);
11
+ const prepared = current === "shared" ? prepareSharedFacetLegend(program) : prepareAuthoredFacetLegend(program);
12
+ let source;
13
+ if (prepared.source !== undefined) {
14
+ source = restoreFacetLegendIntent(intrinsicFacetLegendSource(program), facetLegendIntent(prepared.source));
15
+ } else {
16
+ const entries = prepared.compatibility.entries;
17
+ if (entries.length !== 1) throw new Error("Facet parent inferred legend requires an unambiguous native owner.");
18
+ const config = entries[0].config;
19
+ source = intrinsicFacetLegendSource(program).createLegend({ target: config.target, channels: ["color"],
20
+ itemGap: 26, titleStyle: { fontSize: 12, fontWeight: 700 },
21
+ ...(prepared.legend.mark === "point" ? { symbol: { layers: [{ type: "point", size: 5.5, stroke: "#ffffff", strokeWidth: 0.35 }] } } : {}) });
22
+ }
23
+ source = source[operation](args);
24
+ const intent = Object.keys(source.guideConfigs.legend ?? {}).length === 0 ? false : facetLegendIntent(source);
25
+ const spec = program.compositionSpec;
26
+ return program._clone({ trace: source.trace, actionStack: source.actionStack, actionSequence: source._actionSequence })
27
+ ._withCompositionState({ children: program.children,
28
+ compositionSpec: { ...spec, facet: { ...spec.facet, guides: { ...spec.facet.guides, legend: intent } } } })
29
+ .materializeComposition();
30
+ }
@@ -1 +1 @@
1
- import{PLOT_LEGEND_POSITIONS as B}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as G}from"../../grammar/schemas/graphicBounds.js";import{resolveFacetOuterAxes as I}from"../../grammar/facets/guides.js";import{resolveTextBounds as L,textBoundsIntersect as S}from"../../core/textMetrics.js";import{axisGraphicIds as y}from"../guides/resources.js";import{resolveFacetHeaderItems as F}from"../facetHeaders.js";function p(s,t,e){return{left:s.left+t,right:s.right+t,top:s.top+e,bottom:s.bottom+e}}function P(s,t,e,m){const c=s.materializationConfigs.textMetrics,d=I({placements:t.children}),i=[];for(const n of t.children){const a=s.children[n.id];for(const g of["x","y"])if(!(s.compositionSpec.facet.guides.axes==="outer"&&!d.children[n.id][g]))for(const f of y(g)){if(!/Labels|Title/.test(f)||a.graphicSpec.objects[f]===void 0)continue;const x=G(a.graphicSpec,f,c);x!==void 0&&i.push(p(x,n.x,n.y))}}const{items:h}=F(t,e,m);for(const n of h)n.properties.text!==""&&i.push(L(n.properties,c));return i}function w(s,t,e,m,c){const d=s.right-s.left,i=s.bottom-s.top,h=e==="right"?t.x+t.width+c:e==="left"?t.x-c-d:m==="left"?t.x:m==="right"?t.x+t.width-d:t.x+(t.width-d)/2,n=["left","right"].includes(e)?t.y:e==="top"?t.y-c-i:t.y+t.height+c;return{left:h,right:h+d,top:n,bottom:n+i}}function j(s,{layout:t,plot:e,plots:m,title:c,preparedHeaders:d,prepared:i}){if(i?.mode!=="promoted")return{layout:t,plot:e};const h=i.occupiedGroup;if(h?.placement.at!==void 0||B.includes(h?.placement.position))return{layout:t,plot:e};const n=i.reservation.position,a=i.reservation.align,g=i.kinds.map(r=>i.source.guideConfigs.legend[r]),f=i.reservation.gap,x=h===void 0?g.every(r=>r.inferredOffset===!0):h.inferredOffset===!0;let o=w(i.bounds,e,n,a,f);const v=P(s,t,m,d),b=v.map(r=>n==="right"?r.right-(e.x+e.width):n==="left"?e.x-r.left:n==="top"?e.y-r.top:r.bottom-(e.y+e.height));if(x)o=w(i.bounds,e,n,a,Math.max(f,...b.map(r=>r+f)));else if(v.some(r=>S(o,r)))throw new Error("Facet shared legend overlaps retained axes or headers.");const u=Math.max(0,t.padding.left-o.left),l=Math.max(0,c.height+t.padding.top-o.top),M=t.children.map(r=>({...r,x:r.x+u,y:r.y+l})),O={...e,x:e.x+u,y:e.y+l};return o=p(o,u,l),{plot:O,layout:{...t,children:M,width:Math.ceil(Math.max(t.width+u,o.right+t.padding.right)),height:Math.ceil(Math.max(t.height+l,o.bottom+t.padding.bottom)),legend:{position:n,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{j as resolvePromotedFacetLegendLayout};
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};
@@ -0,0 +1,125 @@
1
+ import { findLayer } from "../../selectors/layers.js";
2
+ import { isAuthoredFacetLegend } from "../../grammar/facets/legendIntent.js";
3
+ export { isAuthoredFacetLegend };
4
+ import { freezeOwned } from "../../core/immutable.js";
5
+ import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
6
+ import { describeLegendBlock } from "../../grammar/legendBlocks.js";
7
+ import { legendResourcePolicy, allLegendGraphicIds } from "../guides/resources.js";
8
+ import { resolveSharedFacetLegends, sameFacetGuideValue } from "../../grammar/facets/guides.js";
9
+
10
+ export function facetLegendIntent(source) {
11
+ return freezeOwned({ type: "authored-legend", blocks: Object.fromEntries(
12
+ Object.entries(source.guideConfigs.legend ?? {}).map(([kind, config]) => [kind, {
13
+ config,
14
+ ...(source.semanticSpec.guides.legend?.[legendResourcePolicy(kind).semanticKind] === undefined ? {} : {
15
+ guide: source.semanticSpec.guides.legend[legendResourcePolicy(kind).semanticKind]
16
+ })
17
+ }])
18
+ ), groups: source.guideConfigs.legendGroups ?? {} });
19
+ }
20
+
21
+ export function intrinsicFacetLegendSource(program) {
22
+ const first = program.children[program.compositionSpec.children[0]];
23
+ let source = new program.constructor({
24
+ semanticSpec: first.semanticSpec, graphicSpec: first.graphicSpec,
25
+ resolvedScales: first.resolvedScales, materializationConfigs: first.materializationConfigs,
26
+ context: { ...first.context, deferGuideLayoutValidation: true, facetLegendIntrinsic: true },
27
+ trace: program.trace, actionStack: program.actionStack, actionSequence: program._actionSequence
28
+ });
29
+ for (const id of allLegendGraphicIds(Object.keys(source.guideConfigs.legend ?? {}))) {
30
+ if (source.graphicSpec.objects[id] !== undefined) source = source.editGraphics({ target: id, remove: true });
31
+ }
32
+ for (const key of Object.keys(source.semanticSpec.guides.legend ?? {})) {
33
+ source = source.editSemantic({ property: `guide.legend.${key}`, remove: true });
34
+ }
35
+ return source._withoutMaterializationConfig(["guides", "legend"])
36
+ ._withoutMaterializationConfig(["guides", "legendGroups"]);
37
+ }
38
+
39
+ export function restoreFacetLegendIntent(source, intent) {
40
+ const scaffolds = [];
41
+ for (const [kind, { config }] of Object.entries(intent.blocks)) {
42
+ const descriptor = describeLegendBlock(source, kind, config);
43
+ const entry = { target: config.target, channels: [...descriptor.channels] };
44
+ if (descriptor.channels.includes("shape") && descriptor.channels.includes("size")) entry.combine = "size-shape";
45
+ if (kind === "colorSize") entry.combine = "color-size";
46
+ if (descriptor.family === "categorical" && config.inferredSymbol === false && entry.combine === undefined) entry.symbol = config.symbol;
47
+ if (kind === "gradient" && config.representation === "symbols") entry.representation = "symbols";
48
+ scaffolds.push(entry);
49
+ }
50
+ // Build unclipped resources first; each authored block restores its own window below.
51
+ // A global scaffold option would also reach gradient strips or overwrite sibling windows.
52
+ for (const args of scaffolds) source = source.createLegend(args);
53
+ for (const [kind, { config, guide }] of Object.entries(intent.blocks)) {
54
+ source = source._withLegendConfig(kind, config);
55
+ const background = legendResourcePolicy(kind).graphicIds.find(id => id.endsWith("Background"));
56
+ if (config.border === false && source.graphicSpec.objects[background] !== undefined) {
57
+ source = source.editGraphics({ target: background, remove: true });
58
+ } else if (config.border !== false && background !== undefined && source.graphicSpec.objects[background] === undefined) {
59
+ const roots = new Set(legendResourcePolicy(kind).graphicIds);
60
+ const before = source.graphicSpec.objects.canvas.children.find(id => roots.has(id));
61
+ source = source.createGraphics({ id: background, type: "rect", parent: "canvas", ...(before === undefined ? {} : { before }) });
62
+ }
63
+ if (guide !== undefined) {
64
+ for (const [property, value] of Object.entries(guide)) {
65
+ source = source.editSemantic({ property: `guide.legend.${legendResourcePolicy(kind).semanticKind}.${property}`, value });
66
+ }
67
+ }
68
+ }
69
+ source = source._withMaterializationConfig(["guides", "legendGroups"], intent.groups);
70
+ return source.rematerializeLegend();
71
+ }
72
+
73
+ export function prepareAuthoredFacetLegend(program, intent = program.compositionSpec.facet.guides.legend) {
74
+ let source = intrinsicFacetLegendSource(program);
75
+ source = isAuthoredFacetLegend(intent) ? restoreFacetLegendIntent(source, intent) : source.createLegend(intent);
76
+ const kinds = Object.keys(source.guideConfigs.legend ?? {});
77
+ for (const [kind, config] of Object.entries(source.guideConfigs.legend)) {
78
+ const descriptor = describeLegendBlock(source, kind, config);
79
+ const owner = findLayer(source, config.target);
80
+ for (const channel of descriptor.channels) {
81
+ if (program.compositionSpec.facet.scales[channel] === "independent") {
82
+ throw new Error("Facet parent legend requires shared encoded scales.");
83
+ }
84
+ }
85
+ for (const child of Object.values(program.children)) {
86
+ const candidate = findLayer(child, config.target);
87
+ if (candidate === undefined || candidate.mark.type !== owner.mark.type ||
88
+ descriptor.channels.some(channel => !sameFacetGuideValue(candidate.encoding?.[channel], owner.encoding?.[channel])) ||
89
+ !sameFacetGuideValue(child.markConfigs[config.target], source.markConfigs[config.target])) {
90
+ throw new Error("Facet parent legend requires compatible source mark bindings and recipes in every child.");
91
+ }
92
+ }
93
+ }
94
+ const compatibility = resolveSharedFacetLegends(program.compositionSpec.children.map(id => ({
95
+ id, guideConfigs: source.guideConfigs, resolvedScales: program.children[id].resolvedScales
96
+ })));
97
+ const blocks = kinds.map(kind => {
98
+ const ids = new Set(allLegendGraphicIds([kind]));
99
+ const roots = source.graphicSpec.objects.canvas.children.filter(id => ids.has(id));
100
+ const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots, source.materializationConfigs.textMetrics);
101
+ if (bounds === undefined) throw new Error("Facet parent legend requires measurable block bounds.");
102
+ return { kind, roots, bounds, config: source.guideConfigs.legend[kind] };
103
+ });
104
+ return { mode: "authored", source, kinds, blocks, compatibility,
105
+ intent: facetLegendIntent(source),
106
+ roots: blocks.flatMap(block => block.roots),
107
+ bounds: unionConcreteGraphicBounds(source.graphicSpec, blocks.flatMap(block => block.roots), source.materializationConfigs.textMetrics),
108
+ reservation: { position: "right", align: "center", gap: 0, width: 0, height: 0 } };
109
+ }
110
+
111
+ // Theme reconciliation changes the derived native appearance in one transaction.
112
+ // Adopt only that transaction's configs; ordinary layout/data replay reads intent.
113
+ export function synchronizeFacetLegendAppearance(program) {
114
+ const intent = program.compositionSpec?.type === "facet" ? program.compositionSpec.facet.guides.legend : undefined;
115
+ if (!isAuthoredFacetLegend(intent)) return program;
116
+ const blocks = Object.fromEntries(Object.entries(intent.blocks).map(([kind, block]) => {
117
+ const config = program.guideConfigs.legend?.[kind];
118
+ if (config === undefined || config.target !== block.config.target) throw new Error("Parent theme reconciliation lost its legend owner.");
119
+ return [kind, { ...block, config }];
120
+ }));
121
+ const spec = program.compositionSpec;
122
+ return program._withCompositionState({ children: program.children,
123
+ compositionSpec: { ...spec, facet: { ...spec.facet,
124
+ guides: { ...spec.facet.guides, legend: { ...intent, blocks, groups: program.guideConfigs.legendGroups ?? {} } } } } });
125
+ }
@@ -0,0 +1,71 @@
1
+ import { localLegendPlacement } from "../../grammar/legendGroups.js";
2
+ import { PLOT_LEGEND_POSITIONS } from "../../core/vocabulary.js";
3
+ import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
4
+ import { textBoundsIntersect } from "../../core/textMetrics.js";
5
+
6
+ const shift = (bounds, x, y) => ({ left: bounds.left + x, right: bounds.right + x,
7
+ top: bounds.top + y, bottom: bounds.bottom + y });
8
+
9
+ function nativeGroup(blocks) {
10
+ const config = blocks[0].config;
11
+ const positions = new Set(blocks.map(block => block.config.position));
12
+ if (positions.size > 1) throw new Error("Parent legend inherited blocks require one common anchor.");
13
+ const placement = { position: config.position ?? "right", align: config.align ?? "center",
14
+ offset: Math.max(...blocks.map(block => block.config.offset ?? 0)) };
15
+ return { placement, blockLayout: "wrap", inferredOffset: blocks.every(block => block.config.inferredOffset === true) };
16
+ }
17
+
18
+ export function resolveAuthoredFacetLegendLayout(program, { layout, plot, title, prepared, guides }) {
19
+ const partitions = [];
20
+ const targets = [...new Set(prepared.blocks.map(block => block.config.target))];
21
+ for (const target of targets) {
22
+ const owned = prepared.blocks.filter(block => block.config.target === target);
23
+ const inherited = owned.filter(block => localLegendPlacement(block.config) === undefined);
24
+ if (inherited.length) partitions.push({ blocks: inherited,
25
+ group: prepared.source.guideConfigs.legendGroups?.[target] ?? nativeGroup(inherited) });
26
+ for (const block of owned) {
27
+ const placement = localLegendPlacement(block.config);
28
+ if (placement !== undefined) partitions.push({ blocks: [block],
29
+ group: { placement, blockLayout: "column", inferredOffset: placement.inferredOffset } });
30
+ }
31
+ }
32
+ const placements = [];
33
+ for (const { blocks, group } of partitions) {
34
+ const position = group.placement.position;
35
+ const inside = group.placement.at !== undefined || PLOT_LEGEND_POSITIONS.includes(position);
36
+ let effective = group;
37
+ if (!inside && group.inferredOffset === true) {
38
+ const base = group.placement.offset;
39
+ const distances = guides.map(bound => position === "right" ? bound.right - plot.x - plot.width
40
+ : position === "left" ? plot.x - bound.left : position === "top" ? plot.y - bound.top
41
+ : bound.bottom - plot.y - plot.height);
42
+ effective = { ...group, placement: { ...group.placement, offset: Math.max(base, ...distances.map(value => value + base)) } };
43
+ }
44
+ const plans = resolveLegendGroupPlacement({ blocks: blocks.map(block => ({ id: block.kind, bounds: block.bounds })),
45
+ group: effective, plot, canvas: layout, deferCanvasFit: true });
46
+ for (const plan of plans) {
47
+ const block = blocks.find(block => block.kind === plan.id);
48
+ const bounds = shift(block.bounds, plan.dx, plan.dy);
49
+ if (inside && (bounds.left < plot.x || bounds.right > plot.x + plot.width ||
50
+ bounds.top < plot.y || bounds.bottom > plot.y + plot.height)) {
51
+ throw new Error("Facet parent legend does not fit the final plot union.");
52
+ }
53
+ if (guides.some(bound => textBoundsIntersect(bounds, bound))) {
54
+ throw new Error("Facet parent legend overlaps retained axes or headers.");
55
+ }
56
+ if (placements.some(other => textBoundsIntersect(bounds, other.bounds))) {
57
+ throw new Error("Facet parent legend blocks overlap.");
58
+ }
59
+ placements.push({ kind: block.kind, dx: plan.dx, dy: plan.dy, bounds, inside });
60
+ }
61
+ }
62
+ const dx = Math.max(0, ...placements.map(item => layout.padding.left - item.bounds.left));
63
+ const dy = Math.max(0, ...placements.map(item => title.height + layout.padding.top - item.bounds.top));
64
+ const moved = placements.map(item => ({ ...item, dx: item.dx + dx, dy: item.dy + dy,
65
+ bounds: shift(item.bounds, dx, dy) }));
66
+ return { plot: { ...plot, x: plot.x + dx, y: plot.y + dy },
67
+ layout: { ...layout, children: layout.children.map(cell => ({ ...cell, x: cell.x + dx, y: cell.y + dy })),
68
+ width: Math.ceil(Math.max(layout.width + dx, ...moved.map(item => item.bounds.right + layout.padding.right))),
69
+ height: Math.ceil(Math.max(layout.height + dy, ...moved.map(item => item.bounds.bottom + layout.padding.bottom))),
70
+ parentLegendPlacements: moved } };
71
+ }
@@ -1,3 +1,5 @@
1
+ import { translateGraphic } from "../graphicTranslation.js";
2
+ import { cartesianAxisOwners } from "../../selectors/axes.js";
1
3
  import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
2
4
  import { isPlainObject } from "../../core/immutable.js";
3
5
  import { resolveConcreteGraphicBounds } from
@@ -118,7 +120,32 @@ function assertSharedLegendFits(program, id, layout, plot, occupiedGroup) {
118
120
  return program;
119
121
  }
120
122
 
121
- export function applyFacetGuideComposition(program, { layout, plot } = {}) {
123
+ function attachAuthoredParentLegend(program, prepared, layout, synchronizeColorSizeMeaning) {
124
+ const meaning = prepared.source.semanticSpec.guides.legend?.colorSize;
125
+ if (meaning !== undefined || program.semanticSpec.guides.legend?.colorSize !== undefined) {
126
+ program = synchronizeColorSizeMeaning(program, meaning);
127
+ }
128
+ const id = `${program.compositionSpec.id}-shared-legend`;
129
+ let next = program._withoutMaterializationConfig(["guides", "legend"])
130
+ ._withoutMaterializationConfig(["guides", "legendGroups"]);
131
+ next = copyLegendConfig(next, prepared.source).createGraphics({ id, type: "canvas", parent: "canvas" });
132
+ for (const [property, value] of Object.entries({ x: 0, y: 0, width: layout.width, height: layout.height, background: "transparent" })) {
133
+ next = next.editGraphics({ target: id, property, value });
134
+ }
135
+ for (const block of prepared.blocks) {
136
+ const placement = layout.parentLegendPlacements.find(item => item.kind === block.kind);
137
+ for (const root of block.roots) {
138
+ next = attachSnapshotObject(next, prepared.source.graphicSpec, root, id);
139
+ next = translateGraphic(next, root, placement.dx, placement.dy);
140
+ }
141
+ }
142
+ if (prepared.keepShared === true) return next;
143
+ const spec = next.compositionSpec;
144
+ return next._withCompositionState({ children: next.children,
145
+ compositionSpec: { ...spec, facet: { ...spec.facet, guides: { ...spec.facet.guides, legend: prepared.intent } } } });
146
+ }
147
+
148
+ export function applyFacetGuideComposition(program, { layout, plot, synchronizeColorSizeMeaning } = {}) {
122
149
  if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
123
150
  throw new TypeError("Facet guide composition requires a resolved layout.");
124
151
  }
@@ -130,8 +157,10 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
130
157
  const child = program.children[cell.id];
131
158
  return {
132
159
  id: cell.id,
133
- axes: Object.keys(child.guideConfigs.axis ?? {})
134
- .filter(channel => ["x", "y"].includes(channel)),
160
+ axes: cartesianAxisOwners(child).map(owner => owner.id),
161
+ axisOwners: cartesianAxisOwners(child).map(owner => ({ id: owner.id, channel: owner.channel,
162
+ components: Object.fromEntries(["line", "ticks", "labels", "title"].flatMap(component =>
163
+ owner.config?.[component] === undefined ? [] : [[component, owner.config[component].position]])) })),
135
164
  legendKinds: legendKinds(child)
136
165
  };
137
166
  });
@@ -146,8 +175,15 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
146
175
  for (const cell of layout.children) {
147
176
  const namespace = `${program.compositionSpec.id}-${cell.id}`;
148
177
  const childPlan = ownership.children[cell.id];
149
- for (const channel of childPlan.removeAxes) {
150
- next = removeNamespacedGraphics(next, namespace, axisGraphicIds(channel));
178
+ const axisOwners = cartesianAxisOwners(program.children[cell.id]);
179
+ for (const axisId of childPlan.removeAxes) {
180
+ const owner = axisOwners.find(owner => owner.id === axisId);
181
+ next = removeNamespacedGraphics(next, namespace, axisGraphicIds(owner.channel, axisId));
182
+ }
183
+ for (const [axisId, components] of Object.entries(childPlan.removeAxisComponents ?? {})) {
184
+ const owner = axisOwners.find(owner => owner.id === axisId);
185
+ const ids = axisGraphicIds(owner.channel, axisId);
186
+ next = removeNamespacedGraphics(next, namespace, components.map(component => ids[["line", "ticks", "labels", "title"].indexOf(component)]));
151
187
  }
152
188
  if (childPlan.removeLegends.length > 0) {
153
189
  next = removeNamespacedGraphics(
@@ -157,7 +193,17 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
157
193
  );
158
194
  }
159
195
  }
160
- if (prepared === undefined) return next;
196
+ if (prepared === undefined) {
197
+ if (next.semanticSpec.guides.legend?.colorSize !== undefined) {
198
+ next = synchronizeColorSizeMeaning(next, undefined);
199
+ }
200
+ return next._withoutMaterializationConfig(["guides", "legend"])
201
+ ._withoutMaterializationConfig(["guides", "legendGroups"]);
202
+ }
203
+ if (prepared.mode === "authored") {
204
+ next = attachAuthoredParentLegend(next, prepared, layout, synchronizeColorSizeMeaning);
205
+ return assertSharedLegendFits(next, `${program.compositionSpec.id}-shared-legend`, layout, plot, {});
206
+ }
161
207
  let id;
162
208
  if (prepared.mode === "legacyCategorical") {
163
209
  id = `${program.compositionSpec.id}-legend`;
@@ -1,3 +1,6 @@
1
+ import { localLegendPosition } from "../../grammar/legendGroups.js";
2
+ import { prepareAuthoredFacetLegend, facetLegendIntent } from "./owner.js";
3
+ import { isPlainObject } from "../../core/immutable.js";
1
4
  import { PLOT_LEGEND_POSITIONS } from "../../layout/legend.js";
2
5
  import { isContinuousColorScaleType } from "../../grammar/scales/types.js";
3
6
  import {
@@ -178,6 +181,7 @@ function promotedLegendPlacement(source, kinds) {
178
181
  }
179
182
 
180
183
  export function prepareSharedFacetLegend(program) {
184
+ if (isPlainObject(program.compositionSpec.facet.guides.legend)) return prepareAuthoredFacetLegend(program);
181
185
  if (program.compositionSpec.facet.guides.legend !== "shared") {
182
186
  return undefined;
183
187
  }
@@ -190,6 +194,9 @@ export function prepareSharedFacetLegend(program) {
190
194
  }
191
195
  const source = inferred ? prepareAutoLegendSource(first) : first;
192
196
  const kinds = legendKinds(source);
197
+ if (kinds.some(kind => localLegendPosition(source.guideConfigs.legend[kind]) !== undefined)) {
198
+ return { ...prepareAuthoredFacetLegend(program, facetLegendIntent(source)), keepShared: true };
199
+ }
193
200
  const compatibility = resolveSharedFacetLegends(
194
201
  compatibleLegendChildren(program, source, inferred)
195
202
  );
@@ -1 +1 @@
1
- import{resolveConcreteGraphicBounds as C}from"../grammar/schemas/graphicBounds.js";import{textBoundsFitCanvas as G,textBoundsIntersect as m,resolveTextBounds as F}from"../core/textMetrics.js";import{axisGraphicIds as k,allLegendGraphicIds as L}from"./guides/resources.js";import{resolveGraphicBounds as u}from"../layout/canvas.js";import{resolvePlacedPlotBounds as j}from"../layout/composition.js";import{alignedTextAnchor as P,buildTitleReadingBlock as z,paintedTitleComponent as T,resolveTitleComponentBounds as B,titleTextProperties as E}from"../layout/title.js";import{resolveFacetLayout as $}from"../layout/facets.js";import{namespaceGraphicId as b,namespaceGraphicSnapshot as M}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as v}from"./facetGuides/index.js";import{resolvePromotedFacetLegendLayout as O}from"./facetGuides/layout.js";import{attachSnapshotObject as H,clearCompositionChildren as I,compositionChildDescriptor as q}from"./composition.js";import{materializeFacetHeaders as A,prepareFacetHeaders as R}from"./facetHeaders.js";const _={top:0,right:0,bottom:0,left:0};function W(e){const n=e.materializationConfigs.facets?.[e.compositionSpec.id];if(n===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return n}function D(e){if(e.semanticSpec.title.text===void 0)return{height:0};const n=e.titleConfig;if(n===void 0)throw new Error("Facet title requires chart title configuration.");if(n.position!=="top")throw new Error("Facet parent title currently supports only top position.");const i=z({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},n),o=8+n.offset;if(o<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:n,block:i,top:o,height:Math.ceil(o+i.height)}}function w(e,n,i,o,l,d,s){if(i.length===0)return e;const r=P(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),t=T({x:r,y:o.map(h=>s+h),lines:i,textAlign:e.titleConfig.align}),c=B(t,l,e.materializationConfigs.textMetrics);if(!G(c,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const a=t.lines.length;let p=e.createGraphics({id:n,type:"text",...a>1?{length:a}:{},parent:"canvas"});for(const[h,f]of Object.entries(E(t,l)))p=p.editGraphics({target:n,property:h,value:f});return p}function N(e,n,i){let o=w(e,"chartTitle",i.block.titleLines,i.block.titleCenters,i.config.titleStyle,n,i.top);return i.block.subtitleLines.length>0&&(o=w(o,"chartSubtitle",i.block.subtitleLines,i.block.subtitleCenters,i.config.subtitleStyle,n,i.top)),o}function Z(e,n){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");n??=v(e);const i=D(e),o=e.compositionSpec,l=R(e,W(e).headers),d=new Map((o.facet.grid?.cells??[]).map(c=>[c.id,c]));let s=$({children:o.children.map((c,a)=>({...q(c,e.children[c]),value:o.facet.values[a],...d.has(c)?{row:d.get(c).row,column:d.get(c).column}:{}})),spacing:o.spacing,plots:o.children.map(c=>({id:c,...u(e.children[c])})),columns:o.columns,gap:o.gap,align:o.align,padding:o.padding,titleHeight:i.height,headerLayout:l.headerLayout,sharedLegend:o.facet.guides.legend==="shared"&&n?.mode!=="promoted",...n===void 0?{}:{sharedLegendGap:n.reservation.gap,sharedLegendWidth:n.reservation.width,sharedLegendHeight:n.reservation.height,sharedLegendPosition:n.reservation.position}});const r=o.children.map(c=>({id:c,...u(e.children[c])}));let t=j({placements:s.children,plots:r});return{layout:s,plot:t}=O(e,{layout:s,plot:t,plots:r,title:i,preparedHeaders:l,prepared:n}),{layout:s,title:i,plot:t,plots:r,preparedHeaders:l}}function J(e,n,i){const o=[],l=n.children.map((s,r)=>({id:s.id,left:s.x+i[r].x,right:s.x+i[r].x+i[r].width,top:s.y+i[r].y,bottom:s.y+i[r].y+i[r].height}));for(const s of n.children){const r=e.children[s.id],t=[...["x","y"].flatMap(k).filter(c=>/Labels|Title/.test(c)),...L(Object.keys(r.guideConfigs.legend??{}))];for(const c of t){const a=b(`${e.compositionSpec.id}-${s.id}`,c);if(e.graphicSpec.objects[a]===void 0)continue;const p=C(e.graphicSpec,a,e.materializationConfigs.textMetrics);p!==void 0&&o.push({owner:s.id,bounds:p})}}const d=e.graphicSpec.objects[`${e.compositionSpec.id}-headers`];for(const s of d?.items??[])s.properties.text!==""&&o.push({owner:"headers",bounds:F(s.properties,e.materializationConfigs.textMetrics)});for(let s=0;s<o.length;s+=1){const r=o[s];if(l.some(t=>t.id!==r.owner&&m(r.bounds,t))||o.slice(0,s).some(t=>t.owner!==r.owner&&m(r.bounds,t.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function se(e){const n=v(e),{layout:i,title:o,plot:l,plots:d,preparedHeaders:s}=Z(e,n),r=e.graphicSpec.objects.canvas?.properties?.background??"white";let t=I(e);t.graphicSpec.objects.canvas===void 0&&(t=t.createGraphics({id:"canvas",type:"canvas"}));for(const[c,a]of Object.entries({width:i.width,height:i.height,background:r}))t=t.editGraphics({target:"canvas",property:c,value:a});for(const c of i.children){const a=e.children[c.id],p=M(a.graphicSpec,{namespace:`${e.compositionSpec.id}-${c.id}`,x:c.x,y:c.y});if(t=H(t,p,p.order[0],"canvas"),i.spacing==="plot"){const h=p.order[0],f=b(`${e.compositionSpec.id}-${c.id}`,"plot-background"),y=u(a),g=t.graphicSpec.objects[h].children?.[0];t=t.editGraphics({target:h,property:"background",value:"transparent"}).createGraphics({id:f,type:"rect",parent:h,...g===void 0?{}:{before:g}});for(const[x,S]of Object.entries({...y,stroke:"none",strokeWidth:0,fill:a.graphicSpec.objects.canvas.properties.background??"transparent"}))t=t.editGraphics({target:f,property:x,value:S})}}return t=A(t,i,d,s),t=t.composeFacetGuides({layout:i,plot:l}),i.spacing==="plot"&&J(t,i,d),o.height>0&&(t=N(t,l,o)),t._withCanvasConfig({margin:_,size:{width:"auto",height:"auto"}})}export{se as materializeFacetGraphics,Z as resolveFacetProgramLayout};
1
+ import{cartesianAxisOwners as C}from"../selectors/axes.js";import{resolveConcreteGraphicBounds as G}from"../grammar/schemas/graphicBounds.js";import{textBoundsFitCanvas as F,textBoundsIntersect as m,resolveTextBounds as k}from"../core/textMetrics.js";import{axisGraphicIds as L,allLegendGraphicIds as j}from"./guides/resources.js";import{resolveGraphicBounds as u}from"../layout/canvas.js";import{resolvePlacedPlotBounds as P}from"../layout/composition.js";import{alignedTextAnchor as z,buildTitleReadingBlock as T,paintedTitleComponent as B,resolveTitleComponentBounds as E,titleTextProperties as O}from"../layout/title.js";import{resolveFacetLayout as $}from"../layout/facets.js";import{namespaceGraphicId as b,namespaceGraphicSnapshot as M}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as v}from"./facetGuides/index.js";import{resolvePromotedFacetLegendLayout as H}from"./facetGuides/layout.js";import{attachSnapshotObject as I,clearCompositionChildren as A,compositionChildDescriptor as q}from"./composition.js";import{materializeFacetHeaders as R,prepareFacetHeaders as _}from"./facetHeaders.js";const W={top:0,right:0,bottom:0,left:0};function D(e){const c=e.materializationConfigs.facets?.[e.compositionSpec.id];if(c===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return c}function N(e){if(e.semanticSpec.title.text===void 0)return{height:0};const c=e.titleConfig;if(c===void 0)throw new Error("Facet title requires chart title configuration.");if(c.position!=="top")throw new Error("Facet parent title currently supports only top position.");const i=T({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},c),o=8+c.offset;if(o<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:c,block:i,top:o,height:Math.ceil(o+i.height)}}function w(e,c,i,o,l,d,s){if(i.length===0)return e;const r=z(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),t=B({x:r,y:o.map(h=>s+h),lines:i,textAlign:e.titleConfig.align}),n=E(t,l,e.materializationConfigs.textMetrics);if(!F(n,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const a=t.lines.length;let p=e.createGraphics({id:c,type:"text",...a>1?{length:a}:{},parent:"canvas"});for(const[h,f]of Object.entries(O(t,l)))p=p.editGraphics({target:c,property:h,value:f});return p}function Z(e,c,i){let o=w(e,"chartTitle",i.block.titleLines,i.block.titleCenters,i.config.titleStyle,c,i.top);return i.block.subtitleLines.length>0&&(o=w(o,"chartSubtitle",i.block.subtitleLines,i.block.subtitleCenters,i.config.subtitleStyle,c,i.top)),o}function J(e,c){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");c??=v(e);const i=N(e),o=e.compositionSpec,l=_(e,D(e).headers),d=new Map((o.facet.grid?.cells??[]).map(n=>[n.id,n]));let s=$({children:o.children.map((n,a)=>({...q(n,e.children[n]),value:o.facet.values[a],...d.has(n)?{row:d.get(n).row,column:d.get(n).column}:{}})),spacing:o.spacing,plots:o.children.map(n=>({id:n,...u(e.children[n])})),columns:o.columns,gap:o.gap,align:o.align,padding:o.padding,titleHeight:i.height,headerLayout:l.headerLayout,sharedLegend:o.facet.guides.legend==="shared"&&c?.mode!=="promoted",...c===void 0?{}:{sharedLegendGap:c.reservation.gap,sharedLegendWidth:c.reservation.width,sharedLegendHeight:c.reservation.height,sharedLegendPosition:c.reservation.position}});const r=o.children.map(n=>({id:n,...u(e.children[n])}));let t=P({placements:s.children,plots:r});return{layout:s,plot:t}=H(e,{layout:s,plot:t,plots:r,title:i,preparedHeaders:l,prepared:c}),{layout:s,title:i,plot:t,plots:r,preparedHeaders:l}}function K(e,c,i){const o=[],l=c.children.map((s,r)=>({id:s.id,left:s.x+i[r].x,right:s.x+i[r].x+i[r].width,top:s.y+i[r].y,bottom:s.y+i[r].y+i[r].height}));for(const s of c.children){const r=e.children[s.id],t=[...C(r).flatMap(n=>L(n.channel,n.id).slice(2)),...j(Object.keys(r.guideConfigs.legend??{}))];for(const n of t){const a=b(`${e.compositionSpec.id}-${s.id}`,n);if(e.graphicSpec.objects[a]===void 0)continue;const p=G(e.graphicSpec,a,e.materializationConfigs.textMetrics);p!==void 0&&o.push({owner:s.id,bounds:p})}}const d=e.graphicSpec.objects[`${e.compositionSpec.id}-headers`];for(const s of d?.items??[])s.properties.text!==""&&o.push({owner:"headers",bounds:k(s.properties,e.materializationConfigs.textMetrics)});for(let s=0;s<o.length;s+=1){const r=o[s];if(l.some(t=>t.id!==r.owner&&m(r.bounds,t))||o.slice(0,s).some(t=>t.owner!==r.owner&&m(r.bounds,t.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function ae(e){const c=v(e),{layout:i,title:o,plot:l,plots:d,preparedHeaders:s}=J(e,c),r=e.graphicSpec.objects.canvas?.properties?.background??"white";let t=A(e);t.graphicSpec.objects.canvas===void 0&&(t=t.createGraphics({id:"canvas",type:"canvas"}));for(const[n,a]of Object.entries({width:i.width,height:i.height,background:r}))t=t.editGraphics({target:"canvas",property:n,value:a});for(const n of i.children){const a=e.children[n.id],p=M(a.graphicSpec,{namespace:`${e.compositionSpec.id}-${n.id}`,x:n.x,y:n.y});if(t=I(t,p,p.order[0],"canvas"),i.spacing==="plot"){const h=p.order[0],f=b(`${e.compositionSpec.id}-${n.id}`,"plot-background"),y=u(a),g=t.graphicSpec.objects[h].children?.[0];t=t.editGraphics({target:h,property:"background",value:"transparent"}).createGraphics({id:f,type:"rect",parent:h,...g===void 0?{}:{before:g}});for(const[x,S]of Object.entries({...y,stroke:"none",strokeWidth:0,fill:a.graphicSpec.objects.canvas.properties.background??"transparent"}))t=t.editGraphics({target:f,property:x,value:S})}}return t=R(t,i,d,s),t=t.composeFacetGuides({layout:i,plot:l}),i.spacing==="plot"&&K(t,i,d),o.height>0&&(t=Z(t,l,o)),t._withCanvasConfig({margin:W,size:{width:"auto",height:"auto"}})}export{ae as materializeFacetGraphics,J as resolveFacetProgramLayout};
@@ -1,3 +1,5 @@
1
+ import { cartesianAxisOwners } from "../selectors/axes.js";
2
+ import { axisGraphicIds } from "./guides/resources.js";
1
3
  import {
2
4
  findGraphicParent,
3
5
  graphicSiblings
@@ -55,8 +57,10 @@ export function resolveMarkGraphicPlacement(program, { data, markType }) {
55
57
  ? dataset.transform[0]
56
58
  : undefined;
57
59
  if (transform?.type !== "regression") {
60
+ const named = new Set(cartesianAxisOwners(program).filter(owner => owner.named)
61
+ .flatMap(owner => axisGraphicIds(owner.channel, owner.id)));
58
62
  const axis = program.graphicSpec.objects[PLOT_GRAPHIC_ID]?.children?.find(
59
- id => id === "plotBorder" || /^(x|y)Axis/.test(id)
63
+ id => id === "plotBorder" || /^(x|y)Axis/.test(id) || named.has(id)
60
64
  );
61
65
  return resolvePlotGraphicPlacement(
62
66
  program,
@@ -0,0 +1,73 @@
1
+ function translateCommands(commands, dx, dy) {
2
+ return commands.map(command => {
3
+ if (command.op === "Z") return { ...command };
4
+ const next = {
5
+ ...command,
6
+ x: command.x + dx,
7
+ y: command.y + dy
8
+ };
9
+ if (command.op === "C") {
10
+ next.x1 = command.x1 + dx;
11
+ next.y1 = command.y1 + dy;
12
+ next.x2 = command.x2 + dx;
13
+ next.y2 = command.y2 + dy;
14
+ }
15
+ return next;
16
+ });
17
+ }
18
+
19
+ export function translatedProperties(type, properties, dx, dy) {
20
+ const next = { ...properties };
21
+ if (["circle", "rect", "text", "canvas"].includes(type)) {
22
+ next.x = (next.x ?? 0) + dx;
23
+ next.y = (next.y ?? 0) + dy;
24
+ } else if (type === "line") {
25
+ next.x1 += dx;
26
+ next.x2 += dx;
27
+ next.y1 += dy;
28
+ next.y2 += dy;
29
+ } else if (type === "path") {
30
+ next.commands = translateCommands(next.commands, dx, dy);
31
+ }
32
+ return next;
33
+ }
34
+
35
+ export function translateGraphic(program, id, dx, dy) {
36
+ const graphic = program.graphicSpec.objects[id];
37
+ if (dx === 0 && dy === 0) return program;
38
+ if (graphic.items !== undefined) {
39
+ let next = program.editGraphics({
40
+ target: id,
41
+ property: "items",
42
+ value: graphic.items.map(item => ({
43
+ type: item.type ?? graphic.type,
44
+ properties: translatedProperties(
45
+ item.type ?? graphic.type,
46
+ item.properties,
47
+ dx,
48
+ dy
49
+ )
50
+ }))
51
+ });
52
+ if (graphic.type !== "canvas") {
53
+ for (const child of graphic.children ?? []) next = translateGraphic(next, child, dx, dy);
54
+ }
55
+ return next;
56
+ }
57
+ let next = program;
58
+ const properties = translatedProperties(
59
+ graphic.type,
60
+ graphic.properties,
61
+ dx,
62
+ dy
63
+ );
64
+ for (const [property, value] of Object.entries(properties)) {
65
+ if (value !== graphic.properties[property]) {
66
+ next = next.editGraphics({ target: id, property, value });
67
+ }
68
+ }
69
+ if (graphic.type !== "canvas") {
70
+ for (const child of graphic.children ?? []) next = translateGraphic(next, child, dx, dy);
71
+ }
72
+ return next;
73
+ }
@@ -1 +1 @@
1
- import{needsLegendAxisReflow as c}from"./resources.js";import{getErrorDetails as u}from"../../core/diagnostics.js";import{planCanvasRematerialization as m}from"../layout.js";import{applyMaterializationPlan as p}from"../planner.js";import{assertGuideCollisionBlocks as g}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as h}from"./resources.js";import{resolveGuideCollisionBlocks as k}from"./resources.js";function C(i,d){if(i.context.deferGuideLayoutValidation===!0)return d(i);const n=i.materializationConfigs.canvas?.plot;let o=i.materializationConfigs.canvas?.margin,e;for(let f=0;;f+=1)try{let t=i._withContext({deferGuideLayoutValidation:!0});f>0&&(t=t.editGraphics({target:"canvas",property:"width",value:n.width+o.left+o.right}).editGraphics({target:"canvas",property:"height",value:n.height+o.top+o.bottom})._withCanvasConfig({...t.materializationConfigs.canvas,margin:o}),t=p(t,m(t))),e=d(t),e.compositionSpec===void 0&&Object.values(e.guideConfigs.legend??{}).some(r=>["left","right"].includes(r.position))&&(e=e.rematerializeSideLegendLane()),e.compositionSpec===void 0&&c(i,e)&&(e=e.rematerializeHorizontalLegendLane()),e.compositionSpec===void 0&&Object.keys(e.guideConfigs.legendGroups??{}).length>0&&(e=e.rematerializeLegendGroups()),n!==void 0&&e.titleConfig!==void 0&&(e=e.rematerializeTitle());break}catch(t){const r=u(t)?.canvasOverflow;if(n===void 0||r===void 0||f>=16)throw t;o=Object.fromEntries(Object.keys(o).map(l=>[l,r[l]>0?Math.ceil((o[l]+r[l])*4)/4:o[l]]))}const{deferGuideLayoutValidation:v,...s}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(s.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const a=e._clone({context:s});return g(h(a.graphicSpec,a.guideConfigs,a.titleConfig,a.materializationConfigs.textMetrics)),a}function b(i){return function(d){return C(this,n=>i.call(n,d))}}export{k as resolveGuideCollisionBlocks,C as withGuideLayoutTransaction,b as withGuideLayoutValidation};
1
+ import{resetLegendClipGraphics as u,hasLegendClipGraphics as c}from"../legendClip.js";import{localLegendPosition as g}from"../legendGroups.js";import{needsLegendAxisReflow as m}from"./resources.js";import{getErrorDetails as p}from"../../core/diagnostics.js";import{planCanvasRematerialization as h}from"../layout.js";import{applyMaterializationPlan as C}from"../planner.js";import{assertGuideCollisionBlocks as L}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as v}from"./resources.js";import{resolveGuideCollisionBlocks as M}from"./resources.js";function G(i,d){if(i.context.deferGuideLayoutValidation===!0)return d(i);const a=i.materializationConfigs.canvas?.plot;let n=i.materializationConfigs.canvas?.margin,e;for(let s=0;;s+=1)try{let t=u(i)._withContext({deferGuideLayoutValidation:!0});s>0&&(t=t.editGraphics({target:"canvas",property:"width",value:a.width+n.left+n.right}).editGraphics({target:"canvas",property:"height",value:a.height+n.top+n.bottom})._withCanvasConfig({...t.materializationConfigs.canvas,margin:n}),t=C(t,h(t))),e=d(t),c(i)&&!c(e)&&Object.values(e.guideConfigs.legend??{}).some(o=>o.clipHeight!==void 0)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&Object.values(e.guideConfigs.legend??{}).some(o=>["left","right"].includes(o.position))&&(e=e.rematerializeSideLegendLane()),e.compositionSpec===void 0&&m(i,e)&&(e=e.rematerializeHorizontalLegendLane()),e.compositionSpec===void 0&&(Object.keys(e.guideConfigs.legendGroups??{}).length>0||Object.values(e.guideConfigs.legend??{}).some(o=>g(o)!==void 0))&&(e=e.rematerializeLegendGroups()),a!==void 0&&e.titleConfig!==void 0&&(e=e.rematerializeTitle());break}catch(t){const o=p(t)?.canvasOverflow;if(a===void 0||o===void 0||s>=16)throw t;n=Object.fromEntries(Object.keys(n).map(l=>[l,o[l]>0?Math.ceil((n[l]+o[l])*4)/4:n[l]]))}const{deferGuideLayoutValidation:y,...f}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(f.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const r=e._clone({context:f});return L(v(r.graphicSpec,r.guideConfigs,r.titleConfig,r.materializationConfigs.textMetrics)),r}function S(i){return function(d){return G(this,a=>i.call(a,d))}}export{M as resolveGuideCollisionBlocks,G as withGuideLayoutTransaction,S as withGuideLayoutValidation};