ggaction 0.0.22 → 0.0.23

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 (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +81 -89
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/package.json +6 -3
  8. package/src/actions/charts/interval-regression.js +2 -2
  9. package/src/actions/data/edit.js +1 -1
  10. package/src/actions/data/timeUnit.js +1 -1
  11. package/src/actions/facets/actions.js +1 -1
  12. package/src/actions/guides/legends/categorical/components.js +1 -4
  13. package/src/actions/regression/create.js +6 -1
  14. package/src/actions/regression/edit.js +10 -1
  15. package/src/core/compositionState.js +5 -3
  16. package/src/core/facetAxisPolicy.js +30 -0
  17. package/src/grammar/curveCommands.js +35 -2
  18. package/src/grammar/datasetSchema.js +1 -1
  19. package/src/grammar/facetHeaders.js +1 -1
  20. package/src/grammar/facets/guides.js +7 -5
  21. package/src/grammar/regression/parameters.js +7 -0
  22. package/src/grammar/timeUnit.js +9 -3
  23. package/src/inspectionDescriptors.js +1 -1
  24. package/src/layout/facets.js +1 -1
  25. package/src/layout/guideCollisions.js +9 -3
  26. package/src/materialization/facetContentBounds.js +96 -0
  27. package/src/materialization/facetGuides/layout.js +1 -1
  28. package/src/materialization/facetGuides/placement.js +16 -0
  29. package/src/materialization/facetHeaders.js +1 -1
  30. package/src/materialization/facets.js +1 -1
  31. package/src/materialization/graphicHierarchy.js +25 -1
  32. package/src/persistence/validation.js +6 -0
  33. package/src/version.js +1 -1
  34. package/types/program.d.ts +17 -11
@@ -1 +1 @@
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
+ 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 at,normalizeCompositionPadding as it,validateCompositionSpacing as E}from"./composition.js";const lt=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 w=Math.max(...r.map(o=>o.column))+1;if(i!==void 0&&i!==w)throw new RangeError("Facet grid columns must match the declared column domain.");return{columns:w,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,m]of Object.entries(d))E(m,`Facet headerLayout.outer.${o}`);const w={outer:d};for(const[o,m,x,z]of[["cellRows",["top","bottom"],i,"row"],["cellColumns",["left","right"],f,"column"]]){if(o==="cellColumns"&&r[o]===void 0)continue;const R=o==="cellRows"?r[o]??{}:r[o];if(!U(R)||Object.keys(R).some(l=>!m.includes(l)))throw new TypeError(`Facet headerLayout.${o} must contain ${m[0]} and ${m[1]} arrays.`);w[o]=Object.fromEntries(m.map(l=>{const F=R[l]??Array(x).fill(0);if(!Array.isArray(F)||F.length!==x)throw new RangeError(`Facet headerLayout.${o}.${l} must match ${z} count.`);return F.forEach((c,D)=>E(c,`Facet headerLayout.${o}.${l}[${D}]`)),[l,[...F]]}))}return w}function yt({children:r,columns:i,gap:f=P.gap,align:d=P.align,padding:w=P.padding,titleHeight:o=0,sharedLegend:m=!1,sharedLegendGap:x=lt,sharedLegendWidth:z=st,sharedLegendHeight:R=ct,sharedLegendPosition:l="right",headerLayout:F,spacing:c="canvas",topLane:D,plots:L}={}){if(!["canvas","plot","content"].includes(c))throw new Error(`Unknown facet spacing "${c}".`);const $=ot(D,c),H=new Map((L??[]).map(e=>[e.id,e])),W=r;let h=M;if(c==="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),g=ht(r??[],i);if(c==="content"&&g!==void 0)throw new Error("Content flow requires a wrapped facet, not a keyed facet grid.");const I=at(r?.map(({value:e,row:t,column:s,...a})=>a)),u=g?.columns??mt(i,I.length),p=E(f,"Facet gap"),N=nt(d),k=it(w),y=Object.fromEntries(Object.keys(k).map(e=>[e,k[e]+h[e]])),j=E(o,"Facet title height"),Y=$===void 0?j:0;if(typeof m!="boolean")throw new TypeError("Facet sharedLegend must be a boolean.");const q=E(x,"Facet shared legend gap"),Z=E(z,"Facet shared legend width"),B=E(R,"Facet shared legend height");if(!ut.includes(l))throw new Error(`Unknown facet shared legend position "${l}".`);const O=["right","left"].includes(l),J=["left","top"].includes(l),G=m?q+(O?Z:B):0,A=g?.rows??Math.ceil(I.length/u);let n=ft(F,A,u);if(c==="plot"){let e=0;for(const[t,s]of[[n.cellRows.bottom,n.cellRows.top],[n.cellColumns?.right??[],n.cellColumns?.left??[]]])for(let a=1;a<t.length;a+=1)e=Math.max(e,t[a-1]+s[a]);if(e>p)throw rt(new Error("Plot spacing gap is too small for the facet headers."),{code:"invalid-value",optionPath:"gap",actual:p,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(u).fill(0),right:Array(u).fill(0)},C=Array(u).fill(-1/0),v=Array(A).fill(-1/0);I.forEach((e,t)=>{const s=g===void 0?t%u:r[t].column,a=g===void 0?Math.floor(t/u):r[t].row;C[s]=Math.max(C[s],e.width),v[a]=Math.max(v[a],e.height)});const X=C.map((e,t)=>y.left+n.outer.left+C.slice(0,t).reduce((s,a,_)=>s+T.left[_]+a+T.right[_],0)+p*t+T.left[t]),tt=v.map((e,t)=>Y+y.top+n.outer.top+v.slice(0,t).reduce((s,a,_)=>s+n.cellRows.top[_]+a+n.cellRows.bottom[_],0)+p*t+n.cellRows.top[t]),b=I.map((e,t)=>{const s=g===void 0?t%u:r[t].column,a=g===void 0?Math.floor(t/u):r[t].row;return{id:e.id,value:V[t],column:s,row:a,x:X[s]+(J&&O?G:0)+Q(C[s]-e.width,N),y:tt[a]+(J&&!O?G:0)+Q(v[a]-e.height,N),width:e.width,height:e.height}}),S=y.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)+p*Math.max(0,u-1)+n.outer.right+y.right,K=Y+y.top+n.outer.top+v.reduce((e,t)=>e+t,0)+n.cellRows.top.reduce((e,t)=>e+t,0)+n.cellRows.bottom.reduce((e,t)=>e+t,0)+p*Math.max(0,A-1)+n.outer.bottom+y.bottom;if(c==="plot")for(let e=0;e<b.length;e+=1){const t=H.get(b[e].id);b[e].x-=t.x,b[e].y-=t.y,b[e].width=W[e].width,b[e].height=W[e].height}return et({...c!=="canvas"?{spacing:c}:{},...$===void 0?{}:{topLane:$},columns:u,rows:A,gap:p,align:N,padding:k,titleHeight:j,headerLayout:n,gridWidth:S,width:S+(O?G:0),height:K+(O?0:G),children:b,...m?{legend:{position:l,x:l==="right"?S+q:0,y:l==="bottom"?K+q:l==="top"?j:j+y.top+30,width:Z,height:B}}:{}})}export{lt as DEFAULT_FACET_LEGEND_GAP,ct as DEFAULT_FACET_LEGEND_HEIGHT,st as DEFAULT_FACET_LEGEND_WIDTH,yt as resolveFacetLayout};
@@ -1,3 +1,10 @@
1
+ function intersects(a, b) {
2
+ const tolerance = Number.EPSILON * 16 * Math.max(1,
3
+ ...Object.values(a).map(Math.abs), ...Object.values(b).map(Math.abs));
4
+ return Math.min(a.right, b.right) - Math.max(a.left, b.left) > tolerance &&
5
+ Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > tolerance;
6
+ }
7
+
1
8
  export function assertGuideCollisionBlocks(blocks) {
2
9
  for (let index = 0; index < blocks.length; index += 1) {
3
10
  const a = blocks[index];
@@ -5,14 +12,13 @@ export function assertGuideCollisionBlocks(blocks) {
5
12
  if (a.kind === "axis" && b.kind === "axis") {
6
13
  if (!(a.namedAxis || b.namedAxis) || a.axisId === b.axisId || !a.textBounds || !b.textBounds) continue;
7
14
  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))) {
15
+ intersects(left, right)))) {
9
16
  throw new Error(`The ${a.id} and ${b.id} text overlap; independent axes require separate text bounds.`);
10
17
  }
11
18
  continue;
12
19
  }
13
20
  if (a.position !== b.position) continue;
14
- if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
15
- a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
21
+ if (intersects(a.bounds, b.bounds)) {
16
22
  throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
17
23
  }
18
24
  }
@@ -0,0 +1,96 @@
1
+ import { cartesianAxisOwners } from "../selectors/axes.js";
2
+ import { facetAxisMode } from "../core/facetAxisPolicy.js";
3
+ import { resolvePrimitiveGraphicBounds, resolveGraphicParentOffset, resolveGraphicClipBounds } from "../grammar/schemas/graphicBounds.js";
4
+ import { intersectClipBounds } from "../grammar/schemas/graphicClip.js";
5
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
6
+ import { walkGraphicTreeEvents } from "../grammar/schemas/graphicTree.js";
7
+ import { resolveTextBounds, requireFiniteBounds } from "../core/textMetrics.js";
8
+ import { axisGraphicIds, allLegendGraphicIds } from "./guides/resources.js";
9
+ import { resolveGraphicBounds } from "../layout/canvas.js";
10
+ import { prepareSharedFacetLegend } from "./facetGuides/index.js";
11
+ import { prepareFacetGuideOwnership } from "./facetGuides/placement.js";
12
+
13
+ function includeBounds(union, bounds) {
14
+ if (bounds === undefined) return union;
15
+ if (union === undefined) return { ...bounds };
16
+ return { left: Math.min(union.left, bounds.left), right: Math.max(union.right, bounds.right),
17
+ top: Math.min(union.top, bounds.top), bottom: Math.max(union.bottom, bounds.bottom) };
18
+ }
19
+
20
+ // Header entries are concrete child-local text, never caller-supplied bounds.
21
+ export function measureFacetContentChildren(program, { headerItems = [] } = {}) {
22
+ if (program?.compositionSpec?.type !== "facet") {
23
+ throw new Error("Facet content measurement requires a facet composition.");
24
+ }
25
+ const spec = program.compositionSpec, grid = new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
26
+ const placements = spec.children.map((id, index) => ({ id,
27
+ row: grid.get(id)?.row ?? Math.floor(index / spec.columns),
28
+ column: grid.get(id)?.column ?? index % spec.columns }));
29
+ const prepared = prepareSharedFacetLegend(program);
30
+ const ownership = prepareFacetGuideOwnership(program, placements, prepared);
31
+ const childIds = new Set(spec.children), headersByOwner = new Map();
32
+ if (!Array.isArray(headerItems) || headerItems.some(item =>
33
+ !childIds.has(item?.ownerId) || item.type !== "text" || !isPlainObject(item.properties))) {
34
+ throw new TypeError("Facet content headers require concrete text with one observed child owner.");
35
+ }
36
+ for (const header of headerItems) {
37
+ if (!headersByOwner.has(header.ownerId)) headersByOwner.set(header.ownerId, []);
38
+ headersByOwner.get(header.ownerId).push(header);
39
+ }
40
+ const components = ["line", "ticks", "labels", "title"];
41
+ return cloneAndFreeze(placements.map(cell => {
42
+ const child = program.children[cell.id], graphic = child.graphicSpec;
43
+ const profile = child.materializationConfigs.textMetrics;
44
+ const plot = resolveGraphicBounds(child);
45
+ const plan = ownership.children[cell.id], roles = new Map();
46
+ for (const owner of cartesianAxisOwners(child)) {
47
+ const roots = axisGraphicIds(owner.channel, owner.id);
48
+ for (const [index, component] of components.entries()) {
49
+ const removed = plan.removeAxes.includes(owner.id) || plan.removeAxisComponents?.[owner.id]?.includes(component);
50
+ if (removed) roles.set(roots[index], "removed");
51
+ else if (facetAxisMode(spec.facet.guides.axes, owner.channel) === "outer" && owner.channel === "x" && owner.config?.[component]?.position === "bottom") {
52
+ roles.set(roots[index], "footer");
53
+ }
54
+ }
55
+ }
56
+ for (const id of allLegendGraphicIds(plan.removeLegends)) roles.set(id, "removed");
57
+ const groups = new Map(), stack = [], frames = new Map();
58
+ const include = entry => {
59
+ const role = stack.at(-1);
60
+ if (role === "removed") return;
61
+ const type = entry.object.type ?? entry.owner?.type, p = entry.object.properties;
62
+ if (!p || type === "canvas" || p.opacity === 0 || type === "text" && (p.text === "" || p.fill === "none" || p.fill === "transparent")) return;
63
+ const ownerId = entry.ownerId ?? entry.id;
64
+ if (!frames.has(ownerId)) frames.set(ownerId, {
65
+ offset: resolveGraphicParentOffset(graphic, ownerId),
66
+ clip: resolveGraphicClipBounds(graphic, ownerId, { rootViewport: true })
67
+ });
68
+ const { offset: [x, y], clip } = frames.get(ownerId);
69
+ let bounds = resolvePrimitiveGraphicBounds(type, p, profile);
70
+ if (bounds === undefined) return;
71
+ bounds = requireFiniteBounds({ left: bounds.left + x, right: bounds.right + x,
72
+ top: bounds.top + y, bottom: bounds.bottom + y });
73
+ if (clip !== undefined) bounds = intersectClipBounds(bounds, clip);
74
+ if (bounds.left > bounds.right || bounds.top > bounds.bottom) return;
75
+ const key = role ?? "body";
76
+ groups.set(key, includeBounds(groups.get(key), bounds));
77
+ };
78
+ walkGraphicTreeEvents(graphic, {
79
+ enter(entry) {
80
+ stack.push(stack.at(-1) === "removed" ? "removed" : roles.get(entry.id) ?? stack.at(-1));
81
+ if (entry.object.items === undefined && !(entry.object.children?.length)) include(entry);
82
+ },
83
+ item: include,
84
+ exit() { stack.pop(); }
85
+ });
86
+ let body = includeBounds({ left: plot.x, right: plot.x + plot.width, top: plot.y, bottom: plot.y + plot.height }, groups.get("body"));
87
+ for (const header of headersByOwner.get(cell.id) ?? []) {
88
+ const p = header.properties;
89
+ if (p.text !== "" && p.opacity !== 0 && p.fill !== "none" && p.fill !== "transparent") body = includeBounds(body, resolveTextBounds(p, profile));
90
+ }
91
+ const relative = bounds => bounds === undefined ? undefined : ({ left: bounds.left - plot.x, right: bounds.right - plot.x,
92
+ top: bounds.top - plot.y, bottom: bounds.bottom - plot.y });
93
+ return { ...cell, plot, bounds: relative(body),
94
+ ...(groups.has("footer") ? { footerBounds: relative(groups.get("footer")) } : {}) };
95
+ }));
96
+ }
@@ -1 +1 @@
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};
1
+ import{cartesianAxisOwners as O,axisGraphicIds as S}from"../../selectors/axes.js";import{resolveAuthoredFacetLegendLayout as B}from"./ownerLayout.js";import{PLOT_LEGEND_POSITIONS as F}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as P}from"../../grammar/schemas/graphicBounds.js";import{resolveTextBounds as y,textBoundsIntersect as C}from"../../core/textMetrics.js";import{resolveFacetHeaderItems as E,retainedFacetGuideIds as T}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,m){const f=n.materializationConfigs.textMetrics,h=[];for(const o of t.children){const c=n.children[o.id],g=new Set(o.footerShift===void 0?[]:O(c).filter(d=>d.channel==="x").flatMap(d=>["line","ticks","labels","title"].flatMap((a,l)=>d.config?.[a]?.position==="bottom"?[S(d.channel,d.id)[l]]:[])));for(const d of T(n,o.id)){const a=P(c.graphicSpec,d,f);a!==void 0&&h.push(w(a,o.x,o.y+(g.has(d)?o.footerShift:0)))}}const{items:i}=E(t,e,m);for(const o of i)o.properties.text!==""&&h.push(y(o.properties,f));return h}function b(n,t,e,m,f){const h=n.right-n.left,i=n.bottom-n.top,o=e==="right"?t.x+t.width+f:e==="left"?t.x-f-h:m==="left"?t.x:m==="right"?t.x+t.width-h:t.x+(t.width-h)/2,c=["left","right"].includes(e)?t.y:e==="top"?t.y-f-i:t.y+t.height+f;return{left:o,right:o+h,top:c,bottom:c+i}}function j(n,{layout:t,plot:e,plots:m,title:f,preparedHeaders:h,prepared:i}){if(i?.mode==="authored")return B(n,{layout:t,plot:e,title:f,prepared:i,guides:M(n,t,m,h)});if(i?.mode!=="promoted")return{layout:t,plot:e};const o=i.occupiedGroup;if(o?.placement.at!==void 0||F.includes(o?.placement.position))return{layout:t,plot:e};const c=i.reservation.position,g=i.reservation.align,d=i.kinds.map(r=>i.source.guideConfigs.legend[r]),a=i.reservation.gap,l=o===void 0?d.every(r=>r.inferredOffset===!0):o.inferredOffset===!0;let s=b(i.bounds,e,c,g,a);const v=M(n,t,m,h),I=v.map(r=>c==="right"?r.right-(e.x+e.width):c==="left"?e.x-r.left:c==="top"?e.y-r.top:r.bottom-(e.y+e.height));if(l)s=b(i.bounds,e,c,g,Math.max(a,...I.map(r=>r+a)));else if(v.some(r=>C(s,r)))throw new Error("Facet shared legend overlaps retained axes or headers.");const x=Math.max(0,t.padding.left-s.left),u=Math.max(0,f.height+t.padding.top-s.top),G=t.children.map(r=>({...r,x:r.x+x,y:r.y+u})),L={...e,x:e.x+x,y:e.y+u};return s=w(s,x,u),{plot:L,layout:{...t,children:G,width:Math.ceil(Math.max(t.width+x,s.right+t.padding.right)),height:Math.ceil(Math.max(t.height+u,s.bottom+t.padding.bottom)),legend:{position:c,x:s.left,y:s.top,width:s.right-s.left,height:s.bottom-s.top,translation:{x:s.left-i.bounds.left,y:s.top-i.bounds.top}}}}}export{j as resolvePromotedFacetLegendLayout};
@@ -197,6 +197,22 @@ export function applyFacetGuideComposition(program, { layout, plot, synchronizeC
197
197
  const ids = axisGraphicIds(owner.channel, axisId);
198
198
  next = removeNamespacedGraphics(next, namespace, components.map(component => ids[["line", "ticks", "labels", "title"].indexOf(component)]));
199
199
  }
200
+ if (cell.footerShift !== undefined) {
201
+ for (const owner of axisOwners.filter(owner => owner.channel === "x")) {
202
+ for (const [index, component] of ["line", "ticks", "labels", "title"].entries()) {
203
+ if (owner.config?.[component]?.position !== "bottom") continue;
204
+ const id = namespaceGraphicId(namespace, axisGraphicIds(owner.channel, owner.id)[index]);
205
+ if (next.graphicSpec.objects[id] !== undefined) {
206
+ // Footer guides belong to the parent viewport: moving them within
207
+ // the child Canvas would clip ragged-row owners at its old edge.
208
+ const snapshot = next.graphicSpec;
209
+ next = next.editGraphics({ target: id, remove: true });
210
+ next = attachSnapshotObject(next, snapshot, id, "canvas");
211
+ next = translateGraphic(next, id, cell.x, cell.y + cell.footerShift);
212
+ }
213
+ }
214
+ }
215
+ }
200
216
  if (childPlan.removeLegends.length > 0) {
201
217
  next = removeNamespacedGraphics(
202
218
  next,
@@ -1 +1 @@
1
- import{isPlainObject as F}from"../core/immutable.js";import{formatVisibleText as V,measureTextWidth as P,resolveTextBounds as z,textBoundsIntersect as S}from"../core/textMetrics.js";import{validateFacetHeaderRotations as W}from"../grammar/facetHeaders.js";import{resolveDisplayLabel as _}from"../grammar/displayLabels.js";import{resolveGraphicBounds as B}from"../layout/canvas.js";import{unionConcreteGraphicBounds as U}from"../grammar/schemas/graphicBounds.js";import{intersectClipBounds as K}from"../grammar/schemas/graphicClip.js";import{DEFAULT_COLORS as N,DEFAULT_FONT_FAMILY as Y}from"../theme/defaults.js";import{prepareFacetHeaderTitles as Z}from"../layout/facetHeaderTitles.js";import{cartesianAxisOwners as J,axisGraphicIds as E}from"../selectors/axes.js";import{resolveFacetOuterSides as Q}from"../grammar/facets/guides.js";import{resolveConcreteGraphicBounds as X}from"../grammar/schemas/graphicBounds.js";import{allLegendGraphicIds as tt}from"./guides/resources.js";const et=Object.freeze(["top","right","bottom","left"]),v=Object.freeze({top:0,right:0,bottom:0,left:0});function ot(t){return`${t}-headers`}function T(){return{mode:"legacy",common:{fontSize:12,fontFamily:Y,fontWeight:600,color:N.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function j(t){if(!F(t))throw new TypeError("Facet headers config must be a plain object.");if(W(t),t.common===void 0&&t.mode===void 0)return{...T(),common:{...T().common,...t}};if(!["legacy","roles"].includes(t.mode)||!F(t.common)||!F(t.row)||!F(t.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:t.mode,common:{...t.common},row:{...t.row},column:{...t.column}}}function y(t,e){const o=j(t);return e==="all"?{...o.common}:{...o.common,...o[e]}}function b(t,e){return _(t,e.labelMap,V)}function nt(t){return new Map((t.facet.grid?.cells??[]).map(e=>[e.id,e]))}function it(t,e){const o=t.compositionSpec,c=y(e,"all"),i=nt(o);return o.children.map((n,s)=>{const r=i.get(n),a=r===void 0?b(o.facet.values[s],c):`${b(r.rowValue,c)} \xB7 ${b(r.columnValue,c)}`;return{topology:"legacy",role:"column",id:n,text:a,config:c}})}function G(t,e){const o=e==="column"?"column":"row",c=e==="column"?"columnValue":"rowValue",i=new Map;for(const n of t.facet.grid.cells){const s=i.get(n[o]);if(s===void 0)i.set(n[o],{coordinate:n[o],value:n[c],ids:[n.id]});else{if(s.value!==n[c])throw new Error(`Facet ${e} header values conflict at coordinate ${n[o]}.`);s.ids.push(n.id)}}return[...i.values()].sort((n,s)=>n.coordinate-s.coordinate)}function ct(t,e){const o=t.compositionSpec,c=y(e,"column");if(o.facet.grid===void 0)return o.children.map((r,a)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(a/o.columns),column:a%o.columns,text:b(o.facet.values[a],c),config:c}));const i=y(e,"row"),n=G(o,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:b(r.value,c),config:c})),s=G(o,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:b(r.value,i),config:i}));return[...n,...s]}function x(t){return t.rotation!==void 0&&t.rotation!==0}function H(t,e){return z({x:0,y:0,text:t.text,...t.config,textAlign:"center",textBaseline:"middle"},e)}function st(t,e,o){if(x(t.config)){const c=H(t,o);return["left","right"].includes(e)?c.right-c.left:c.bottom-c.top}return["left","right"].includes(e)?P(t.text,t.config,o):t.config.fontSize}function rt(t){const e={...v};if(t.compositionSpec.spacing!=="plot")return e;for(const o of t.compositionSpec.children){const c=t.children[o],i=B(c),n=c.graphicSpec.objects.canvas.properties;e.left=Math.max(e.left,i.x),e.top=Math.max(e.top,i.y),e.right=Math.max(e.right,n.width-i.x-i.width),e.bottom=Math.max(e.bottom,n.height-i.y-i.height)}return e}function O(t,e,o,c,i){const n=st(t,e,o)+t.config.offset+(t.clearance?.[e]??0);return x(t.config)&&i?Math.max(0,n-c[e]):n}function ft(t,e,o){if(!e.some(i=>i.config.anchor==="content"))return;if(t.compositionSpec.spacing!=="plot")throw new Error("Facet header content anchor requires plot spacing.");const c=new Map;for(const i of t.compositionSpec.children){const n=t.children[i],s=B(n),r={left:s.x,right:s.x+s.width,top:s.y,bottom:s.y+s.height},a=n.semanticSpec.layers.map(f=>f.id).filter(f=>n.graphicSpec.objects[f]!==void 0),u=U(n.graphicSpec,a,o),l=n.graphicSpec.objects.canvas.properties,h=u===void 0?void 0:K(u,{left:0,right:l.width,top:0,bottom:l.height});c.set(i,h===void 0||h.left>h.right||h.top>h.bottom?r:C([r,h]))}for(const i of e)if(i.config.anchor==="content"){i.clearance={...v};for(const n of i.ids??[i.id]){const s=B(t.children[n]),r=c.get(n);for(const[a,u]of Object.entries({left:s.x-r.left,right:r.right-s.x-s.width,top:s.y-r.top,bottom:r.bottom-s.y-s.height}))i.clearance[a]=Math.max(i.clearance[a],u)}}return c}function R(t,e,o,c){const i=t.filter(n=>n.role===e&&n.text!=="");return i.length===0?0:Math.max(...i.map(n=>O(n,n.config.side,o,c,!0)))}function at(t,e){const o=t.compositionSpec,c=t.children[e],i=o.facet.guides.axes==="outer"?Q({placements:o.facet.grid?.cells??o.children.map((s,r)=>({id:s,row:Math.floor(r/o.columns),column:r%o.columns}))}):void 0,n=J(c).flatMap(s=>E(s.channel,s.id).filter((r,a)=>a>=2&&(i===void 0||i[s.config?.[["line","ticks","labels","title"][a]]?.position]?.includes(e))));for(const s of["theta","radius","parallel"])c.guideConfigs.axis?.[s]!==void 0&&n.push(...E(s).slice(2));return o.facet.guides.legend===!1&&n.push(...tt(Object.keys(c.guideConfigs.legend??{}))),n.filter(s=>c.graphicSpec.objects[s]!==void 0)}function lt(t,e){const o=t.children[e],c=B(o),i=at(t,e).map(n=>X(o.graphicSpec,n,o.materializationConfigs.textMetrics)).filter(Boolean);return{top:Math.max(0,...i.map(n=>c.y-n.top)),bottom:Math.max(0,...i.map(n=>n.bottom-c.y-c.height)),left:Math.max(0,...i.map(n=>c.x-n.left)),right:Math.max(0,...i.map(n=>n.right-c.x-c.width))}}function Et(t,e){const o=j(e),c=o.mode==="legacy"?it(t,o):ct(t,o),i=t.compositionSpec.facet.grid===void 0?Math.ceil(t.compositionSpec.children.length/t.compositionSpec.columns):Math.max(...t.compositionSpec.facet.grid.cells.map(f=>f.row))+1,n=t.materializationConfigs.textMetrics,s=["column",...t.compositionSpec.facet.grid===void 0?[]:["row"]].map(f=>[f,y(o,f)]),r=s.some(([,f])=>f.title!==void 0);if(t.compositionSpec.spacing==="plot"){const f=new Map(t.compositionSpec.children.map(p=>[p,lt(t,p)]));for(const p of c)p.clearance=Object.fromEntries(et.map(m=>{const d=Math.max(0,...(p.ids??[p.id]).map(g=>f.get(g)[m]));return[m,d>p.config.offset?d:0]}))}const a=ft(t,c,n),u=r||c.some(f=>x(f.config))?rt(t):v,l={outer:{...v},cellRows:{top:Array(i).fill(0),bottom:Array(i).fill(0)}};if(o.mode==="legacy"&&t.compositionSpec.spacing==="plot")for(const f of c){if(f.text==="")continue;const p=t.compositionSpec.children.indexOf(f.id),m=t.compositionSpec.facet.grid?.cells[p]?.row??Math.floor(p/t.compositionSpec.columns);l.cellRows.top[m]=Math.max(l.cellRows.top[m],O(f,"top",n,u,m===0))}if(o.mode==="roles"){const f=y(o,"column"),p=R(c,"column",n,u);if(t.compositionSpec.facet.grid===void 0){const m=["left","right"].includes(f.side);m&&(l.cellColumns={left:Array(t.compositionSpec.columns).fill(0),right:Array(t.compositionSpec.columns).fill(0)});for(const d of c)if(d.text!==""){const g=m?l.cellColumns:l.cellRows,M=m?d.column:d.row,$=m?t.compositionSpec.columns-1:i-1,q=["top","left"].includes(f.side)?M===0:M===$;g[f.side][M]=Math.max(g[f.side][M],O(d,f.side,n,u,q))}}else{l.outer[f.side]=p;const m=y(o,"row");l.outer[m.side]=R(c,"row",n,u)}}const h=Z({roles:s,descriptors:c,space:u,headerLayout:l,profile:n,ids:t.compositionSpec.children});return{headers:o,descriptors:c,titles:h,headerLayout:l,...a===void 0?{}:{contentById:a},...(r||c.some(f=>x(f.config)))&&n!==void 0?{profile:n}:{}}}function D(t,e){return e==="start"?{x:t.left,textAlign:"left"}:e==="end"?{x:t.right,textAlign:"right"}:{x:(t.left+t.right)/2,textAlign:"center"}}function dt(t,e){return e==="start"?{y:t.top,textBaseline:"top"}:e==="end"?{y:t.bottom,textBaseline:"bottom"}:{y:(t.top+t.bottom)/2,textBaseline:"middle"}}function C(t){return{left:Math.min(...t.map(e=>e.left)),right:Math.max(...t.map(e=>e.right)),top:Math.min(...t.map(e=>e.top)),bottom:Math.max(...t.map(e=>e.bottom))}}function w(t,e){return{left:t.x+e.x,right:t.x+e.x+e.width,top:t.y+e.y,bottom:t.y+e.y+e.height}}function A(t){return{left:t.x,right:t.x+t.width,top:t.y,bottom:t.y+t.height}}function I(t,e){return{left:t.x+e.left,right:t.x+e.right,top:t.y+e.top,bottom:t.y+e.bottom}}function L(t,e,o,c,i){return i==="start"?t-o:i==="end"?e-c:(t+e-o-c)/2}function k(t,e,o,c){const{config:i}=t,n=H(t,c),s={textAlign:"center",textBaseline:"middle",rotation:i.rotation};return["top","bottom"].includes(i.side)?{...s,x:L(e.left,e.right,n.left,n.right,i.align),y:i.side==="top"?o.top-i.offset-n.bottom:o.bottom+i.offset-n.top}:{...s,x:i.side==="left"?o.left-i.offset-n.right:o.right+i.offset-n.left,y:L(e.top,e.bottom,n.top,n.bottom,i.align)}}function ut(t,e,o,c,i,n){const s=t.ids.map(d=>e.get(d)),r=t.ids.find((d,g)=>s[g]===void 0),a=t.ids.find(d=>o.get(d)===void 0);if(r!==void 0||a!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${r??a}".`);const u=t.ids.map((d,g)=>w(s[g],o.get(d))),l=C(u),h=t.config.anchor==="content"?C(t.ids.map((d,g)=>I(s[g],n.get(d)))):c==="plot"?l:C(s.map(A)),f=t.config;if(x(f)||t.topology==="role-title"){const d=k(t,l,h,i),g=["left","right"].includes(f.side)?"y":"x";return f.alongOffset!==void 0&&(d[g]+=f.alongOffset),{descriptor:t,snapshots:h,properties:d}}const p=f.anchor==="content"?0:t.clearance?.[f.side]??0;let m;if(["top","bottom"].includes(f.side)){m=D(l,f.align);const d=f.side==="top";m.y=d?h.top-p-f.offset-f.fontSize/2:h.bottom+p+f.offset+f.fontSize/2,m.textBaseline="middle"}else{m=dt(l,f.align);const d=f.side==="left";m.x=d?h.left-p-f.offset:h.right+p+f.offset,m.textAlign=d?"right":"left"}return{descriptor:t,snapshots:h,properties:m}}function mt(t,e,o,c,i,n){if(e===void 0||o===void 0)throw new Error(`Facet header requires cell and plot bounds for "${t.id}".`);const s=t.config.anchor==="content"?I(e,n.get(t.id)):w(e,o);if(x(t.config)&&c==="plot"){const r=w(e,o);return{descriptor:t,snapshots:A(e),properties:k({...t,config:{...t.config,side:"top"}},r,s,i)}}return{descriptor:t,snapshots:A(e),properties:{...D(w(e,o),t.config.align),y:c==="plot"?e.y+o.y-(t.clearance?.top??0)-t.config.offset-t.config.fontSize/2:e.y+t.config.offset,textBaseline:"middle",...x(t.config)?{rotation:t.config.rotation}:{}}}}function pt(t){const{config:e}=t.descriptor;return{type:"text",properties:{...t.properties,text:t.descriptor.text,fill:e.color,fontSize:e.fontSize,fontFamily:e.fontFamily,fontWeight:e.fontWeight}}}function ht(t,e,o,c,i,n){const s=e.map((r,a)=>t[a].descriptor.text===""?void 0:z(r.properties,c));for(const[r,a]of s.entries())if(a!==void 0&&(a.left<-1e-9||a.right>o.width+1e-9||a.top<-1e-9||a.bottom>o.height+1e-9||s.some((u,l)=>l<r&&u!==void 0&&S(u,a))||o.children.some(u=>S(a,n===void 0?o.spacing==="plot"?w(u,i.get(u.id)):A(u):I(u,n.get(u.id))))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function gt(t,e,o,c,i){const n=[];for(let s=0;s<t.length;s+=1){if(t[s].properties.text==="")continue;const r=z(t[s].properties,c),a=e.children[s],u=o.get(a.id);if(r.left<0||r.right>e.width||r.top<0||r.bottom>e.height||n.some(l=>S(l,r))||(e.spacing==="plot"?e.children.some(l=>S(r,i===void 0?w(l,o.get(l.id)):I(l,i.get(l.id)))):S(r,w(a,u))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");n.push(r)}}function xt(t,e,o){const c=new Map(t.children.map(s=>[s.id,s])),i=new Map(e.map(s=>[s.id,s])),n=[...o.descriptors,...o.titles??[]].map(s=>s.topology==="legacy"?mt(s,c.get(s.id),i.get(s.id),t.spacing,o.profile,o.contentById):ut(s,c,i,t.spacing,o.profile,o.contentById));return{records:n,items:n.map(pt),plotById:i}}function Tt(t,e,o,c){const i=t.materializationConfigs.textMetrics,{records:n,items:s,plotById:r}=xt(e,o,c);c.headers.mode==="legacy"?gt(s,e,r,i,c.contentById):ht(n,s,e,i,r,c.contentById);const a=ot(t.compositionSpec.id);return t.createGraphics({id:a,type:"collection",parent:"canvas"}).editGraphics({target:a,property:"items",value:s})}export{T as createDefaultFacetHeaders,ot as facetHeaderGraphicId,Tt as materializeFacetHeaders,j as normalizeFacetHeadersConfig,Et as prepareFacetHeaders,y as resolveFacetHeaderConfig,xt as resolveFacetHeaderItems,at as retainedFacetGuideIds};
1
+ import{measureFacetContentChildren as V}from"./facetContentBounds.js";import{facetAxisMode as P,hasOuterFacetAxes as W}from"../core/facetAxisPolicy.js";import{isPlainObject as v}from"../core/immutable.js";import{formatVisibleText as _,measureTextWidth as U,resolveTextBounds as z,textBoundsIntersect as S}from"../core/textMetrics.js";import{validateFacetHeaderRotations as K}from"../grammar/facetHeaders.js";import{resolveDisplayLabel as N}from"../grammar/displayLabels.js";import{resolveGraphicBounds as F}from"../layout/canvas.js";import{unionConcreteGraphicBounds as Y}from"../grammar/schemas/graphicBounds.js";import{intersectClipBounds as Z}from"../grammar/schemas/graphicClip.js";import{DEFAULT_COLORS as J,DEFAULT_FONT_FAMILY as Q}from"../theme/defaults.js";import{prepareFacetHeaderTitles as X}from"../layout/facetHeaderTitles.js";import{cartesianAxisOwners as tt,axisGraphicIds as E}from"../selectors/axes.js";import{resolveFacetOuterSides as et}from"../grammar/facets/guides.js";import{resolveConcreteGraphicBounds as ot}from"../grammar/schemas/graphicBounds.js";import{allLegendGraphicIds as nt}from"./guides/resources.js";const it=Object.freeze(["top","right","bottom","left"]),B=Object.freeze({top:0,right:0,bottom:0,left:0});function ct(t){return`${t}-headers`}function T(){return{mode:"legacy",common:{fontSize:12,fontFamily:Q,fontWeight:600,color:J.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function j(t){if(!v(t))throw new TypeError("Facet headers config must be a plain object.");if(K(t),t.common===void 0&&t.mode===void 0)return{...T(),common:{...T().common,...t}};if(!["legacy","roles"].includes(t.mode)||!v(t.common)||!v(t.row)||!v(t.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:t.mode,common:{...t.common},row:{...t.row},column:{...t.column}}}function y(t,e){const o=j(t);return e==="all"?{...o.common}:{...o.common,...o[e]}}function b(t,e){return N(t,e.labelMap,_)}function st(t){return new Map((t.facet.grid?.cells??[]).map(e=>[e.id,e]))}function rt(t,e){const o=t.compositionSpec,c=y(e,"all"),s=st(o);return o.children.map((i,n)=>{const r=s.get(i),f=r===void 0?b(o.facet.values[n],c):`${b(r.rowValue,c)} \xB7 ${b(r.columnValue,c)}`;return{topology:"legacy",role:"column",id:i,text:f,config:c}})}function G(t,e){const o=e==="column"?"column":"row",c=e==="column"?"columnValue":"rowValue",s=new Map;for(const i of t.facet.grid.cells){const n=s.get(i[o]);if(n===void 0)s.set(i[o],{coordinate:i[o],value:i[c],ids:[i.id]});else{if(n.value!==i[c])throw new Error(`Facet ${e} header values conflict at coordinate ${i[o]}.`);n.ids.push(i.id)}}return[...s.values()].sort((i,n)=>i.coordinate-n.coordinate)}function ft(t,e){const o=t.compositionSpec,c=y(e,"column");if(o.facet.grid===void 0)return o.children.map((r,f)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(f/o.columns),column:f%o.columns,text:b(o.facet.values[f],c),config:c}));const s=y(e,"row"),i=G(o,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:b(r.value,c),config:c})),n=G(o,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:b(r.value,s),config:s}));return[...i,...n]}function x(t){return t.rotation!==void 0&&t.rotation!==0}function H(t,e){return z({x:0,y:0,text:t.text,...t.config,textAlign:"center",textBaseline:"middle"},e)}function at(t,e,o){if(x(t.config)){const c=H(t,o);return["left","right"].includes(e)?c.right-c.left:c.bottom-c.top}return["left","right"].includes(e)?U(t.text,t.config,o):t.config.fontSize}function lt(t){const e={...B};if(!["plot","content"].includes(t.compositionSpec.spacing))return e;for(const o of t.compositionSpec.children){const c=t.children[o],s=F(c),i=c.graphicSpec.objects.canvas.properties;e.left=Math.max(e.left,s.x),e.top=Math.max(e.top,s.y),e.right=Math.max(e.right,i.width-s.x-s.width),e.bottom=Math.max(e.bottom,i.height-s.y-s.height)}return e}function O(t,e,o,c,s){const i=at(t,e,o)+t.config.offset+(t.clearance?.[e]??0);return x(t.config)&&s?Math.max(0,i-c[e]):i}function dt(t,e,o){if(!e.some(n=>["content","view"].includes(n.config.anchor)))return;if(!["plot","content"].includes(t.compositionSpec.spacing))throw new Error("Facet header content anchor requires plot spacing or content spacing.");const c=new Map,s=e.some(n=>n.config.anchor==="view")?new Map(V(t).map(n=>[n.id,n])):void 0;for(const n of t.compositionSpec.children){const r=t.children[n],f=F(r),d={left:f.x,right:f.x+f.width,top:f.y,bottom:f.y+f.height},l=r.semanticSpec.layers.map(u=>u.id).filter(u=>r.graphicSpec.objects[u]!==void 0),h=Y(r.graphicSpec,l,o),a=r.graphicSpec.objects.canvas.properties,m=h===void 0?void 0:Z(h,{left:0,right:a.width,top:0,bottom:a.height});c.set(n,m===void 0||m.left>m.right||m.top>m.bottom?d:C([d,m]))}const i=s===void 0?void 0:new Map([...s].map(([n,r])=>[n,{left:r.bounds.left+r.plot.x,right:r.bounds.right+r.plot.x,top:r.bounds.top+r.plot.y,bottom:r.bounds.bottom+r.plot.y}]));for(const n of e)if(["content","view"].includes(n.config.anchor)){n.config.anchor==="view"&&(n.contentById=i),n.clearance={...B};for(const r of n.ids??[n.id]){const f=F(t.children[r]),d=(n.contentById??c).get(r);for(const[l,h]of Object.entries({left:f.x-d.left,right:d.right-f.x-f.width,top:f.y-d.top,bottom:d.bottom-f.y-f.height}))n.clearance[l]=Math.max(n.clearance[l],h)}}return c}function R(t,e,o,c){const s=t.filter(i=>i.role===e&&i.text!=="");return s.length===0?0:Math.max(...s.map(i=>O(i,i.config.side,o,c,!0)))}function ut(t,e){const o=t.compositionSpec,c=t.children[e],s=W(o.facet.guides.axes)?et({placements:o.facet.grid?.cells??o.children.map((n,r)=>({id:n,row:Math.floor(r/o.columns),column:r%o.columns}))}):void 0,i=tt(c).flatMap(n=>E(n.channel,n.id).filter((r,f)=>f>=2&&(s===void 0||P(o.facet.guides.axes,n.channel)==="each"||s[n.config?.[["line","ticks","labels","title"][f]]?.position]?.includes(e))));for(const n of["theta","radius","parallel"])c.guideConfigs.axis?.[n]!==void 0&&i.push(...E(n).slice(2));return o.facet.guides.legend===!1&&i.push(...nt(Object.keys(c.guideConfigs.legend??{}))),i.filter(n=>c.graphicSpec.objects[n]!==void 0)}function mt(t,e){const o=t.children[e],c=F(o),s=ut(t,e).map(i=>ot(o.graphicSpec,i,o.materializationConfigs.textMetrics)).filter(Boolean);return{top:Math.max(0,...s.map(i=>c.y-i.top)),bottom:Math.max(0,...s.map(i=>i.bottom-c.y-c.height)),left:Math.max(0,...s.map(i=>c.x-i.left)),right:Math.max(0,...s.map(i=>i.right-c.x-c.width))}}function Rt(t,e){const o=j(e),c=o.mode==="legacy"?rt(t,o):ft(t,o),s=t.compositionSpec.facet.grid===void 0?Math.ceil(t.compositionSpec.children.length/t.compositionSpec.columns):Math.max(...t.compositionSpec.facet.grid.cells.map(a=>a.row))+1,i=t.materializationConfigs.textMetrics,n=["column",...t.compositionSpec.facet.grid===void 0?[]:["row"]].map(a=>[a,y(o,a)]),r=n.some(([,a])=>a.title!==void 0);if(["plot","content"].includes(t.compositionSpec.spacing)){const a=new Map(t.compositionSpec.children.map(m=>[m,mt(t,m)]));for(const m of c)m.clearance=Object.fromEntries(it.map(u=>{const p=Math.max(0,...(m.ids??[m.id]).map(g=>a.get(g)[u]));return[u,p>m.config.offset?p:0]}))}const f=dt(t,c,i),d=r||c.some(a=>x(a.config))?lt(t):B,l={outer:{...B},cellRows:{top:Array(s).fill(0),bottom:Array(s).fill(0)}};if(o.mode==="legacy"&&["plot","content"].includes(t.compositionSpec.spacing))for(const a of c){if(a.text==="")continue;const m=t.compositionSpec.children.indexOf(a.id),u=t.compositionSpec.facet.grid?.cells[m]?.row??Math.floor(m/t.compositionSpec.columns);l.cellRows.top[u]=Math.max(l.cellRows.top[u],O(a,"top",i,d,u===0))}if(o.mode==="roles"){const a=y(o,"column"),m=R(c,"column",i,d);if(t.compositionSpec.facet.grid===void 0){const u=["left","right"].includes(a.side);u&&(l.cellColumns={left:Array(t.compositionSpec.columns).fill(0),right:Array(t.compositionSpec.columns).fill(0)});for(const p of c)if(p.text!==""){const g=u?l.cellColumns:l.cellRows,M=u?p.column:p.row,$=u?t.compositionSpec.columns-1:s-1,q=["top","left"].includes(a.side)?M===0:M===$;g[a.side][M]=Math.max(g[a.side][M],O(p,a.side,i,d,q))}}else{l.outer[a.side]=m;const u=y(o,"row");l.outer[u.side]=R(c,"row",i,d)}}const h=X({roles:n,descriptors:c,space:d,headerLayout:l,profile:i,ids:t.compositionSpec.children});return{headers:o,descriptors:c,titles:h,headerLayout:l,...f===void 0?{}:{contentById:f},...(r||c.some(a=>x(a.config)))&&i!==void 0?{profile:i}:{}}}function D(t,e){return e==="start"?{x:t.left,textAlign:"left"}:e==="end"?{x:t.right,textAlign:"right"}:{x:(t.left+t.right)/2,textAlign:"center"}}function pt(t,e){return e==="start"?{y:t.top,textBaseline:"top"}:e==="end"?{y:t.bottom,textBaseline:"bottom"}:{y:(t.top+t.bottom)/2,textBaseline:"middle"}}function C(t){return{left:Math.min(...t.map(e=>e.left)),right:Math.max(...t.map(e=>e.right)),top:Math.min(...t.map(e=>e.top)),bottom:Math.max(...t.map(e=>e.bottom))}}function w(t,e){return{left:t.x+e.x,right:t.x+e.x+e.width,top:t.y+e.y,bottom:t.y+e.y+e.height}}function A(t){return{left:t.x,right:t.x+t.width,top:t.y,bottom:t.y+t.height}}function I(t,e){return{left:t.x+e.left,right:t.x+e.right,top:t.y+e.top,bottom:t.y+e.bottom}}function L(t,e,o,c,s){return s==="start"?t-o:s==="end"?e-c:(t+e-o-c)/2}function k(t,e,o,c){const{config:s}=t,i=H(t,c),n={textAlign:"center",textBaseline:"middle",rotation:s.rotation};return["top","bottom"].includes(s.side)?{...n,x:L(e.left,e.right,i.left,i.right,s.align),y:s.side==="top"?o.top-s.offset-i.bottom:o.bottom+s.offset-i.top}:{...n,x:s.side==="left"?o.left-s.offset-i.right:o.right+s.offset-i.left,y:L(e.top,e.bottom,i.top,i.bottom,s.align)}}function ht(t,e,o,c,s,i){i=t.contentById??i;const n=t.ids.map(p=>e.get(p)),r=t.ids.find((p,g)=>n[g]===void 0),f=t.ids.find(p=>o.get(p)===void 0);if(r!==void 0||f!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${r??f}".`);const d=t.ids.map((p,g)=>w(n[g],o.get(p))),l=C(d),h=["content","view"].includes(t.config.anchor)?C(t.ids.map((p,g)=>I(n[g],i.get(p)))):["plot","content"].includes(c)?l:C(n.map(A)),a=t.config;if(x(a)||t.topology==="role-title"){const p=k(t,l,h,s),g=["left","right"].includes(a.side)?"y":"x";return a.alongOffset!==void 0&&(p[g]+=a.alongOffset),{descriptor:t,snapshots:h,properties:p}}const m=["content","view"].includes(a.anchor)?0:t.clearance?.[a.side]??0;let u;if(["top","bottom"].includes(a.side)){u=D(l,a.align);const p=a.side==="top";u.y=p?h.top-m-a.offset-a.fontSize/2:h.bottom+m+a.offset+a.fontSize/2,u.textBaseline="middle"}else{u=pt(l,a.align);const p=a.side==="left";u.x=p?h.left-m-a.offset:h.right+m+a.offset,u.textAlign=p?"right":"left"}return{descriptor:t,snapshots:h,properties:u}}function gt(t,e,o,c,s,i){if(i=t.contentById??i,e===void 0||o===void 0)throw new Error(`Facet header requires cell and plot bounds for "${t.id}".`);const n=["content","view"].includes(t.config.anchor)?I(e,i.get(t.id)):w(e,o);if(x(t.config)&&["plot","content"].includes(c)){const r=w(e,o);return{descriptor:t,snapshots:A(e),properties:k({...t,config:{...t.config,side:"top"}},r,n,s)}}return{descriptor:t,snapshots:A(e),properties:{...D(w(e,o),t.config.align),y:["plot","content"].includes(c)?e.y+o.y-(t.clearance?.top??0)-t.config.offset-t.config.fontSize/2:e.y+t.config.offset,textBaseline:"middle",...x(t.config)?{rotation:t.config.rotation}:{}}}}function xt(t){const{config:e}=t.descriptor;return{type:"text",properties:{...t.properties,text:t.descriptor.text,fill:e.color,fontSize:e.fontSize,fontFamily:e.fontFamily,fontWeight:e.fontWeight}}}function wt(t,e,o,c,s,i){const n=e.map((r,f)=>t[f].descriptor.text===""?void 0:z(r.properties,c));for(const[r,f]of n.entries())if(f!==void 0&&(f.left<-1e-9||f.right>o.width+1e-9||f.top<-1e-9||f.bottom>o.height+1e-9||n.some((d,l)=>l<r&&d!==void 0&&S(d,f))||o.children.some(d=>S(f,i===void 0?["plot","content"].includes(o.spacing)?w(d,s.get(d.id)):A(d):I(d,i.get(d.id))))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function yt(t,e,o,c,s){const i=[];for(let n=0;n<t.length;n+=1){if(t[n].properties.text==="")continue;const r=z(t[n].properties,c),f=e.children[n],d=o.get(f.id);if(r.left<0||r.right>e.width||r.top<0||r.bottom>e.height||i.some(l=>S(l,r))||(["plot","content"].includes(e.spacing)?e.children.some(l=>S(r,s===void 0?w(l,o.get(l.id)):I(l,s.get(l.id)))):S(r,w(f,d))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");i.push(r)}}function bt(t,e,o){const c=new Map(t.children.map(n=>[n.id,n])),s=new Map(e.map(n=>[n.id,n])),i=[...o.descriptors,...o.titles??[]].map(n=>n.topology==="legacy"?gt(n,c.get(n.id),s.get(n.id),t.spacing,o.profile,o.contentById):ht(n,c,s,t.spacing,o.profile,o.contentById));return{records:i,items:i.map(xt),plotById:s}}function Dt(t,e,o,c){const s=t.materializationConfigs.textMetrics,{records:i,items:n,plotById:r}=bt(e,o,c);c.headers.mode==="legacy"?yt(n,e,r,s,c.contentById):wt(i,n,e,s,r,c.contentById);const f=ct(t.compositionSpec.id);return t.createGraphics({id:f,type:"collection",parent:"canvas"}).editGraphics({target:f,property:"items",value:n})}export{T as createDefaultFacetHeaders,ct as facetHeaderGraphicId,Dt as materializeFacetHeaders,j as normalizeFacetHeadersConfig,Rt as prepareFacetHeaders,y as resolveFacetHeaderConfig,bt as resolveFacetHeaderItems,ut as retainedFacetGuideIds};
@@ -1 +1 @@
1
- import{cartesianAxisOwners as W}from"../selectors/axes.js";import{resolveConcreteGraphicBounds as q,resolvePrimitiveGraphicBounds as N,resolveGraphicParentOffset as V,resolveGraphicClipBounds as Z}from"../grammar/schemas/graphicBounds.js";import{intersectClipBounds as J}from"../grammar/schemas/graphicClip.js";import{cloneAndFreeze as K,isPlainObject as Q}from"../core/immutable.js";import{walkGraphicTreeEvents as _}from"../grammar/schemas/graphicTree.js";import{textBoundsFitCanvas as U,textBoundsIntersect as L,resolveTextBounds as M,requireFiniteBounds as X}from"../core/textMetrics.js";import{axisGraphicIds as Y,allLegendGraphicIds as ee}from"./guides/resources.js";import{resolveGraphicBounds as G}from"../layout/canvas.js";import{resolvePlacedPlotBounds as te}from"../layout/composition.js";import{alignedTextAnchor as oe,buildTitleReadingBlock as ie,paintedTitleComponent as ne,resolveTitleComponentBounds as re,titleTextProperties as ce}from"../layout/title.js";import{resolveFacetLayout as se}from"../layout/facets.js";import{assertFacetTopLane as ae}from"./facetTopLane.js";import{namespaceGraphicId as R,namespaceGraphicSnapshot as pe}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as I}from"./facetGuides/index.js";import{prepareFacetGuideOwnership as de}from"./facetGuides/placement.js";import{resolvePromotedFacetLegendLayout as le}from"./facetGuides/layout.js";import{attachSnapshotObject as fe,clearCompositionChildren as he,compositionChildDescriptor as ue}from"./composition.js";import{materializeFacetHeaders as me,prepareFacetHeaders as ge,facetHeaderGraphicId as B,retainedFacetGuideIds as we}from"./facetHeaders.js";const be={top:0,right:0,bottom:0,left:0},T=Object.freeze(["chartTitle","chartSubtitle"]);function z(e,i){return i===void 0?e:e===void 0?{...i}:{left:Math.min(e.left,i.left),right:Math.max(e.right,i.right),top:Math.min(e.top,i.top),bottom:Math.max(e.bottom,i.bottom)}}function De(e,{headerItems:i=[]}={}){if(e?.compositionSpec?.type!=="facet")throw new Error("Facet content measurement requires a facet composition.");const t=e.compositionSpec,r=new Map((t.facet.grid?.cells??[]).map(c=>[c.id,c])),l=t.children.map((c,f)=>({id:c,row:r.get(c)?.row??Math.floor(f/t.columns),column:r.get(c)?.column??f%t.columns})),d=I(e),n=de(e,l,d),p=new Set(t.children),o=new Map;if(!Array.isArray(i)||i.some(c=>!p.has(c?.ownerId)||c.type!=="text"||!Q(c.properties)))throw new TypeError("Facet content headers require concrete text with one observed child owner.");for(const c of i)o.has(c.ownerId)||o.set(c.ownerId,[]),o.get(c.ownerId).push(c);const s=["line","ticks","labels","title"];return K(l.map(c=>{const f=e.children[c.id],m=f.graphicSpec,b=f.materializationConfigs.textMetrics,h=G(f),x=n.children[c.id],y=new Map;for(const a of W(f)){const g=Y(a.channel,a.id);for(const[S,w]of s.entries())x.removeAxes.includes(a.id)||x.removeAxisComponents?.[a.id]?.includes(w)?y.set(g[S],"removed"):t.facet.guides.axes==="outer"&&a.channel==="x"&&a.config?.[w]?.position==="bottom"&&y.set(g[S],"footer")}for(const a of ee(x.removeLegends))y.set(a,"removed");const v=new Map,F=[],j=new Map,P=a=>{const g=F.at(-1);if(g==="removed")return;const S=a.object.type??a.owner?.type,w=a.object.properties;if(!w||S==="canvas"||w.opacity===0||S==="text"&&(w.text===""||w.fill==="none"||w.fill==="transparent"))return;const C=a.ownerId??a.id;j.has(C)||j.set(C,{offset:V(m,C),clip:Z(m,C,{rootViewport:!0})});const{offset:[O,A],clip:H}=j.get(C);let u=N(S,w,b);if(u===void 0||(u=X({left:u.left+O,right:u.right+O,top:u.top+A,bottom:u.bottom+A}),H!==void 0&&(u=J(u,H)),u.left>u.right||u.top>u.bottom))return;const $=g??"body";v.set($,z(v.get($),u))};_(m,{enter(a){F.push(F.at(-1)==="removed"?"removed":y.get(a.id)??F.at(-1)),a.object.items===void 0&&!a.object.children?.length&&P(a)},item:P,exit(){F.pop()}});let k=z({left:h.x,right:h.x+h.width,top:h.y,bottom:h.y+h.height},v.get("body"));for(const a of o.get(c.id)??[]){const g=a.properties;g.text!==""&&g.opacity!==0&&g.fill!=="none"&&g.fill!=="transparent"&&(k=z(k,M(g,b)))}const E=a=>a===void 0?void 0:{left:a.left-h.x,right:a.right-h.x,top:a.top-h.y,bottom:a.bottom-h.y};return{...c,plot:h,bounds:E(k),...v.has("footer")?{footerBounds:E(v.get("footer"))}:{}}}))}function ve(e){const i=e.materializationConfigs.facets?.[e.compositionSpec.id];if(i===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return i}function xe(e){if(e.semanticSpec.title.text===void 0)return{height:0};const i=e.titleConfig;if(i===void 0)throw new Error("Facet title requires chart title configuration.");if(i.position!=="top")throw new Error("Facet parent title currently supports only top position.");const t=ie({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},i),r=8+i.offset;if(r<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:i,block:t,top:r,height:Math.ceil(r+t.height)}}function D(e,i,t,r,l,d,n){if(t.length===0)return e;const p=oe(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),o=ne({x:p,y:r.map(m=>n+m),lines:t,textAlign:e.titleConfig.align}),s=re(o,l,e.materializationConfigs.textMetrics);if(!U(s,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const c=o.lines.length;let f=e.createGraphics({id:i,type:"text",...c>1?{length:c}:{},parent:"canvas"});for(const[m,b]of Object.entries(ce(o,l)))f=f.editGraphics({target:i,property:m,value:b});return f}function ye(e,i,t){let r=D(e,T[0],t.block.titleLines,t.block.titleCenters,t.config.titleStyle,i,t.top);return t.block.subtitleLines.length>0&&(r=D(r,T[1],t.block.subtitleLines,t.block.subtitleCenters,t.config.subtitleStyle,i,t.top)),r}function Se(e,i){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");i??=I(e);const t=xe(e),r=e.compositionSpec,l=ge(e,ve(e).headers),d=new Map((r.facet.grid?.cells??[]).map(s=>[s.id,s]));let n=se({children:r.children.map((s,c)=>({...ue(s,e.children[s]),value:r.facet.values[c],...d.has(s)?{row:d.get(s).row,column:d.get(s).column}:{}})),spacing:r.spacing,topLane:r.topLane,plots:r.children.map(s=>({id:s,...G(e.children[s])})),columns:r.columns,gap:r.gap,align:r.align,padding:r.padding,titleHeight:t.height,headerLayout:l.headerLayout,sharedLegend:r.facet.guides.legend==="shared"&&i?.mode!=="promoted",...i===void 0?{}:{sharedLegendGap:i.reservation.gap,sharedLegendWidth:i.reservation.width,sharedLegendHeight:i.reservation.height,sharedLegendPosition:i.reservation.position}});const p=r.children.map(s=>({id:s,...G(e.children[s])}));let o=te({placements:n.children,plots:p});return{layout:n,plot:o}=le(e,{layout:n,plot:o,plots:p,title:t,preparedHeaders:l,prepared:i}),{layout:n,title:t,plot:o,plots:p,preparedHeaders:l}}function Ce(e,i,t){const r=[],l=i.children.map((n,p)=>({id:n.id,left:n.x+t[p].x,right:n.x+t[p].x+t[p].width,top:n.y+t[p].y,bottom:n.y+t[p].y+t[p].height}));for(const n of i.children){const p=we(e,n.id);for(const o of p){const s=R(`${e.compositionSpec.id}-${n.id}`,o);if(e.graphicSpec.objects[s]===void 0)continue;const c=q(e.graphicSpec,s,e.materializationConfigs.textMetrics);c!==void 0&&r.push({owner:n.id,bounds:c})}}const d=e.graphicSpec.objects[B(e.compositionSpec.id)];for(const n of d?.items??[])n.properties.text!==""&&r.push({owner:"headers",bounds:M(n.properties,e.materializationConfigs.textMetrics)});for(let n=0;n<r.length;n+=1){const p=r[n];if(l.some(o=>o.id!==p.owner&&L(p.bounds,o))||r.slice(0,n).some(o=>o.owner!==p.owner&&L(p.bounds,o.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function Fe(e,i){if(i.titles.length===0)return;const t=B(e.compositionSpec.id),r=e.materializationConfigs.textMetrics,l=e.graphicSpec.objects[t].items.slice(i.descriptors.length).map(n=>M(n.properties,r)),d=n=>{if((n.ownerId??n.id)===t)return;const p=n.object.type??n.owner?.type,o=n.object.properties;if(p!=="text"||o?.text===""||o?.opacity===0)return;const s=q(e.graphicSpec,n.id,r);if(s!==void 0&&l.some(c=>L(c,s)))throw new Error("Facet field title overlaps retained chart text.")};_(e.graphicSpec,{enter:d,item:d})}function We(e){const i=I(e),{layout:t,title:r,plot:l,plots:d,preparedHeaders:n}=Se(e,i),p=e.graphicSpec.objects.canvas?.properties?.background??"white";let o=he(e);o.graphicSpec.objects.canvas===void 0&&(o=o.createGraphics({id:"canvas",type:"canvas"}));for(const[s,c]of Object.entries({width:t.width,height:t.height,background:p}))o=o.editGraphics({target:"canvas",property:s,value:c});for(const s of t.children){const c=e.children[s.id],f=pe(c.graphicSpec,{namespace:`${e.compositionSpec.id}-${s.id}`,x:s.x,y:s.y});if(o=fe(o,f,f.order[0],"canvas"),t.spacing==="plot"){const m=f.order[0],b=R(`${e.compositionSpec.id}-${s.id}`,"plot-background"),h=G(c),x=o.graphicSpec.objects[m].children?.[0];o=o.editGraphics({target:m,property:"background",value:"transparent"}).createGraphics({id:b,type:"rect",parent:m,...x===void 0?{}:{before:x}});for(const[y,v]of Object.entries({...h,stroke:"none",strokeWidth:0,fill:c.graphicSpec.objects.canvas.properties.background??"transparent"}))o=o.editGraphics({target:b,property:y,value:v})}}return o=me(o,t,d,n),o=o.composeFacetGuides({layout:t,plot:l}),t.spacing==="plot"&&Ce(o,t,d),r.height>0&&(o=ye(o,l,r)),Fe(o,n),ae(o,{layout:t,plots:d,headerId:B(e.compositionSpec.id),titleIds:T}),o._withCanvasConfig({margin:be,size:{width:"auto",height:"auto"}})}export{We as materializeFacetGraphics,De as measureFacetContentChildren,Se as resolveFacetProgramLayout};
1
+ import{walkGraphicTreeEvents as P}from"../grammar/schemas/graphicTree.js";import{measureFacetContentChildren as E}from"./facetContentBounds.js";import{measureFacetContentChildren as It}from"./facetContentBounds.js";import{resolveFacetContentFlow as O,resolveFacetContentFooters as H}from"../layout/facetContentFlow.js";import{resolveConcreteGraphicBounds as G}from"../grammar/schemas/graphicBounds.js";import{textBoundsFitCanvas as $,textBoundsIntersect as S,resolveTextBounds as I}from"../core/textMetrics.js";import{resolveGraphicBounds as C}from"../layout/canvas.js";import{resolvePlacedPlotBounds as R}from"../layout/composition.js";import{alignedTextAnchor as A,alignedTitleAnchor as _,buildTitleReadingBlock as q,paintedTitleComponent as D,resolveTitleComponentBounds as W,titleTextProperties as N}from"../layout/title.js";import{resolveFacetLayout as k}from"../layout/facets.js";import{assertFacetTopLane as Z}from"./facetTopLane.js";import{namespaceGraphicId as j,namespaceGraphicSnapshot as J}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as T}from"./facetGuides/index.js";import{resolvePromotedFacetLegendLayout as K}from"./facetGuides/layout.js";import{attachSnapshotObject as Q,clearCompositionChildren as U,compositionChildDescriptor as V}from"./composition.js";import{materializeFacetHeaders as X,resolveFacetHeaderItems as Y,prepareFacetHeaders as tt,facetHeaderGraphicId as F,retainedFacetGuideIds as et}from"./facetHeaders.js";const ot={top:0,right:0,bottom:0,left:0},L=Object.freeze(["chartTitle","chartSubtitle"]);function it(t){const c=t.materializationConfigs.facets?.[t.compositionSpec.id];if(c===void 0)throw new Error(`Facet "${t.compositionSpec.id}" requires materialization config.`);return c}function nt(t){if(t.semanticSpec.title.text===void 0)return{height:0};const c=t.titleConfig;if(c===void 0)throw new Error("Facet title requires chart title configuration.");const e=q({text:t.semanticSpec.title.text,subtitle:t.semanticSpec.title.subtitle},c,t.materializationConfigs.textMetrics),i=8+c.offset;if(i<0)throw new Error("Facet parent title offset places text outside the Canvas.");const h=Math.ceil(i+e.height);return{config:c,block:e,top:i,reservation:h,height:c.position==="top"?h:0}}function z(t,c,e,i,h,f,n){if(e.length===0)return t;const{config:s,block:o,top:a}=n,d=t.graphicSpec.objects.canvas.properties,p=["top","bottom"].includes(s.position),u=D(p?{x:A(f.x,f.width,s.align)+(s.alongOffset??0),y:i.map(m=>(s.position==="top"?a:d.height-a-o.height)+m),lines:e,textAlign:s.align,...s.position==="bottom"?{rotation:0,explicitRotation:!0}:{}}:{x:i.map(m=>s.position==="left"?a+m:d.width-a-m),y:_(f.y,f.height,o.width,s.align)+(s.alongOffset??0),lines:e,textAlign:"center",rotation:s.position==="left"?-Math.PI/2:Math.PI/2,explicitRotation:!0}),w=W(u,h,t.materializationConfigs.textMetrics);if(!$(w,t.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const v=u.lines.length;let l=t.createGraphics({id:c,type:"text",...v>1?{length:v}:{},parent:"canvas"});for(const[m,x]of Object.entries({...N(u,h),...u.explicitRotation?{rotation:u.rotation}:{}}))l=l.editGraphics({target:c,property:m,value:x});return l}function ct(t,c,e){let i=z(t,L[0],e.block.titleLines,e.block.titleCenters,e.config.titleStyle,c,e);return e.block.subtitleLines.length>0&&(i=z(i,L[1],e.block.subtitleLines,e.block.subtitleCenters,e.config.subtitleStyle,c,e)),i}function st(t,c,e){const i=t.compositionSpec,h=c.plots,f={spacing:"content",children:c.children.map((r,g)=>({...r,x:0,y:0,row:Math.floor(g/i.columns),column:g%i.columns}))},{records:n,items:s}=Y(f,h,e),o=n.flatMap((r,g)=>{const b=r.descriptor;return b.topology==="role-title"||b.ids?.length>1?[]:[{...s[g],ownerId:b.id??b.ids[0]}]}),a=E(t,{headerItems:o}),d=O({children:a,columns:i.columns,gap:i.gap}),p=new Map(a.map(r=>[r.id,r])),u=new Map(d.children.map(r=>[r.column,r])),w=H({flow:d,footers:[...u.values()].filter(r=>p.get(r.id).footerBounds!==void 0).map(r=>({id:r.id,column:r.column}))}),v=new Map(w.map(r=>[r.ownerId,r])),l={...d.bounds};for(const r of w){const g=p.get(r.ownerId),b=g.footerBounds,y=r.y-g.plot.height;l.left=Math.min(l.left,r.x+b.left),l.right=Math.max(l.right,r.x+b.right),l.top=Math.min(l.top,y+b.top),l.bottom=Math.max(l.bottom,y+b.bottom)}const m=k({...c,spacing:"canvas",topLane:void 0,children:[{id:"packed",width:l.right-l.left,height:l.bottom-l.top}],columns:1,gap:0,headerLayout:{outer:e.headerLayout.outer},plots:void 0}),x=m.children[0].x-l.left,B=m.children[0].y-l.top;return{...m,spacing:"content",columns:d.columns,rows:Math.ceil(d.children.length/d.columns),gap:i.gap,children:d.children.map(r=>{const g=p.get(r.id),y=t.children[r.id].graphicSpec.objects.canvas.properties,M=v.get(r.id);return{id:r.id,row:r.row,column:r.column,x:x+r.x-g.plot.x,y:B+r.y-g.plot.y,width:y.width,height:y.height,...M===void 0?{}:{footerShift:M.y-r.y-g.plot.height}}})}}function rt(t,c){if(t._assertCompositionProgram("resolveFacetProgramLayout"),t.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");c??=T(t);const e=nt(t),i=t.compositionSpec,h=tt(t,it(t).headers),f=new Map((i.facet.grid?.cells??[]).map(p=>[p.id,p])),n={...i.padding};e.config&&e.config.position!=="top"&&(n[e.config.position]+=e.reservation);const s={children:i.children.map((p,u)=>({...V(p,t.children[p]),value:i.facet.values[u],...f.has(p)?{row:f.get(p).row,column:f.get(p).column}:{}})),spacing:i.spacing,topLane:i.topLane,plots:i.children.map(p=>({id:p,...C(t.children[p])})),columns:i.columns,gap:i.gap,align:i.align,padding:n,titleHeight:e.height,headerLayout:h.headerLayout,sharedLegend:i.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}};let o=i.spacing==="content"?st(t,s,h):k(s);const a=i.children.map(p=>({id:p,...C(t.children[p])}));let d=R({placements:o.children,plots:a});return{layout:o,plot:d}=K(t,{layout:o,plot:d,plots:a,title:e,preparedHeaders:h,prepared:c}),{layout:o,title:e,plot:d,plots:a,preparedHeaders:h}}function at(t,c,e){const i=[],h=c.children.map((n,s)=>({id:n.id,left:n.x+e[s].x,right:n.x+e[s].x+e[s].width,top:n.y+e[s].y,bottom:n.y+e[s].y+e[s].height}));for(const n of c.children){const s=et(t,n.id);for(const o of s){const a=j(`${t.compositionSpec.id}-${n.id}`,o);if(t.graphicSpec.objects[a]===void 0)continue;const d=G(t.graphicSpec,a,t.materializationConfigs.textMetrics);d!==void 0&&i.push({owner:n.id,bounds:d})}}const f=t.graphicSpec.objects[F(t.compositionSpec.id)];for(const n of f?.items??[])n.properties.text!==""&&i.push({owner:"headers",bounds:I(n.properties,t.materializationConfigs.textMetrics)});for(let n=0;n<i.length;n+=1){const s=i[n];if(h.some(o=>o.id!==s.owner&&S(s.bounds,o))||i.slice(0,n).some(o=>o.owner!==s.owner&&S(s.bounds,o.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function dt(t,c){if(c.titles.length===0)return;const e=F(t.compositionSpec.id),i=t.materializationConfigs.textMetrics,h=t.graphicSpec.objects[e].items.slice(c.descriptors.length).map(n=>I(n.properties,i)),f=n=>{if((n.ownerId??n.id)===e)return;const s=n.object.type??n.owner?.type,o=n.object.properties;if(s!=="text"||o?.text===""||o?.opacity===0)return;const a=G(t.graphicSpec,n.id,i);if(a!==void 0&&h.some(d=>S(d,a)))throw new Error("Facet field title overlaps retained chart text.")};P(t.graphicSpec,{enter:f,item:f})}function Lt(t){const c=T(t),{layout:e,title:i,plot:h,plots:f,preparedHeaders:n}=rt(t,c),s=t.graphicSpec.objects.canvas?.properties?.background??"white";let o=U(t);o.graphicSpec.objects.canvas===void 0&&(o=o.createGraphics({id:"canvas",type:"canvas"}));for(const[a,d]of Object.entries({width:e.width,height:e.height,background:s}))o=o.editGraphics({target:"canvas",property:a,value:d});for(const a of e.children){const d=t.children[a.id],p=J(d.graphicSpec,{namespace:`${t.compositionSpec.id}-${a.id}`,x:a.x,y:a.y});if(o=Q(o,p,p.order[0],"canvas"),["plot","content"].includes(e.spacing)){const u=p.order[0],w=j(`${t.compositionSpec.id}-${a.id}`,"plot-background"),v=C(d),l=o.graphicSpec.objects[u].children?.[0];o=o.editGraphics({target:u,property:"background",value:"transparent"}).createGraphics({id:w,type:"rect",parent:u,...l===void 0?{}:{before:l}});for(const[m,x]of Object.entries({...v,stroke:"none",strokeWidth:0,fill:d.graphicSpec.objects.canvas.properties.background??"transparent"}))o=o.editGraphics({target:w,property:m,value:x})}}return o=X(o,e,f,n),o=o.composeFacetGuides({layout:e,plot:h}),e.spacing==="plot"&&at(o,e,f),i.config&&(o=ct(o,h,i)),dt(o,n),Z(o,{layout:e,plots:f,headerId:F(t.compositionSpec.id),titleIds:L}),o._withCanvasConfig({margin:ot,size:{width:"auto",height:"auto"}})}export{Lt as materializeFacetGraphics,It as measureFacetContentChildren,rt as resolveFacetProgramLayout};
@@ -1,3 +1,4 @@
1
+ import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
1
2
  import { cartesianAxisOwners } from "../selectors/axes.js";
2
3
  import { axisGraphicIds } from "./guides/resources.js";
3
4
  import {
@@ -79,7 +80,9 @@ export function resolveMarkGraphicPlacement(program, { data, markType }) {
79
80
  const namespaced = eligible.filter(layer =>
80
81
  `${layer.id}RegressionData` === dataset.id
81
82
  );
82
- const sources = namespaced.length === 1 ? namespaced : eligible;
83
+ const declared = program.semanticSpec.layers.filter(layer =>
84
+ program.markConfigs[layer.id]?.regression?.dataId === dataset.id);
85
+ const sources = declared.length === 1 ? declared : namespaced.length === 1 ? namespaced : eligible;
83
86
  if (sources.length !== 1) {
84
87
  throw new Error(
85
88
  "Regression graphic placement requires one source mark layer."
@@ -121,3 +124,24 @@ export function preserveGraphicPlacement(program, id) {
121
124
  ...(next === undefined ? {} : { before: next })
122
125
  };
123
126
  }
127
+
128
+ export function reconcileRegressionPlacement(program, ownerId) {
129
+ const config = program.markConfigs[ownerId].regression;
130
+ const placement = validateRegressionPlacement(config.placement);
131
+ const children = program.graphicSpec.objects[PLOT_GRAPHIC_ID]?.children;
132
+ if (children === undefined) throw new Error("Regression placement requires the plot container.");
133
+ const owned = [config.bandId, config.lineId].filter(id => id !== undefined && children.includes(id));
134
+ const order = children.filter(id => !owned.includes(id));
135
+ if (placement === "plot-last") {
136
+ const overlay = firstOverlayGuide(program);
137
+ const index = overlay === undefined ? order.length : order.indexOf(overlay);
138
+ order.splice(index < 0 ? order.length : index, 0, ...owned);
139
+ } else {
140
+ const index = order.indexOf(ownerId);
141
+ if (index < 0) throw new Error("Regression placement requires its source mark in the plot.");
142
+ order.splice(index, 1, ...(owned.includes(config.bandId) ? [config.bandId] : []),
143
+ ownerId, ...(owned.includes(config.lineId) ? [config.lineId] : []));
144
+ }
145
+ if (order.every((id, i) => id === children[i])) return program;
146
+ return program.editGraphics({ target: PLOT_GRAPHIC_ID, property: "children", value: order });
147
+ }
@@ -1,3 +1,6 @@
1
+ import { validateFacetAxisPolicy } from "../core/facetAxisPolicy.js";
2
+ import { resolveFacetFamily } from "../grammar/facets/dependencies.js";
3
+ import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
1
4
  import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
2
5
  import { validatePointShape } from "../grammar/pointShapes.js";
3
6
  import {deriveLookupRows,sameLookupValue} from "../grammar/lookup.js";
@@ -215,6 +218,8 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
215
218
  validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
216
219
  }
217
220
  if (program.compositionSpec?.type === "facet") {
221
+ validateFacetAxisPolicy(program.compositionSpec.facet.guides.axes,
222
+ resolveFacetFamily(program.semanticSpec).family, program.compositionSpec.facet.repeat !== undefined);
218
223
  const headers = program.materializationConfigs.facets?.[program.compositionSpec.id]?.headers;
219
224
  if (headers !== undefined) validateFacetHeaderRotations(headers);
220
225
  if (headers?.row?.title !== undefined && program.compositionSpec.facet.grid === undefined) {
@@ -463,6 +468,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
463
468
  }
464
469
  }
465
470
  for (const id of Object.keys(program.markConfigs)) {
471
+ validateRegressionPlacement(program.markConfigs[id].regression?.placement);
466
472
  requestedMarkClip(program.markConfigs[id]);
467
473
  validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
468
474
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Must match package.json; enforced by the snapshot producer-version test.
2
- export const packageVersion = "0.0.22";
2
+ export const packageVersion = "0.0.23";
@@ -100,6 +100,7 @@ export type CurveInterpolation =
100
100
  | "basis"
101
101
  | "cardinal"
102
102
  | "monotone"
103
+ | "monotone-steffen"
103
104
  | "natural";
104
105
  export type DashStyle = "solid" | "dashed" | "dotted" | "dashdot";
105
106
  export type DashPattern = readonly number[];
@@ -168,8 +169,8 @@ export interface EditCompositionLayoutOptions {
168
169
  gap?: number;
169
170
  align?: CompositionAlign;
170
171
  padding?: number | CompositionPadding;
171
- /** Interpret gap between child canvases (default) or inner plot regions. */
172
- spacing?: "canvas" | "plot";
172
+ /** Interpret gap between child canvases (default), inner plots, or retained full content (wrapped facets). */
173
+ spacing?: "canvas" | "plot" | "content";
173
174
  /** Share existing top margin with parent title and headers; requires plot spacing. Stacked restores the default. */
174
175
  topLane?: FacetTopLaneMode;
175
176
  }
@@ -213,7 +214,7 @@ interface StoredFacetLegendIntent {
213
214
  readonly groups: Readonly<Record<string, unknown>>;
214
215
  }
215
216
  export interface FacetGuideOptions {
216
- axes?: "each" | "outer";
217
+ axes?: "each" | "outer" | { x?: "each" | "outer"; y?: "each" | "outer" };
217
218
  legend?: false | "shared" | LegendOptions;
218
219
  }
219
220
  export interface FacetOptions {
@@ -229,8 +230,8 @@ export interface FacetOptions {
229
230
  scales?: FacetScaleResolutions;
230
231
  guides?: FacetGuideOptions;
231
232
  headers?: FacetHeadersOptions;
232
- /** Interpret gap between child canvases (default) or inner plot regions. */
233
- spacing?: "canvas" | "plot";
233
+ /** Interpret gap between child canvases (default), inner plots, or retained full content (wrapped facets). */
234
+ spacing?: "canvas" | "plot" | "content";
234
235
  /** Share the existing top margin rather than adding title/header reservations. Requires plot spacing. */
235
236
  topLane?: FacetTopLaneMode;
236
237
  }
@@ -250,7 +251,7 @@ export interface FacetGridOptions {
250
251
  scales?: FacetScaleResolutions;
251
252
  guides?: FacetGuideOptions;
252
253
  headers?: FacetHeadersOptions;
253
- /** Interpret gap between child canvases (default) or inner plot regions. */
254
+ /** Interpret gap between child canvases (default), inner plots, or retained full content (wrapped facets). */
254
255
  spacing?: "canvas" | "plot";
255
256
  /** Share the existing top margin rather than adding title/header reservations. Requires plot spacing. */
256
257
  topLane?: FacetTopLaneMode;
@@ -267,8 +268,8 @@ export interface RepeatChartsOptions {
267
268
  scales?: FacetScaleResolutions;
268
269
  guides?: FacetGuideOptions;
269
270
  headers?: FacetHeadersOptions;
270
- /** Interpret gap between child canvases (default) or inner plot regions. */
271
- spacing?: "canvas" | "plot";
271
+ /** Interpret gap between child canvases (default), inner plots, or retained full content (wrapped facets). */
272
+ spacing?: "canvas" | "plot" | "content";
272
273
  /** Share the existing top margin rather than adding title/header reservations. Requires plot spacing. */
273
274
  topLane?: FacetTopLaneMode;
274
275
  }
@@ -294,7 +295,7 @@ export interface EditFacetHeadersOptions {
294
295
  color?: string;
295
296
  offset?: number;
296
297
  /** Content follows concrete mark bounds with plot spacing; plot preserves existing placement. */
297
- anchor?: "plot" | "content";
298
+ anchor?: "plot" | "content" | "view";
298
299
  role?: FacetHeaderRole;
299
300
  labelMap?: DisplayLabelMap | "auto";
300
301
  side?: FacetHeaderSide;
@@ -324,7 +325,7 @@ export interface FacetCompositionSpec {
324
325
  readonly type: "facet";
325
326
  readonly children: readonly string[];
326
327
  readonly columns: number;
327
- readonly spacing?: "canvas" | "plot";
328
+ readonly spacing?: "canvas" | "plot" | "content";
328
329
  /** Canonical opt-in intent; stacked is represented by omission. */
329
330
  readonly topLane?: "shared";
330
331
  readonly gap: number;
@@ -354,7 +355,7 @@ export interface FacetCompositionSpec {
354
355
  };
355
356
  readonly scales: Readonly<Required<FacetScaleResolutions>>;
356
357
  readonly guides: {
357
- readonly axes: "each" | "outer";
358
+ readonly axes: "each" | "outer" | { readonly x: "each" | "outer"; readonly y: "each" | "outer" };
358
359
  readonly legend: false | "shared" | Readonly<LegendOptions> | StoredFacetLegendIntent;
359
360
  };
360
361
  };
@@ -735,6 +736,7 @@ export type TimeUnit =
735
736
  | "week"
736
737
  | "weekday";
737
738
  type DatasetTimeUnitTransformBase = {
739
+ readonly missing?: "error" | "null";
738
740
  readonly type: "timeUnit";
739
741
  readonly field: string;
740
742
  readonly temporalUnit?: TemporalInputUnit;
@@ -2496,6 +2498,8 @@ export interface ECDFDataOptions {
2496
2498
  }
2497
2499
 
2498
2500
  type TimeUnitDataBaseOptions = {
2501
+ /** Preserve null or absent inputs as null buckets; defaults to strict errors. */
2502
+ missing?: "error" | "null";
2499
2503
  id: string;
2500
2504
  source?: string;
2501
2505
  field: string;
@@ -4800,6 +4804,7 @@ export interface EditRegressionStatisticsDataOptions {
4800
4804
  }
4801
4805
 
4802
4806
  type RegressionCommonOptions = {
4807
+ placement?: "source" | "plot-last";
4803
4808
  target?: string;
4804
4809
  x?: string;
4805
4810
  y?: string;
@@ -4825,6 +4830,7 @@ export type RegressionOptions = RegressionCommonOptions & (
4825
4830
  );
4826
4831
 
4827
4832
  export interface EditRegressionOptions {
4833
+ placement?: "source" | "plot-last";
4828
4834
  target?: string;
4829
4835
  data?: string;
4830
4836
  x?: string;