ggaction 0.0.22 → 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 +35 -0
- package/README.md +82 -90
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +6 -3
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/timeUnit.js +1 -1
- 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/guides/legends/categorical/components.js +1 -4
- package/src/actions/regression/create.js +6 -1
- package/src/actions/regression/edit.js +10 -1
- package/src/actions/titles/resolve.js +11 -1
- package/src/core/compositionState.js +9 -4
- package/src/core/facetAxisPolicy.js +30 -0
- package/src/core/facetGap.js +44 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +34 -4
- package/src/grammar/curveCommands.js +35 -2
- package/src/grammar/datasetSchema.js +1 -1
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/guides.js +7 -5
- package/src/grammar/regression/parameters.js +7 -0
- package/src/grammar/timeUnit.js +9 -3
- 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/guideCollisions.js +9 -3
- package/src/layout/title.js +1 -1
- package/src/materialization/facetContentBounds.js +96 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +16 -0
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +25 -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 +25 -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 +36 -18
|
@@ -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};
|
|
@@ -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
|
|
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
|
|
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
|
}
|
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,
|
|
@@ -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
|
|
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{
|
|
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
|
|
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 +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
|
}
|