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.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +1 -537
- package/src/actions/guides/axes/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- package/src/actions/titles/resolve.js +11 -1
- package/src/core/compositionState.js +4 -1
- package/src/core/facetGap.js +44 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +34 -4
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +2 -6
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facets.js +1 -1
- package/src/layout/title.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/planner.js +19 -4
- package/src/persistence/validation.js +19 -1
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +19 -7
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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] +
|
|
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] +
|
|
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 (
|
|
46
|
-
y + cell.bounds.
|
|
47
|
-
|
|
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,
|
package/src/layout/facets.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cloneAndFreeze as
|
|
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};
|
package/src/layout/title.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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.
|
|
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,
|
package/types/program.d.ts
CHANGED
|
@@ -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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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:
|
|
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
|
|
1512
|
-
offset?:
|
|
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
|
}
|