ggaction 0.0.23 → 0.0.24

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.
@@ -1,3 +1,4 @@
1
+ import { validateCompositionSpacing } from "../core/facetGap.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
 
3
4
  const DIRECTIONS = ["horizontal", "vertical"];
@@ -11,12 +12,7 @@ export const DEFAULT_COMPOSITION_LAYOUT = cloneAndFreeze({
11
12
  padding: { top: 0, right: 0, bottom: 0, left: 0 }
12
13
  });
13
14
 
14
- export function validateCompositionSpacing(value, label) {
15
- if (!Number.isFinite(value) || value < 0) {
16
- throw new RangeError(`${label} must be a non-negative finite number.`);
17
- }
18
- return value;
19
- }
15
+ export { validateCompositionSpacing } from "../core/facetGap.js";
20
16
 
21
17
  function validatePaddingBase(base) {
22
18
  if (!isPlainObject(base)) {
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { validateCompositionSpacing } from "./composition.js";
2
+ import { normalizeFacetGap, facetGapAxis, assertFacetGapFinite } from "../core/facetGap.js";
3
3
 
4
4
  /** Pack ordered full content bounds without imposing common plot origins. */
5
5
  export function resolveFacetContentFlow({ children, columns, gap = 16 } = {}) {
@@ -11,7 +11,8 @@ export function resolveFacetContentFlow({ children, columns, gap = 16 } = {}) {
11
11
  throw new RangeError("Facet content flow columns must be a positive integer.");
12
12
  }
13
13
  columns = Math.min(columns, children.length);
14
- validateCompositionSpacing(gap, "Facet content flow gap");
14
+ gap = normalizeFacetGap(gap, "Facet content flow gap");
15
+ const gapX = facetGapAxis(gap, "x"), gapY = facetGapAxis(gap, "y");
15
16
  const ids = new Set();
16
17
  const rightExtents = Array(columns).fill(0);
17
18
  const bottomExtents = Array(Math.ceil(children.length / columns)).fill(0);
@@ -35,16 +36,23 @@ export function resolveFacetContentFlow({ children, columns, gap = 16 } = {}) {
35
36
  const columnOrigins = Array(columns).fill(0);
36
37
  let x = 0;
37
38
  const placed = cells.map(cell => {
38
- x = cell.column === 0 ? 0 : x + rightExtents[cell.column - 1] + gap +
39
+ x = cell.column === 0 ? 0 : x + rightExtents[cell.column - 1] + gapX +
39
40
  Math.max(0, Math.ceil(-cell.bounds.left));
40
41
  if (cell.row > 0) {
41
- columnOrigins[cell.column] += bottomExtents[cell.row - 1] + gap +
42
+ columnOrigins[cell.column] += bottomExtents[cell.row - 1] + gapY +
42
43
  Math.max(0, Math.ceil(-cell.bounds.top));
43
44
  }
44
45
  const y = columnOrigins[cell.column];
45
- if (![x, y, x + cell.bounds.left, x + cell.bounds.right,
46
- y + cell.bounds.top, y + cell.bounds.bottom].every(Number.isFinite)) {
47
- throw new RangeError("Facet content flow exceeds finite layout bounds.");
46
+ if (typeof gap === "number") {
47
+ if (![x, y, x + cell.bounds.left, x + cell.bounds.right,
48
+ y + cell.bounds.top, y + cell.bounds.bottom].every(Number.isFinite)) {
49
+ throw new RangeError("Facet content flow exceeds finite layout bounds.");
50
+ }
51
+ } else {
52
+ assertFacetGapFinite(gap, "x", [x, x + cell.bounds.left, x + cell.bounds.right],
53
+ "Facet content flow exceeds finite layout bounds.");
54
+ assertFacetGapFinite(gap, "y", [y, y + cell.bounds.top, y + cell.bounds.bottom],
55
+ "Facet content flow exceeds finite layout bounds.");
48
56
  }
49
57
  return {
50
58
  id: cell.id, column: cell.column, row: cell.row, x, y,
@@ -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 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
+ import{cloneAndFreeze as at,isPlainObject as P}from"../core/immutable.js";import{normalizeFacetGap as it,facetGapAxis as V,assertFacetGapFinite as lt}from"../core/facetGap.js";import{normalizeFacetTopLane as st}from"./facetTopLane.js";import{annotateError as tt}from"../core/diagnostics.js";import{DEFAULT_COMPOSITION_LAYOUT as W,normalizeCompositionAlign as ct,normalizeCompositionChildren as ut,normalizeCompositionPadding as mt,validateCompositionSpacing as M}from"./composition.js";const ft=18,ht=132,dt=0,pt=Object.freeze(["right","left","top","bottom"]);function gt(r,l){const p=r??l;if(!Number.isInteger(p)||p<=0)throw new RangeError("Facet columns must be a positive integer.");return Math.min(p,l)}function wt(r,l){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 g=r.map(o=>`${o.row}:${o.column}`);if(new Set(g).size!==g.length)throw new Error("Facet grid children must occupy unique row and column coordinates.");const y=Math.max(...r.map(o=>o.column))+1;if(l!==void 0&&l!==y)throw new RangeError("Facet grid columns must match the declared column domain.");return{columns:y,rows:Math.max(...r.map(o=>o.row))+1}}function et(r,l){return l==="start"?0:l==="end"?r:r/2}const _=Object.freeze({top:0,right:0,bottom:0,left:0});function yt(r,l,p){if(r===void 0)return{outer:_,cellRows:{top:Array(l).fill(0),bottom:Array(l).fill(0)}};if(!P(r)||Object.keys(r).some(o=>!["outer","cellRows","cellColumns"].includes(o)))throw new TypeError("Facet headerLayout must contain outer and cellRows.");const g={..._,...r.outer??{}};if(!P(r.outer??{})||Object.keys(g).some(o=>!Object.hasOwn(_,o)))throw new TypeError("Facet headerLayout.outer must contain side reservations.");for(const[o,m]of Object.entries(g))M(m,`Facet headerLayout.outer.${o}`);const y={outer:g};for(const[o,m,L,D]of[["cellRows",["top","bottom"],l,"row"],["cellColumns",["left","right"],p,"column"]]){if(o==="cellColumns"&&r[o]===void 0)continue;const O=o==="cellRows"?r[o]??{}:r[o];if(!P(O)||Object.keys(O).some(s=>!m.includes(s)))throw new TypeError(`Facet headerLayout.${o} must contain ${m[0]} and ${m[1]} arrays.`);y[o]=Object.fromEntries(m.map(s=>{const x=O[s]??Array(L).fill(0);if(!Array.isArray(x)||x.length!==L)throw new RangeError(`Facet headerLayout.${o}.${s} must match ${D} count.`);return x.forEach((c,H)=>M(c,`Facet headerLayout.${o}.${s}[${H}]`)),[s,[...x]]}))}return y}function Ct({children:r,columns:l,gap:p=W.gap,align:g=W.align,padding:y=W.padding,titleHeight:o=0,sharedLegend:m=!1,sharedLegendGap:L=ft,sharedLegendWidth:D=ht,sharedLegendHeight:O=dt,sharedLegendPosition:s="right",headerLayout:x,spacing:c="canvas",topLane:H,plots:$}={}){if(!["canvas","plot","content"].includes(c))throw new Error(`Unknown facet spacing "${c}".`);const G=st(H,c),N=new Map(($??[]).map(e=>[e.id,e])),Y=r;let f=_;if(c==="plot"){if(!Array.isArray($)||$.length!==r?.length||N.size!==$.length)throw new Error("Plot spacing requires one plot bounds record per child.");f={..._},r=r.map(e=>{const t=N.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 f.left=Math.max(f.left,t.x),f.top=Math.max(f.top,t.y),f.right=Math.max(f.right,e.width-t.x-t.width),f.bottom=Math.max(f.bottom,e.height-t.y-t.height),{...e,width:t.width,height:t.height}})}const ot=r?.map(e=>e?.value),b=wt(r??[],l);if(c==="content"&&b!==void 0)throw new Error("Content flow requires a wrapped facet, not a keyed facet grid.");const I=ut(r?.map(({value:e,row:t,column:i,...a})=>a)),u=b?.columns??gt(l,I.length),h=it(p,"Facet gap"),X=V(h,"x"),Z=V(h,"y"),k=ct(g),q=mt(y),E=Object.fromEntries(Object.keys(q).map(e=>[e,q[e]+f[e]])),j=M(o,"Facet title height"),B=G===void 0?j:0;if(typeof m!="boolean")throw new TypeError("Facet sharedLegend must be a boolean.");const S=M(L,"Facet shared legend gap"),J=M(D,"Facet shared legend width"),K=M(O,"Facet shared legend height");if(!pt.includes(s))throw new Error(`Unknown facet shared legend position "${s}".`);const F=["right","left"].includes(s),Q=["left","top"].includes(s),A=m?S+(F?J:K):0,C=b?.rows??Math.ceil(I.length/u);let n=yt(x,C,u);if(c==="plot"){const e={x:0,y:0};for(const[t,i,a]of[["y",n.cellRows.bottom,n.cellRows.top],["x",n.cellColumns?.right??[],n.cellColumns?.left??[]]])for(let d=1;d<i.length;d+=1)e[t]=Math.max(e[t],i[d-1]+a[d]);if(typeof h=="number"){const t=Math.max(e.y,e.x);if(t>h)throw tt(new Error("Plot spacing gap is too small for the facet headers."),{code:"invalid-value",optionPath:"gap",actual:h,limit:t})}else for(const t of["x","y"])if(e[t]>h[t])throw tt(new Error("Plot spacing gap is too small for the facet headers."),{code:"invalid-value",optionPath:`gap.${t}`,actual:h[t],limit:e[t]});n={outer:{top:n.outer.top+n.cellRows.top[0],bottom:n.outer.bottom+n.cellRows.bottom.at(-1),left:n.outer.left+(n.cellColumns?.left[0]??0),right:n.outer.right+(n.cellColumns?.right.at(-1)??0)},cellRows:{top:Array(C).fill(0),bottom:Array(C).fill(0)}},G!==void 0&&(n.outer.top=0)}const T=n.cellColumns??{left:Array(u).fill(0),right:Array(u).fill(0)},v=Array(u).fill(-1/0),R=Array(C).fill(-1/0);I.forEach((e,t)=>{const i=b===void 0?t%u:r[t].column,a=b===void 0?Math.floor(t/u):r[t].row;v[i]=Math.max(v[i],e.width),R[a]=Math.max(R[a],e.height)});const rt=v.map((e,t)=>E.left+n.outer.left+v.slice(0,t).reduce((i,a,d)=>i+T.left[d]+a+T.right[d],0)+X*t+T.left[t]),nt=R.map((e,t)=>B+E.top+n.outer.top+R.slice(0,t).reduce((i,a,d)=>i+n.cellRows.top[d]+a+n.cellRows.bottom[d],0)+Z*t+n.cellRows.top[t]),w=I.map((e,t)=>{const i=b===void 0?t%u:r[t].column,a=b===void 0?Math.floor(t/u):r[t].row;return{id:e.id,value:ot[t],column:i,row:a,x:rt[i]+(Q&&F?A:0)+et(v[i]-e.width,k),y:nt[a]+(Q&&!F?A:0)+et(R[a]-e.height,k),width:e.width,height:e.height}}),z=E.left+n.outer.left+v.reduce((e,t)=>e+t,0)+T.left.reduce((e,t)=>e+t,0)+T.right.reduce((e,t)=>e+t,0)+X*Math.max(0,u-1)+n.outer.right+E.right,U=B+E.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)+Z*Math.max(0,C-1)+n.outer.bottom+E.bottom;if(c==="plot")for(let e=0;e<w.length;e+=1){const t=N.get(w[e].id);w[e].x-=t.x,w[e].y-=t.y,w[e].width=Y[e].width,w[e].height=Y[e].height}if(typeof h!="number")for(const[e,t,i]of[["x",z+(F?A:0),"width"],["y",U+(F?0:A),"height"]])lt(h,e,[t,...w.flatMap(a=>[a[e],a[i],a[e]+a[i]])],"Facet layout exceeds finite bounds.");return at({...c!=="canvas"?{spacing:c}:{},...G===void 0?{}:{topLane:G},columns:u,rows:C,gap:h,align:k,padding:q,titleHeight:j,headerLayout:n,gridWidth:z,width:z+(F?A:0),height:U+(F?0:A),children:w,...m?{legend:{position:s,x:s==="right"?z+S:0,y:s==="bottom"?U+S:s==="top"?j:j+E.top+30,width:J,height:K}}:{}})}export{ft as DEFAULT_FACET_LEGEND_GAP,dt as DEFAULT_FACET_LEGEND_HEIGHT,ht as DEFAULT_FACET_LEGEND_WIDTH,Ct as resolveFacetLayout};
@@ -21,7 +21,7 @@ export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
21
21
  style: config.subtitleStyle, profile
22
22
  });
23
23
  const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
24
- const subtitleLineHeight = config.lineHeight ??
24
+ const subtitleLineHeight = config.subtitleLineHeight ?? config.lineHeight ??
25
25
  config.subtitleStyle.fontSize * 1.2;
26
26
  const titleCenters = lineCenters(
27
27
  titleLines,
@@ -1 +1 @@
1
- import{resetLegendClipGraphics as m,hasLegendClipGraphics as c}from"../legendClip.js";import{localLegendPosition as g}from"../legendGroups.js";import{needsLegendAxisReflow as p,needsSideLegendAxisReflow as h}from"./resources.js";import{getErrorDetails as C}from"../../core/diagnostics.js";import{planCanvasRematerialization as L}from"../layout.js";import{applyMaterializationPlan as v}from"../planner.js";import{assertGuideCollisionBlocks as G}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as y}from"./resources.js";import{hasPlotUnderlays as u}from"../plotDrawOrder.js";import{resolveGuideCollisionBlocks as A}from"./resources.js";function z(i,l){if(i.context.deferGuideLayoutValidation===!0)return l(i);const a=i.materializationConfigs.canvas?.plot;let n=i.materializationConfigs.canvas?.margin,e;for(let s=0;;s+=1)try{let t=m(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=v(t,L(t))),e=l(t),c(i)&&!c(e)&&Object.values(e.guideConfigs.legend??{}).some(o=>o.clipHeight!==void 0)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&h(i,e)&&(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&&p(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()),(u(i)||u(e))&&(e=e.rematerializePlotDrawOrder());break}catch(t){const o=C(t)?.canvasOverflow;if(a===void 0||o===void 0||s>=16)throw t;n=Object.fromEntries(Object.keys(n).map(d=>[d,o[d]>0?Math.ceil((n[d]+o[d])*4)/4:n[d]]))}const{deferGuideLayoutValidation:w,...f}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(f.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const r=e._clone({context:f});return G(y(r.graphicSpec,r.guideConfigs,r.titleConfig,r.materializationConfigs.textMetrics)),r}function B(i){return function(l){return z(this,a=>i.call(a,l))}}export{A as resolveGuideCollisionBlocks,z as withGuideLayoutTransaction,B as withGuideLayoutValidation};
1
+ import{resetLegendClipGraphics as p,hasLegendClipGraphics as u}from"../legendClip.js";import{localLegendPosition as m}from"../legendGroups.js";import{needsLegendAxisReflow as h,needsSideLegendAxisReflow as x}from"./resources.js";import{getErrorDetails as v}from"../../core/diagnostics.js";import{planCanvasRematerialization as C}from"../layout.js";import{applyMaterializationPlan as L}from"../planner.js";import{assertGuideCollisionBlocks as G}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as w}from"./resources.js";import{hasPlotUnderlays as g}from"../plotDrawOrder.js";import{resolveGuideCollisionBlocks as M}from"./resources.js";function _(i,l,a){const t=a(i._withContext({pendingRelativeTitleAxis:l})),{pendingRelativeTitleAxis:e,...s}=t.context;return Object.hasOwn(i.context,"pendingRelativeTitleAxis")&&(s.pendingRelativeTitleAxis=i.context.pendingRelativeTitleAxis),t._clone({context:s})}function y(i,l){if(i.context.deferGuideLayoutValidation===!0)return l(i);const a=i.materializationConfigs.canvas?.plot;let t=i.materializationConfigs.canvas?.margin,e;for(let f=0;;f+=1)try{let n=p(i)._withContext({deferGuideLayoutValidation:!0});f>0&&(n=n.editGraphics({target:"canvas",property:"width",value:a.width+t.left+t.right}).editGraphics({target:"canvas",property:"height",value:a.height+t.top+t.bottom})._withCanvasConfig({...n.materializationConfigs.canvas,margin:t}),n=L(n,C(n))),e=l(n),u(i)&&!u(e)&&Object.values(e.guideConfigs.legend??{}).some(o=>o.clipHeight!==void 0)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&x(i,e)&&(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&&h(i,e)&&(e=e.rematerializeHorizontalLegendLane()),e.compositionSpec===void 0&&(Object.keys(e.guideConfigs.legendGroups??{}).length>0||Object.values(e.guideConfigs.legend??{}).some(o=>m(o)!==void 0))&&(e=e.rematerializeLegendGroups()),a!==void 0&&e.titleConfig!==void 0&&(e=e.rematerializeTitle()),(g(i)||g(e))&&(e=e.rematerializePlotDrawOrder());break}catch(n){const o=v(n)?.canvasOverflow;if(a===void 0||o===void 0||f>=16)throw n;t=Object.fromEntries(Object.keys(t).map(d=>[d,o[d]>0?Math.ceil((t[d]+o[d])*4)/4:t[d]]))}const{deferGuideLayoutValidation:s,...c}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(c.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const r=e._clone({context:c});return G(w(r.graphicSpec,r.guideConfigs,r.titleConfig,r.materializationConfigs.textMetrics)),r}function k(i){return function(l){return y(this,a=>i.call(a,l))}}export{M as resolveGuideCollisionBlocks,y as withGuideLayoutTransaction,k as withGuideLayoutValidation,_ as withPendingRelativeAxisTitle};
@@ -1 +1 @@
1
- import{axisGraphicIds as k}from"../../selectors/axes.js";import{localLegendPosition as z,localLegendPlacement as C}from"../legendGroups.js";import{resolveConcreteGraphicBounds as B,unionConcreteGraphicBounds as x}from"../../grammar/schemas/graphicBounds.js";function T(e,i,n,s){const t=[],r=(o,a,u,l)=>{if(u===void 0)return;const b=l.filter(O=>e.objects[O]!==void 0),m=x(e,b,s);m!==void 0&&t.push({id:o,kind:a,position:u,bounds:m})};r("chart title","title",n?.position,["chartTitle","chartSubtitle"]);for(const[o,a]of Object.entries(i.axis??{})){const u=["line","ticks","labels","title"].map(b=>a[b]?.position).find(Boolean),l=["x","y"].includes(o)?o:["top","bottom"].includes(u)?"x":["left","right"].includes(u)?"y":void 0;if(l!==void 0)for(const[b,m]of["line","ticks","labels","title"].entries()){const O=k(l,o)[b],I=t.length;if(r(`${o}-axis ${m}`,"axis",a[m]?.position,[O]),t.length===I)continue;const h=t.at(-1);if(h.axisId=o,h.namedAxis=o!==l,["labels","title"].includes(m)){const G=e.objects[O];h.textBounds=G.items?.map(v=>B(e,v.id,s))??[h.bounds]}}}const c=$(),d=i.legend??{},f=c.find(o=>o.family==="categorical"&&d[o.kind]!==void 0),g=f!==void 0&&d.size!==void 0&&d[f.kind].target===d.size.target&&z(d[f.kind])===void 0&&z(d.size)===void 0&&(d.size.inheritPosition!==!1||d[f.kind].position===d.size.position),p=i.legendGroups??{};for(const[o,a]of Object.entries(p)){const u=c.filter(l=>d[l.kind]?.target===o&&z(d[l.kind])===void 0).flatMap(l=>l.graphicIds);r(`${o} legend group`,"legend",a.placement.position??"free",u)}for(const o of c){const a=d[o.kind];if(p[a?.target]!==void 0&&z(a)===void 0||a===void 0||g&&o.kind==="size")continue;const u=g&&o===f?[...o.graphicIds,...c.find(l=>l.kind==="size").graphicIds]:o.graphicIds;r(`${o.kind}${g&&o===f?"+size":""} legend`,"legend",z(a)??a.position,u)}return t}const L=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),j=Object.freeze({colorSize:Object.freeze({semanticKind:"colorSize",family:"sampled",rematerializeOp:void 0,graphicIds:Object.freeze(["colorSizeLegendBackground","colorSizeLegendSymbols","colorSizeLegendLabels","colorSizeLegendTitle"])}),series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function K(e){return w(e).graphicIds}function w(e){const i=j[e];if(i===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return i}function $(){return Object.entries(j).map(([e,i])=>({kind:e,...i}))}function R(e){return[...new Set(e.flatMap(K))]}function y(e){const i=T(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(n=>n.kind==="axis");for(const n of["theta","radius"]){if(e.guideConfigs.axis?.[n]===void 0)continue;const s=k(n).slice(2).filter(r=>e.graphicSpec.objects[r]!==void 0),t=x(e.graphicSpec,s,e.materializationConfigs.textMetrics);if(t!==void 0)for(const r of["left","right","top","bottom"])i.push({id:`${n}-axis text`,kind:"axis",position:r,bounds:t})}return i}function A(e,i,n){if(i.inferredOffset!==!0||!["top","bottom","left","right"].includes(i.position)||i.layout==="legacy-bottom")return i.offset;const s=y(e).filter(r=>r.position===i.position),t=Math.max(0,...s.map(r=>({bottom:r.bounds.bottom-n.y-n.height,top:n.y-r.bounds.top,left:n.x-r.bounds.left,right:r.bounds.right-n.x-n.width})[i.position]));return i.offset+t}function S(e,i,n){if(i!=="size"||n.inheritPosition===!1)return n;const s=["series","color","stroke"].map(t=>e.guideConfigs.legend?.[t]).find(t=>t?.target===n.target);return s===void 0?n:{...n,position:s.position,offset:s.offset,inferredOffset:s.inferredOffset}}function M(e,i){return[e,i].some(s=>Object.entries(s.guideConfigs.legend??{}).some(([t,r])=>{const c=S(s,t,r);return c.inferredOffset===!0&&["top","bottom"].includes(c.position)&&c.layout!=="legacy-bottom"})||Object.values(s.guideConfigs.legendGroups??{}).some(t=>t.inferredOffset===!0&&["top","bottom"].includes(t.placement.position)))&&[e,i].some(s=>y(s).some(t=>["top","bottom"].includes(t.position)))}function N(e,i){const n=new Set;for(const t of[e,i])for(const[r,c]of Object.entries(t.guideConfigs.legend??{})){const d=C(c),f=d===void 0?t.guideConfigs.legendGroups?.[c.target]:void 0,g=S(t,r,c),p=d??f?.placement??g;(d?.inferredOffset??f?.inferredOffset??g.inferredOffset)===!0&&p.at===void 0&&["left","right"].includes(p.position)&&g.layout!=="legacy-bottom"&&n.add(p.position)}const s=t=>JSON.stringify(y(t).filter(r=>n.has(r.position)));return n.size>0&&s(e)!==s(i)}export{R as allLegendGraphicIds,k as axisGraphicIds,S as effectiveLegendOffsetConfig,K as legendGraphicIds,$ as legendResourcePolicies,w as legendResourcePolicy,M as needsLegendAxisReflow,N as needsSideLegendAxisReflow,T as resolveGuideCollisionBlocks,A as resolveLegendAxisOffset};
1
+ import{axisGraphicIds as k,selectCartesianAxis as v}from"../../selectors/axes.js";import{localLegendPosition as z,localLegendPlacement as T}from"../legendGroups.js";import{resolveConcreteGraphicBounds as B,unionConcreteGraphicBounds as y}from"../../grammar/schemas/graphicBounds.js";function W(e,i,n){const o=e.context.pendingMaterializationPlan;if(o===void 0)return!1;const t=`edit${i.toUpperCase()}AxisTitle`;return o.steps.slice(o.next).some(r=>r.op===t&&v(e,i,r.args).id===n)}function K(e,i,n,o){const t=[],r=(s,a,u,l)=>{if(u===void 0)return;const b=l.filter(O=>e.objects[O]!==void 0),m=y(e,b,o);m!==void 0&&t.push({id:s,kind:a,position:u,bounds:m})};r("chart title","title",n?.position,["chartTitle","chartSubtitle"]);for(const[s,a]of Object.entries(i.axis??{})){const u=["line","ticks","labels","title"].map(b=>a[b]?.position).find(Boolean),l=["x","y"].includes(s)?s:["top","bottom"].includes(u)?"x":["left","right"].includes(u)?"y":void 0;if(l!==void 0)for(const[b,m]of["line","ticks","labels","title"].entries()){const O=k(l,s)[b],I=t.length;if(r(`${s}-axis ${m}`,"axis",a[m]?.position,[O]),t.length===I)continue;const h=t.at(-1);if(h.axisId=s,h.namedAxis=s!==l,["labels","title"].includes(m)){const G=e.objects[O];h.textBounds=G.items?.map(C=>B(e,C.id,o))??[h.bounds]}}}const c=w(),d=i.legend??{},f=c.find(s=>s.family==="categorical"&&d[s.kind]!==void 0),g=f!==void 0&&d.size!==void 0&&d[f.kind].target===d.size.target&&z(d[f.kind])===void 0&&z(d.size)===void 0&&(d.size.inheritPosition!==!1||d[f.kind].position===d.size.position),p=i.legendGroups??{};for(const[s,a]of Object.entries(p)){const u=c.filter(l=>d[l.kind]?.target===s&&z(d[l.kind])===void 0).flatMap(l=>l.graphicIds);r(`${s} legend group`,"legend",a.placement.position??"free",u)}for(const s of c){const a=d[s.kind];if(p[a?.target]!==void 0&&z(a)===void 0||a===void 0||g&&s.kind==="size")continue;const u=g&&s===f?[...s.graphicIds,...c.find(l=>l.kind==="size").graphicIds]:s.graphicIds;r(`${s.kind}${g&&s===f?"+size":""} legend`,"legend",z(a)??a.position,u)}return t}const L=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),j=Object.freeze({colorSize:Object.freeze({semanticKind:"colorSize",family:"sampled",rematerializeOp:void 0,graphicIds:Object.freeze(["colorSizeLegendBackground","colorSizeLegendSymbols","colorSizeLegendLabels","colorSizeLegendTitle"])}),series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function P(e){return $(e).graphicIds}function $(e){const i=j[e];if(i===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return i}function w(){return Object.entries(j).map(([e,i])=>({kind:e,...i}))}function M(e){return[...new Set(e.flatMap(P))]}function x(e){const i=K(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(n=>n.kind==="axis");for(const n of["theta","radius"]){if(e.guideConfigs.axis?.[n]===void 0)continue;const o=k(n).slice(2).filter(r=>e.graphicSpec.objects[r]!==void 0),t=y(e.graphicSpec,o,e.materializationConfigs.textMetrics);if(t!==void 0)for(const r of["left","right","top","bottom"])i.push({id:`${n}-axis text`,kind:"axis",position:r,bounds:t})}return i}function N(e,i,n){if(i.inferredOffset!==!0||!["top","bottom","left","right"].includes(i.position)||i.layout==="legacy-bottom")return i.offset;const o=x(e).filter(r=>r.position===i.position),t=Math.max(0,...o.map(r=>({bottom:r.bounds.bottom-n.y-n.height,top:n.y-r.bounds.top,left:n.x-r.bounds.left,right:r.bounds.right-n.x-n.width})[i.position]));return i.offset+t}function S(e,i,n){if(i!=="size"||n.inheritPosition===!1)return n;const o=["series","color","stroke"].map(t=>e.guideConfigs.legend?.[t]).find(t=>t?.target===n.target);return o===void 0?n:{...n,position:o.position,offset:o.offset,inferredOffset:o.inferredOffset}}function U(e,i){return[e,i].some(o=>Object.entries(o.guideConfigs.legend??{}).some(([t,r])=>{const c=S(o,t,r);return c.inferredOffset===!0&&["top","bottom"].includes(c.position)&&c.layout!=="legacy-bottom"})||Object.values(o.guideConfigs.legendGroups??{}).some(t=>t.inferredOffset===!0&&["top","bottom"].includes(t.placement.position)))&&[e,i].some(o=>x(o).some(t=>["top","bottom"].includes(t.position)))}function _(e,i){const n=new Set;for(const t of[e,i])for(const[r,c]of Object.entries(t.guideConfigs.legend??{})){const d=T(c),f=d===void 0?t.guideConfigs.legendGroups?.[c.target]:void 0,g=S(t,r,c),p=d??f?.placement??g;(d?.inferredOffset??f?.inferredOffset??g.inferredOffset)===!0&&p.at===void 0&&["left","right"].includes(p.position)&&g.layout!=="legacy-bottom"&&n.add(p.position)}const o=t=>JSON.stringify(x(t).filter(r=>n.has(r.position)));return n.size>0&&o(e)!==o(i)}export{M as allLegendGraphicIds,k as axisGraphicIds,S as effectiveLegendOffsetConfig,W as hasPendingAxisTitleRematerialization,P as legendGraphicIds,w as legendResourcePolicies,$ as legendResourcePolicy,U as needsLegendAxisReflow,_ as needsSideLegendAxisReflow,K as resolveGuideCollisionBlocks,N as resolveLegendAxisOffset};
@@ -1,3 +1,5 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+
1
3
  const STAGES = Object.freeze([
2
4
  "scales",
3
5
  "marks",
@@ -116,16 +118,29 @@ export function buildMaterializationPlan(stages = {}) {
116
118
 
117
119
  export function applyMaterializationPlan(program, plan) {
118
120
  const steps = deduplicateSteps(normalizeSteps(plan));
121
+ const ownedSteps = typeof program?._withContext === "function" ? cloneAndFreeze(steps) : steps;
119
122
  let next = program;
120
- for (const step of steps) {
123
+ for (const [index, step] of steps.entries()) {
121
124
  if (typeof next?.[step.op] !== "function") {
122
125
  throw new Error(
123
126
  `Materialization operation "${step.op}" is not available on the program.`
124
127
  );
125
128
  }
126
- next = step.args === undefined
127
- ? next[step.op]()
128
- : next[step.op](step.args);
129
+ const previous = next;
130
+ // Only execution context exposes the remaining normalized plan. Domain
131
+ // owners decide which pending sibling operations affect their preflight.
132
+ const scoped = typeof previous._withContext === "function";
133
+ const current = scoped ? previous._withContext({ pendingMaterializationPlan: {
134
+ steps: ownedSteps, next: index + 1
135
+ } }) : previous;
136
+ next = step.args === undefined ? current[step.op]() : current[step.op](step.args);
137
+ if (scoped) {
138
+ const { pendingMaterializationPlan: _pending, ...context } = next.context;
139
+ if (Object.hasOwn(previous.context, "pendingMaterializationPlan")) {
140
+ context.pendingMaterializationPlan = previous.context.pendingMaterializationPlan;
141
+ }
142
+ next = next._clone({ context });
143
+ }
129
144
  }
130
145
  return next;
131
146
  }
@@ -30,7 +30,8 @@ import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
30
30
  import { validateGraphicItemLimit } from "../core/validation.js";
31
31
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
32
32
  import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
33
- import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
33
+ import { validateAxisLabelFlushThreshold, validateAxisTextPlacement,
34
+ axisLabelHasText } from "../grammar/axisTextPlacement.js";
34
35
  import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
35
36
  import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
36
37
  import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
@@ -236,6 +237,23 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
236
237
  }
237
238
  const cartesianOwners = cartesianAxisOwners(program);
238
239
  for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
240
+ if (isPlainObject(axis?.title?.offset)) {
241
+ if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Relative title offset requires a Cartesian axis owner.", path);
242
+ validateAxisTextPlacement(axis.title, { title: true });
243
+ if (axis.title.inferredOffset === true) throw invalidSnapshot("Relative title offset cannot be inferred.", path);
244
+ const owner = cartesianOwners.find(owner => owner.id === id);
245
+ const labels = program.graphicSpec.objects[axisGraphicIds(owner.channel, id)[2]];
246
+ // Composition parents retain source intent, not local unit graphics.
247
+ // Their retained unit/children are validated through the same unit rule.
248
+ if (axis.labels?.position !== axis.title.position || (program.compositionSpec === undefined &&
249
+ !(labels?.items ?? (labels ? [labels] : [])).some(item => axisLabelHasText(item.properties)))) {
250
+ throw invalidSnapshot("Relative title offset requires nonempty same-side labels.", path);
251
+ }
252
+ }
253
+ if (axis?.labels?.flushThreshold !== undefined) {
254
+ if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Label flushThreshold requires a Cartesian axis owner.", path);
255
+ validateAxisLabelFlushThreshold(axis.labels.flushThreshold);
256
+ }
239
257
  for (const config of [axis?.ticks, axis?.labels]) {
240
258
  if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
241
259
  if (!cartesianOwners.some(owner => owner.id === id)) {
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.23";
2
+ export const packageVersion = "0.0.24";
package/types/basic.d.ts CHANGED
@@ -10,11 +10,13 @@ export type {
10
10
  CartesianGuideDashMap,
11
11
  AxisTicksAndLabelsOptions,
12
12
  AxisTitleOptions,
13
+ AxisTitleOffset,
13
14
  DisplayLabelMap,
14
15
  DisplayLabelOptions,
15
16
  ApplyThemeOptions,
16
17
  BarWidthOptions,
17
18
  BasicSeriesLayoutOptions,
19
+ Bin2DAssignment,
18
20
  Bin2DDataOptions,
19
21
  CanvasOptions,
20
22
  ColorEncodingOptions,
package/types/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export type {
27
27
  AxisTicksAndLabelsOptions,
28
28
  AxisTickStyleOptions,
29
29
  AxisTitleOptions,
30
+ AxisTitleOffset,
30
31
  AxisValue,
31
32
  DisplayLabelMap,
32
33
  DisplayLabelOptions,
@@ -36,6 +37,7 @@ export type {
36
37
  BinnedHeatmapColorOptions,
37
38
  BinnedHeatmapOptions,
38
39
  BinnedHeatmapPositionChannel,
40
+ Bin2DAssignment,
39
41
  Bin2DCounts,
40
42
  Bin2DDataOptions,
41
43
  Bin2DExtent,
@@ -287,6 +289,7 @@ export type {
287
289
  FieldType,
288
290
  FillPaint,
289
291
  FacetCompositionSpec,
292
+ FacetGap,
290
293
  FacetGuideOptions,
291
294
  FacetOptions,
292
295
  FacetTopLaneMode,
@@ -164,9 +164,11 @@ export interface CompositionOptions {
164
164
  align?: CompositionAlign;
165
165
  padding?: number | CompositionPadding;
166
166
  }
167
+ /** Facets may set independent axis gaps; concat continues to require a number. */
168
+ export type FacetGap = number | Readonly<{ x: number; y: number }>;
167
169
  export interface EditCompositionLayoutOptions {
168
170
  columns?: number;
169
- gap?: number;
171
+ gap?: FacetGap;
170
172
  align?: CompositionAlign;
171
173
  padding?: number | CompositionPadding;
172
174
  /** Interpret gap between child canvases (default), inner plots, or retained full content (wrapped facets). */
@@ -224,7 +226,7 @@ export interface FacetOptions {
224
226
  data?: string;
225
227
  values?: readonly DatasetScalar[];
226
228
  columns?: number;
227
- gap?: number;
229
+ gap?: FacetGap;
228
230
  align?: CompositionAlign;
229
231
  padding?: number | CompositionPadding;
230
232
  scales?: FacetScaleResolutions;
@@ -245,7 +247,7 @@ export interface FacetGridOptions {
245
247
  rows: FacetGridRole;
246
248
  columns: FacetGridRole;
247
249
  combinations?: "observed" | "full";
248
- gap?: number;
250
+ gap?: FacetGap;
249
251
  align?: CompositionAlign;
250
252
  padding?: number | CompositionPadding;
251
253
  scales?: FacetScaleResolutions;
@@ -262,7 +264,7 @@ export interface RepeatChartsOptions {
262
264
  channel: "x" | "y" | "theta" | "r" | { parallelDimension: string };
263
265
  fields: readonly [string, ...string[]];
264
266
  columns?: number;
265
- gap?: number;
267
+ gap?: FacetGap;
266
268
  align?: CompositionAlign;
267
269
  padding?: number | CompositionPadding;
268
270
  scales?: FacetScaleResolutions;
@@ -328,7 +330,7 @@ export interface FacetCompositionSpec {
328
330
  readonly spacing?: "canvas" | "plot" | "content";
329
331
  /** Canonical opt-in intent; stacked is represented by omission. */
330
332
  readonly topLane?: "shared";
331
- readonly gap: number;
333
+ readonly gap: FacetGap;
332
334
  readonly align: CompositionAlign;
333
335
  readonly padding: Readonly<Required<CompositionPadding>>;
334
336
  readonly facet: {
@@ -755,6 +757,7 @@ export type DatasetTimeUnitTransform = DatasetTimeUnitTransformBase & (
755
757
  readonly weekRule: "calendar" | "iso";
756
758
  }
757
759
  );
760
+ export type Bin2DAssignment = "edges" | "step";
758
761
  export interface Bin2DCounts {
759
762
  x: number;
760
763
  y: number;
@@ -781,6 +784,7 @@ export interface DatasetBin2DOutputFields {
781
784
  }
782
785
  export interface DatasetBin2DTransform {
783
786
  readonly type: "bin2d";
787
+ readonly assignment?: "step";
784
788
  readonly x: string;
785
789
  readonly y: string;
786
790
  readonly bins: Readonly<Bin2DCounts>;
@@ -1469,6 +1473,8 @@ export interface CartesianAxisLabelStyleOptions extends AxisLabelStyleOptions {
1469
1473
  colorMap?: CartesianAxisLabelColorMap | "auto";
1470
1474
  /** Keep unrotated label bounds inside the mapped axis span; defaults to false. */
1471
1475
  flush?: boolean;
1476
+ /** Only flush mapped ticks within this non-negative distance of a range edge. Auto restores all-label containment. */
1477
+ flushThreshold?: number | "auto";
1472
1478
  /** Signed displacement from the mapped tick position, independent of outward offset. */
1473
1479
  alongOffset?: number;
1474
1480
  }
@@ -1494,6 +1500,7 @@ export interface AxisTicksAndLabelsOptions<P extends string> {
1494
1500
  ticks?: CartesianAxisTickStyleOptions;
1495
1501
  labels?: CartesianAxisLabelStyleOptions & AxisLabelLayoutOptions & DisplayLabelOptions;
1496
1502
  }
1503
+ export type AxisTitleOffset = number | Readonly<{ from: "labels"; gap: number }>;
1497
1504
  export interface AxisTitleOptions<P extends string> {
1498
1505
  /** Axis resource identity; omission prefers canonical, then a unique named owner. */
1499
1506
  target?: string;
@@ -1508,8 +1515,8 @@ export interface AxisTitleOptions<P extends string> {
1508
1515
  scale?: string;
1509
1516
  position?: P;
1510
1517
  at?: "start" | "center" | "end" | number;
1511
- /** Signed distance from the axis frame: positive outward, negative inward. */
1512
- offset?: number;
1518
+ /** Signed distance from the axis frame, or a nonnegative gap outside same-owner labels. */
1519
+ offset?: AxisTitleOffset;
1513
1520
  rotation?: RotationInput;
1514
1521
  color?: string;
1515
1522
  fontSize?: number;
@@ -2673,6 +2680,7 @@ export interface Bin2DDataOptions {
2673
2680
  y: string;
2674
2681
  bins?: number | Bin2DCounts;
2675
2682
  extent?: Bin2DExtent;
2683
+ assignment?: "edges" | "step";
2676
2684
  includeEmpty?: boolean;
2677
2685
  members?: boolean;
2678
2686
  as?: Bin2DOutputFields;
@@ -2685,6 +2693,7 @@ export interface EditBin2DDataOptions {
2685
2693
  y?: string;
2686
2694
  bins?: number | Bin2DCounts;
2687
2695
  extent?: Bin2DExtent;
2696
+ assignment?: "edges" | "step";
2688
2697
  includeEmpty?: boolean;
2689
2698
  members?: boolean;
2690
2699
  as?: DatasetBin2DOutputFields;
@@ -4043,6 +4052,7 @@ export interface BinnedHeatmapColorOptions {
4043
4052
  export interface HeatmapBinOptions {
4044
4053
  bins?: number | Bin2DCounts;
4045
4054
  extent?: Bin2DExtent;
4055
+ assignment?: "edges" | "step";
4046
4056
  includeEmpty?: boolean;
4047
4057
  }
4048
4058
 
@@ -5193,6 +5203,8 @@ export interface TitleOptions {
5193
5203
  maxWidth?: number;
5194
5204
  wrap?: "word" | "character";
5195
5205
  lineHeight?: number;
5206
+ /** Subtitle-only line spacing; overrides lineHeight without requiring maxWidth. */
5207
+ subtitleLineHeight?: number;
5196
5208
  titleStyle?: TitleTextStyleOptions;
5197
5209
  subtitleStyle?: TitleTextStyleOptions;
5198
5210
  }