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