ggaction 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +83 -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 +11 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +1 -0
- package/src/actions/boxPlots/components.js +1 -96
- package/src/actions/boxPlots/create.js +6 -1
- package/src/actions/boxPlots/edit.js +8 -0
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +1 -1
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +2 -1
- package/src/actions/canvas/plotOrder.js +14 -0
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +4 -4
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +1 -1
- package/src/actions/charts/endpoints.js +54 -5
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/index.js +2 -1
- package/src/actions/charts/interval-regression.js +3 -3
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/radar.js +63 -3
- package/src/actions/charts/raincloud.js +37 -6
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +3 -3
- package/src/actions/charts/text.js +1 -1
- package/src/actions/composition/actions.js +10 -3
- package/src/actions/coordinates/actions.js +2 -0
- package/src/actions/data/basic.js +6 -0
- package/src/actions/data/derived.js +6 -2
- package/src/actions/data/edit.js +22 -36
- package/src/actions/data/filter.js +1 -376
- package/src/actions/data/flatten.js +19 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/lookup.js +50 -0
- package/src/actions/data/raincloudFilter.js +56 -0
- package/src/actions/data/shared.js +10 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/distributions/revision.js +3 -1
- package/src/actions/encodings/angle.js +2 -1
- package/src/actions/encodings/appearance.js +38 -13
- package/src/actions/encodings/channels.js +14 -3
- package/src/actions/encodings/color/continuous.js +26 -10
- package/src/actions/encodings/color/index.js +50 -20
- package/src/actions/encodings/color/policy.js +57 -4
- package/src/actions/encodings/parallel.js +4 -0
- package/src/actions/encodings/position/policies/bar.js +10 -6
- package/src/actions/encodings/position/policies/line.js +1 -152
- package/src/actions/encodings/position/resolve.js +4 -1
- package/src/actions/encodings/ranged.js +46 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/encodings/ruleAppearance.js +7 -6
- package/src/actions/encodings/shared.js +14 -2
- package/src/actions/encodings/stroke.js +20 -14
- package/src/actions/encodings/strokeDash.js +9 -11
- package/src/actions/encodings/text.js +30 -2
- package/src/actions/errorBars/edit.js +24 -4
- package/src/actions/facets/actions.js +1 -793
- package/src/actions/facets/derive.js +34 -5
- package/src/actions/facets/replay.js +1 -38
- package/src/actions/gradientPlots/components.js +21 -9
- package/src/actions/gradientPlots/create.js +4 -2
- package/src/actions/gradientPlots/edit.js +18 -1
- package/src/actions/gradientPlots/materialize.js +35 -36
- package/src/actions/gradientPlots/options.js +1 -1
- package/src/actions/gradientPlots/paint.js +13 -7
- package/src/actions/guides/applicability.js +2 -0
- package/src/actions/guides/axes/axes.js +39 -0
- package/src/actions/guides/axes/axis.js +9 -5
- package/src/actions/guides/axes/edit.js +11 -8
- package/src/actions/guides/axes/labels.js +24 -8
- package/src/actions/guides/axes/parallel/policy.js +3 -5
- package/src/actions/guides/axes/parallel/resolve.js +4 -3
- package/src/actions/guides/axes/policy.js +22 -0
- package/src/actions/guides/axes/tickGroups.js +4 -2
- package/src/actions/guides/axes/ticks.js +22 -4
- package/src/actions/guides/axes/titles.js +19 -6
- package/src/actions/guides/facadeAxes.js +3 -0
- package/src/actions/guides/grids/grid.js +3 -1
- package/src/actions/guides/grids/resolve.js +7 -11
- package/src/actions/guides/guides.js +21 -1
- package/src/actions/guides/legends/categorical/actions.js +1 -515
- package/src/actions/guides/legends/categorical/components.js +1 -4
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +4 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -3
- package/src/actions/guides/legends/categorical/symbols.js +3 -3
- package/src/actions/guides/legends/colorSize.js +2 -0
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -360
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +15 -1
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/sampling.js +1 -140
- package/src/actions/guides/legends/strokeWidth.js +3 -2
- package/src/actions/guides/legends/transition.js +19 -2
- package/src/actions/guides/polar/axes/facade.js +5 -4
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/resolve.js +2 -1
- package/src/actions/guides/tickValues.js +7 -7
- package/src/actions/marks/arc/actions.js +5 -7
- package/src/actions/marks/area/actions.js +9 -3
- package/src/actions/marks/bar/create.js +4 -2
- package/src/actions/marks/bar/edit.js +9 -6
- package/src/actions/marks/bar/materialize.js +5 -3
- package/src/actions/marks/dependentReferences.js +87 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/lifecycle.js +2 -1
- package/src/actions/marks/line/actions.js +24 -6
- package/src/actions/marks/line/materialize.js +7 -3
- package/src/actions/marks/point/create.js +5 -4
- package/src/actions/marks/point/edit.js +6 -3
- package/src/actions/marks/point/jitter.js +6 -6
- package/src/actions/marks/point/materialize.js +36 -39
- package/src/actions/marks/rect/actions.js +16 -4
- package/src/actions/marks/references.js +183 -35
- package/src/actions/marks/rule/actions.js +20 -9
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +10 -3
- package/src/actions/marks/tick/actions.js +11 -2
- package/src/actions/primitives/editGraphics.js +28 -3
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semanticAction.js +13 -7
- package/src/actions/primitives/semanticValidation/layer.js +20 -0
- package/src/actions/primitives/semanticValidation/scale.js +8 -1
- package/src/actions/regression/create.js +6 -1
- package/src/actions/regression/edit.js +10 -1
- package/src/actions/scales/channels.js +1 -1
- package/src/actions/scales/consumers/common.js +19 -1
- package/src/actions/scales/consumers/families.js +2 -2
- package/src/actions/scales/consumers/seriesLayout.js +4 -3
- package/src/actions/scales/edit.js +11 -0
- package/src/actions/scales/editPolicy.js +5 -3
- package/src/actions/scales/preview.js +2 -1
- package/src/actions/scales/quantitativeColor.js +2 -4
- package/src/actions/selection/actions.js +10 -1
- package/src/actions/theme/reconcile.js +37 -9
- package/src/core/compositionState.js +26 -7
- package/src/core/datasetReferences.js +17 -0
- package/src/core/facetAxisPolicy.js +30 -0
- package/src/core/immutable.js +1 -0
- package/src/core/resourceReferences.js +5 -3
- package/src/core/validation.js +4 -0
- package/src/core/vocabulary.js +24 -2
- package/src/grammar/aggregate.js +1 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bars/policy.js +2 -2
- package/src/grammar/curveCommands.js +35 -2
- package/src/grammar/datasetDependencies.js +97 -0
- package/src/grammar/datasetSchema.js +32 -2
- package/src/grammar/facetHeaders.js +1 -0
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +7 -5
- package/src/grammar/facets/legendIntent.js +6 -2
- package/src/grammar/facets/scales.js +27 -7
- package/src/grammar/flatten.js +85 -0
- package/src/grammar/histogram.js +13 -1
- package/src/grammar/horizon.js +1 -494
- package/src/grammar/jitter.js +2 -2
- package/src/grammar/legendSpacing.js +45 -0
- package/src/grammar/lineMissing.js +90 -0
- package/src/grammar/lineSeries.js +1 -376
- package/src/grammar/lookup.js +97 -0
- package/src/grammar/markFilter.js +4 -2
- package/src/grammar/markLabels.js +8 -3
- package/src/grammar/numeric.js +1 -1
- package/src/grammar/pathCommands.js +1 -1
- package/src/grammar/pathSeries.js +1 -109
- package/src/grammar/pointPolygon.js +81 -0
- package/src/grammar/pointShapes.js +15 -15
- package/src/grammar/rects.js +1 -1
- package/src/grammar/regression/derive.js +1 -195
- package/src/grammar/regression/parameters.js +7 -0
- package/src/grammar/scales/appearance.js +1 -1
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -63
- package/src/grammar/scales/continuous.js +58 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +3 -3
- package/src/grammar/schemas/graphicBounds.js +40 -12
- package/src/grammar/schemas/graphicClip.js +23 -0
- package/src/grammar/schemas/graphicTree.js +16 -0
- package/src/grammar/statisticalReference.js +22 -0
- package/src/grammar/text.js +65 -1
- package/src/grammar/ticks.js +92 -1
- package/src/grammar/timeUnit.js +9 -3
- package/src/grammar/transformTopology.js +3 -1
- package/src/grammar/transforms.js +24 -0
- package/src/inspection.js +1 -495
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -1
- package/src/layout/facetContentFlow.js +101 -0
- package/src/layout/facetHeaderTitles.js +1 -0
- package/src/layout/facetTopLane.js +29 -0
- package/src/layout/facets.js +1 -1
- package/src/layout/guideCollisions.js +9 -3
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +26 -10
- package/src/layout/legendSpacing.js +66 -0
- package/src/layout/plotDrawOrder.js +17 -0
- package/src/materialization/bars/histogram.js +8 -2
- package/src/materialization/bars/resolve.js +1 -1
- package/src/materialization/composition.js +1 -0
- package/src/materialization/compositionSnapshot.js +22 -2
- package/src/materialization/dataProvenance.js +1 -1
- package/src/materialization/facetContentBounds.js +96 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +39 -11
- package/src/materialization/facetGuides/preparation.js +43 -24
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facetTopLane.js +25 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +30 -1
- package/src/materialization/graphicTranslation.js +2 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/markClip.js +26 -0
- package/src/materialization/marks/capabilities.js +3 -2
- package/src/materialization/marks/grouping.js +7 -5
- package/src/materialization/marks/index.js +4 -2
- package/src/materialization/marks/policies.js +4 -0
- package/src/materialization/plotDrawOrder.js +44 -0
- package/src/materialization/rect.js +11 -3
- package/src/materialization/scaleGuideDependencies.js +13 -5
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/policies/index.js +2 -1
- package/src/materialization/text.js +10 -5
- package/src/mcp/cli.js +0 -0
- package/src/persistence/validation.js +114 -18
- package/src/persistence.js +4 -2
- package/src/renderers/canvas/index.js +16 -0
- package/src/renderers/canvas/isolated.js +11 -1
- package/src/renderers/canvas/native.js +7 -0
- package/src/renderers/canvas/scene.js +66 -0
- package/src/renderers/canvas/text.js +3 -2
- package/src/renderers/svg.js +1 -617
- package/src/renderers/svgSerialization.js +643 -0
- package/src/renderers/transitionScene.js +67 -0
- package/src/scene.js +89 -0
- package/src/selectors/axes.js +1 -1
- package/src/selectors/datasets.js +1 -0
- package/src/selectors/grids.js +9 -0
- package/src/selectors/markClip.js +28 -0
- package/src/selectors/raincloud.js +7 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +7 -0
- package/types/index.d.ts +8 -0
- package/types/program.d.ts +286 -70
- package/types/scene.d.ts +35 -0
package/src/layout/canvas.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{annotateError as
|
|
1
|
+
import{annotateError as p}from"../core/diagnostics.js";import{cloneAndFreeze as n,isPlainObject as u}from"../core/immutable.js";import{validateNonEmptyString as l,validateNonNegativeFinite as v,validatePositiveFinite as a,validateOptionObject as h,validateBoolean as w}from"../core/validation.js";const f=Object.freeze(["top","right","bottom","left"]),d=n({top:30,right:30,bottom:60,left:70}),x=n({width:640,height:400,background:"white",margin:d});function c(e,r){v(e,`Canvas margin.${r}`)}function E(e,r=d){if(Number.isFinite(e)){if(e<0)throw new RangeError("Canvas margin must not be negative.");return n({top:e,right:e,bottom:e,left:e})}if(!u(e))throw new TypeError("Canvas margin must be a number or a plain object.");const o=Object.keys(e);if(o.length===0)throw new TypeError("Canvas margin object must contain at least one side.");for(const t of o){if(!f.includes(t))throw new Error(`Unknown canvas margin option "${t}".`);c(e[t],t)}return n({...r,...e})}function O(e){if(e===!1)return!1;h(e,["color","lineWidth","underlay"],"Canvas plotBorder"),e.underlay!==void 0&&w(e.underlay,"Canvas plotBorder underlay");const{underlay:r,...o}=e,t={color:"#ddd",lineWidth:1,...o,...r===!0?{underlay:!0}:{}};return l(t.color,"Canvas plotBorder color"),a(t.lineWidth,"Canvas plotBorder lineWidth"),n(t)}function j({width:e,height:r,background:o,margin:t,plotBorder:i=!1}){a(e,"Canvas width"),a(r,"Canvas height"),l(o,"Canvas background");for(const s of f)c(t[s],s);if(t.left+t.right>=e)throw new RangeError("Canvas horizontal margins must be smaller than width.");if(t.top+t.bottom>=r)throw new RangeError("Canvas vertical margins must be smaller than height.");if(i!==!1&&i.lineWidth>=Math.min(e-t.left-t.right,r-t.top-t.bottom))throw new RangeError("Canvas plotBorder lineWidth must be smaller than both plot dimensions.")}function b({width:e,height:r,margin:o}){return n({x:o.left,y:o.top,width:e-o.left-o.right,height:r-o.top-o.bottom})}function N(e){const r=e.graphicSpec.objects.canvas,o=e.materializationConfigs.canvas?.margin;if(r?.type!=="canvas"||!Number.isFinite(r.properties.width)||!Number.isFinite(r.properties.height)||o===void 0)throw new Error("Graphical layout requires Canvas dimensions and margin.");return b({width:r.properties.width,height:r.properties.height,margin:o})}function m(e,r){if(!r||![r.width,r.height].every(t=>Number.isFinite(t)&&t>0)||!Array.isArray(e)||e.some(t=>!t||![t.left,t.right,t.top,t.bottom].every(Number.isFinite)||t.left>t.right||t.top>t.bottom))return;const o={top:0,right:0,bottom:0,left:0};for(const t of e)o.top=Math.max(o.top,-t.top),o.right=Math.max(o.right,t.right-r.width),o.bottom=Math.max(o.bottom,t.bottom-r.height),o.left=Math.max(o.left,-t.left);return Object.values(o).some(t=>t>1e-9)?n(o):void 0}function F(e,r,o){const t=new Error(e),i=m(r,o);return i===void 0?t:p(t,{reason:"canvas-overflow",canvasOverflow:i})}function A(e,r){const o=Object.hasOwn(e,"plot")?e.plot:r;if(!(o===void 0||o===!1)){if(h(o,["width","height"],"Canvas plot"),a(o.width,"Canvas plot width"),a(o.height,"Canvas plot height"),Object.hasOwn(e,"width")||Object.hasOwn(e,"height"))throw new Error("Canvas plot dimensions cannot be combined with fixed outer dimensions.");return n(o)}}export{x as DEFAULT_CANVAS,d as DEFAULT_MARGIN,F as canvasOverflowError,b as createGraphicBounds,E as normalizeMargin,O as normalizePlotBorder,m as resolveCanvasOverflow,A as resolveCanvasPlot,N as resolveGraphicBounds,j as validateCanvasState};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateCompositionSpacing } from "./composition.js";
|
|
3
|
+
|
|
4
|
+
/** Pack ordered full content bounds without imposing common plot origins. */
|
|
5
|
+
export function resolveFacetContentFlow({ children, columns, gap = 16 } = {}) {
|
|
6
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
7
|
+
throw new TypeError("Facet content flow requires non-empty children.");
|
|
8
|
+
}
|
|
9
|
+
columns ??= children.length;
|
|
10
|
+
if (!Number.isInteger(columns) || columns <= 0) {
|
|
11
|
+
throw new RangeError("Facet content flow columns must be a positive integer.");
|
|
12
|
+
}
|
|
13
|
+
columns = Math.min(columns, children.length);
|
|
14
|
+
validateCompositionSpacing(gap, "Facet content flow gap");
|
|
15
|
+
const ids = new Set();
|
|
16
|
+
const rightExtents = Array(columns).fill(0);
|
|
17
|
+
const bottomExtents = Array(Math.ceil(children.length / columns)).fill(0);
|
|
18
|
+
const cells = children.map((child, index) => {
|
|
19
|
+
if (!isPlainObject(child) || typeof child.id !== "string" ||
|
|
20
|
+
child.id.length === 0 || ids.has(child.id)) {
|
|
21
|
+
throw new TypeError("Facet content flow requires unique non-empty child IDs.");
|
|
22
|
+
}
|
|
23
|
+
const bounds = child.bounds;
|
|
24
|
+
if (!isPlainObject(bounds) ||
|
|
25
|
+
!["left", "right", "top", "bottom"].every(key => Number.isFinite(bounds[key])) ||
|
|
26
|
+
bounds.left > bounds.right || bounds.top > bounds.bottom) {
|
|
27
|
+
throw new RangeError("Facet content flow requires finite ordered content bounds.");
|
|
28
|
+
}
|
|
29
|
+
ids.add(child.id);
|
|
30
|
+
const column = index % columns, row = Math.floor(index / columns);
|
|
31
|
+
rightExtents[column] = Math.max(rightExtents[column], Math.ceil(bounds.right));
|
|
32
|
+
bottomExtents[row] = Math.max(bottomExtents[row], Math.ceil(bounds.bottom));
|
|
33
|
+
return { id: child.id, column, row, bounds: { ...bounds } };
|
|
34
|
+
});
|
|
35
|
+
const columnOrigins = Array(columns).fill(0);
|
|
36
|
+
let x = 0;
|
|
37
|
+
const placed = cells.map(cell => {
|
|
38
|
+
x = cell.column === 0 ? 0 : x + rightExtents[cell.column - 1] + gap +
|
|
39
|
+
Math.max(0, Math.ceil(-cell.bounds.left));
|
|
40
|
+
if (cell.row > 0) {
|
|
41
|
+
columnOrigins[cell.column] += bottomExtents[cell.row - 1] + gap +
|
|
42
|
+
Math.max(0, Math.ceil(-cell.bounds.top));
|
|
43
|
+
}
|
|
44
|
+
const y = columnOrigins[cell.column];
|
|
45
|
+
if (![x, y, x + cell.bounds.left, x + cell.bounds.right,
|
|
46
|
+
y + cell.bounds.top, y + cell.bounds.bottom].every(Number.isFinite)) {
|
|
47
|
+
throw new RangeError("Facet content flow exceeds finite layout bounds.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
id: cell.id, column: cell.column, row: cell.row, x, y,
|
|
51
|
+
bounds: {
|
|
52
|
+
left: x + cell.bounds.left, right: x + cell.bounds.right,
|
|
53
|
+
top: y + cell.bounds.top, bottom: y + cell.bounds.bottom
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const bounds = { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity };
|
|
58
|
+
for (const child of placed) {
|
|
59
|
+
bounds.left = Math.min(bounds.left, child.bounds.left);
|
|
60
|
+
bounds.right = Math.max(bounds.right, child.bounds.right);
|
|
61
|
+
bounds.top = Math.min(bounds.top, child.bounds.top);
|
|
62
|
+
bounds.bottom = Math.max(bounds.bottom, child.bounds.bottom);
|
|
63
|
+
}
|
|
64
|
+
return cloneAndFreeze({ columns, gap, children: placed, bounds });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Place one shared footer per occupied column beneath the complete content. */
|
|
68
|
+
export function resolveFacetContentFooters({ flow, footers, offset = 0 } = {}) {
|
|
69
|
+
if (!isPlainObject(flow) || !Array.isArray(flow.children) || flow.children.length === 0 ||
|
|
70
|
+
!Number.isFinite(flow.bounds?.bottom)) {
|
|
71
|
+
throw new TypeError("Facet content footers require a resolved content flow.");
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(footers) || !Number.isFinite(offset)) {
|
|
74
|
+
throw new TypeError("Facet content footers require an array and finite offset.");
|
|
75
|
+
}
|
|
76
|
+
const lastByColumn = new Map(), occupied = new Set(), ownerIds = new Set();
|
|
77
|
+
for (const child of flow.children) {
|
|
78
|
+
if (!isPlainObject(child) || typeof child.id !== "string" || child.id.length === 0 || ownerIds.has(child.id) ||
|
|
79
|
+
!Number.isInteger(child.column) || child.column < 0 ||
|
|
80
|
+
!Number.isInteger(child.row) || child.row < 0 || !Number.isFinite(child.x)) {
|
|
81
|
+
throw new RangeError("Facet content footer owners require finite occupied cells.");
|
|
82
|
+
}
|
|
83
|
+
const slot = `${child.row}:${child.column}`;
|
|
84
|
+
if (occupied.has(slot)) throw new Error("Facet content footer column has duplicate cells.");
|
|
85
|
+
occupied.add(slot); ownerIds.add(child.id);
|
|
86
|
+
const previous = lastByColumn.get(child.column);
|
|
87
|
+
if (previous === undefined || child.row > previous.row) lastByColumn.set(child.column, child);
|
|
88
|
+
}
|
|
89
|
+
const columns = new Set(), ids = new Set(), y = Math.ceil(flow.bounds.bottom) + offset;
|
|
90
|
+
if (!Number.isFinite(y)) throw new RangeError("Facet content footer exceeds finite layout bounds.");
|
|
91
|
+
return cloneAndFreeze(footers.map(footer => {
|
|
92
|
+
if (!isPlainObject(footer) || typeof footer.id !== "string" || footer.id.length === 0 ||
|
|
93
|
+
ids.has(footer.id) || !Number.isInteger(footer.column) ||
|
|
94
|
+
columns.has(footer.column) || !lastByColumn.has(footer.column)) {
|
|
95
|
+
throw new Error("Facet content footer requires one unique ID and occupied column.");
|
|
96
|
+
}
|
|
97
|
+
ids.add(footer.id); columns.add(footer.column);
|
|
98
|
+
const owner = lastByColumn.get(footer.column);
|
|
99
|
+
return { id: footer.id, column: footer.column, ownerId: owner.id, x: owner.x, y };
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveTextBounds as m}from"../core/textMetrics.js";const p=Object.freeze(["fontSize","fontFamily","fontWeight","color"]),b=Object.freeze({left:-Math.PI/2,right:Math.PI/2,top:0,bottom:0});function u(r,o,n,l){const i=m({...o,text:r,x:0,y:0,textAlign:"center",textBaseline:"middle"},l);return["left","right"].includes(n)?i.right-i.left:i.bottom-i.top}function M(r,o){const n=["left","right"].includes(o)?r.cellColumns?.[o]:r.cellRows[o];return n===void 0?0:["left","top"].includes(o)?n[0]:n.at(-1)}function T({roles:r,descriptors:o,space:n,headerLayout:l,profile:i,ids:g}){const a=[];for(const[x,f]of r){if(f.title===void 0)continue;const{side:t}=f,c=f.title,s={...Object.fromEntries(p.map(e=>[e,f[e]])),...c,side:t,align:c.align??"center",rotation:c.rotation??b[t]},d=Math.max(0,...o.filter(e=>e.config.side===t&&e.text!=="").map(e=>u(e.text,e.config,t,i)+e.config.offset+(e.clearance?.[t]??0)));s.offset=c.offset??Math.max(n[t],d)+8;const h=s.offset+u(c.text,s,t,i);l.outer[t]=Math.max(l.outer[t],h-n[t]-M(l,t),0),a.push({topology:"role-title",role:x,ids:g,text:c.text,config:s})}return a}export{T as prepareFacetHeaderTitles};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DEFAULT_FACET_TOP_LANE, FACET_TOP_LANE_MODES } from "../core/vocabulary.js";
|
|
2
|
+
import { textBoundsFitCanvas, textBoundsIntersect } from "../core/textMetrics.js";
|
|
3
|
+
|
|
4
|
+
export function normalizeFacetTopLane(value, spacing) {
|
|
5
|
+
const mode = value === undefined ? DEFAULT_FACET_TOP_LANE : value;
|
|
6
|
+
if (!FACET_TOP_LANE_MODES.includes(mode)) {
|
|
7
|
+
throw new Error('Facet topLane must be "stacked" or "shared".');
|
|
8
|
+
}
|
|
9
|
+
if (mode !== DEFAULT_FACET_TOP_LANE && spacing !== "plot") {
|
|
10
|
+
throw new Error('Shared facet topLane requires spacing "plot".');
|
|
11
|
+
}
|
|
12
|
+
return mode === DEFAULT_FACET_TOP_LANE ? undefined : mode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertFacetTopLaneBounds({ canvas, titles, headers, retained, plots }) {
|
|
16
|
+
const owned = [...titles, ...headers];
|
|
17
|
+
if (owned.some(bounds => !textBoundsFitCanvas(bounds, canvas))) {
|
|
18
|
+
throw new Error("Shared facet top lane text does not fit the final Canvas.");
|
|
19
|
+
}
|
|
20
|
+
if (titles.some(title => headers.some(header => textBoundsIntersect(title, header)))) {
|
|
21
|
+
throw new Error("Shared facet top lane title overlaps a facet header.");
|
|
22
|
+
}
|
|
23
|
+
if (owned.some(bounds => plots.some(plot => textBoundsIntersect(bounds, plot)))) {
|
|
24
|
+
throw new Error("Shared facet top lane text overlaps a child plot.");
|
|
25
|
+
}
|
|
26
|
+
if (owned.some(bounds => retained.some(other => textBoundsIntersect(bounds, other)))) {
|
|
27
|
+
throw new Error("Shared facet top lane text overlaps retained chart text.");
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/layout/facets.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cloneAndFreeze as
|
|
1
|
+
import{cloneAndFreeze as et,isPlainObject as U}from"../core/immutable.js";import{normalizeFacetTopLane as ot}from"./facetTopLane.js";import{annotateError as rt}from"../core/diagnostics.js";import{DEFAULT_COMPOSITION_LAYOUT as P,normalizeCompositionAlign as nt,normalizeCompositionChildren as at,normalizeCompositionPadding as it,validateCompositionSpacing as E}from"./composition.js";const lt=18,st=132,ct=0,ut=Object.freeze(["right","left","top","bottom"]);function mt(r,i){const f=r??i;if(!Number.isInteger(f)||f<=0)throw new RangeError("Facet columns must be a positive integer.");return Math.min(f,i)}function ht(r,i){if(!r.some(o=>o.row!==void 0||o.column!==void 0))return;if(r.some(o=>!(Number.isInteger(o.row)&&o.row>=0&&Number.isInteger(o.column)&&o.column>=0)))throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");const d=r.map(o=>`${o.row}:${o.column}`);if(new Set(d).size!==d.length)throw new Error("Facet grid children must occupy unique row and column coordinates.");const w=Math.max(...r.map(o=>o.column))+1;if(i!==void 0&&i!==w)throw new RangeError("Facet grid columns must match the declared column domain.");return{columns:w,rows:Math.max(...r.map(o=>o.row))+1}}function Q(r,i){return i==="start"?0:i==="end"?r:r/2}const M=Object.freeze({top:0,right:0,bottom:0,left:0});function ft(r,i,f){if(r===void 0)return{outer:M,cellRows:{top:Array(i).fill(0),bottom:Array(i).fill(0)}};if(!U(r)||Object.keys(r).some(o=>!["outer","cellRows","cellColumns"].includes(o)))throw new TypeError("Facet headerLayout must contain outer and cellRows.");const d={...M,...r.outer??{}};if(!U(r.outer??{})||Object.keys(d).some(o=>!Object.hasOwn(M,o)))throw new TypeError("Facet headerLayout.outer must contain side reservations.");for(const[o,m]of Object.entries(d))E(m,`Facet headerLayout.outer.${o}`);const w={outer:d};for(const[o,m,x,z]of[["cellRows",["top","bottom"],i,"row"],["cellColumns",["left","right"],f,"column"]]){if(o==="cellColumns"&&r[o]===void 0)continue;const R=o==="cellRows"?r[o]??{}:r[o];if(!U(R)||Object.keys(R).some(l=>!m.includes(l)))throw new TypeError(`Facet headerLayout.${o} must contain ${m[0]} and ${m[1]} arrays.`);w[o]=Object.fromEntries(m.map(l=>{const F=R[l]??Array(x).fill(0);if(!Array.isArray(F)||F.length!==x)throw new RangeError(`Facet headerLayout.${o}.${l} must match ${z} count.`);return F.forEach((c,D)=>E(c,`Facet headerLayout.${o}.${l}[${D}]`)),[l,[...F]]}))}return w}function yt({children:r,columns:i,gap:f=P.gap,align:d=P.align,padding:w=P.padding,titleHeight:o=0,sharedLegend:m=!1,sharedLegendGap:x=lt,sharedLegendWidth:z=st,sharedLegendHeight:R=ct,sharedLegendPosition:l="right",headerLayout:F,spacing:c="canvas",topLane:D,plots:L}={}){if(!["canvas","plot","content"].includes(c))throw new Error(`Unknown facet spacing "${c}".`);const $=ot(D,c),H=new Map((L??[]).map(e=>[e.id,e])),W=r;let h=M;if(c==="plot"){if(!Array.isArray(L)||L.length!==r?.length||H.size!==L.length)throw new Error("Plot spacing requires one plot bounds record per child.");h={...M},r=r.map(e=>{const t=H.get(e.id);if(!t||![t.x,t.y,t.width,t.height].every(Number.isFinite)||t.x<0||t.y<0||t.width<=0||t.height<=0||t.x+t.width>e.width+1e-9||t.y+t.height>e.height+1e-9)throw new Error("Plot spacing requires valid contained child plot bounds.");return h.left=Math.max(h.left,t.x),h.top=Math.max(h.top,t.y),h.right=Math.max(h.right,e.width-t.x-t.width),h.bottom=Math.max(h.bottom,e.height-t.y-t.height),{...e,width:t.width,height:t.height}})}const V=r?.map(e=>e?.value),g=ht(r??[],i);if(c==="content"&&g!==void 0)throw new Error("Content flow requires a wrapped facet, not a keyed facet grid.");const I=at(r?.map(({value:e,row:t,column:s,...a})=>a)),u=g?.columns??mt(i,I.length),p=E(f,"Facet gap"),N=nt(d),k=it(w),y=Object.fromEntries(Object.keys(k).map(e=>[e,k[e]+h[e]])),j=E(o,"Facet title height"),Y=$===void 0?j:0;if(typeof m!="boolean")throw new TypeError("Facet sharedLegend must be a boolean.");const q=E(x,"Facet shared legend gap"),Z=E(z,"Facet shared legend width"),B=E(R,"Facet shared legend height");if(!ut.includes(l))throw new Error(`Unknown facet shared legend position "${l}".`);const O=["right","left"].includes(l),J=["left","top"].includes(l),G=m?q+(O?Z:B):0,A=g?.rows??Math.ceil(I.length/u);let n=ft(F,A,u);if(c==="plot"){let e=0;for(const[t,s]of[[n.cellRows.bottom,n.cellRows.top],[n.cellColumns?.right??[],n.cellColumns?.left??[]]])for(let a=1;a<t.length;a+=1)e=Math.max(e,t[a-1]+s[a]);if(e>p)throw rt(new Error("Plot spacing gap is too small for the facet headers."),{code:"invalid-value",optionPath:"gap",actual:p,limit:e});n={outer:{top:n.outer.top+n.cellRows.top[0],bottom:n.outer.bottom+n.cellRows.bottom.at(-1),left:n.outer.left+(n.cellColumns?.left[0]??0),right:n.outer.right+(n.cellColumns?.right.at(-1)??0)},cellRows:{top:Array(A).fill(0),bottom:Array(A).fill(0)}},$!==void 0&&(n.outer.top=0)}const T=n.cellColumns??{left:Array(u).fill(0),right:Array(u).fill(0)},C=Array(u).fill(-1/0),v=Array(A).fill(-1/0);I.forEach((e,t)=>{const s=g===void 0?t%u:r[t].column,a=g===void 0?Math.floor(t/u):r[t].row;C[s]=Math.max(C[s],e.width),v[a]=Math.max(v[a],e.height)});const X=C.map((e,t)=>y.left+n.outer.left+C.slice(0,t).reduce((s,a,_)=>s+T.left[_]+a+T.right[_],0)+p*t+T.left[t]),tt=v.map((e,t)=>Y+y.top+n.outer.top+v.slice(0,t).reduce((s,a,_)=>s+n.cellRows.top[_]+a+n.cellRows.bottom[_],0)+p*t+n.cellRows.top[t]),b=I.map((e,t)=>{const s=g===void 0?t%u:r[t].column,a=g===void 0?Math.floor(t/u):r[t].row;return{id:e.id,value:V[t],column:s,row:a,x:X[s]+(J&&O?G:0)+Q(C[s]-e.width,N),y:tt[a]+(J&&!O?G:0)+Q(v[a]-e.height,N),width:e.width,height:e.height}}),S=y.left+n.outer.left+C.reduce((e,t)=>e+t,0)+T.left.reduce((e,t)=>e+t,0)+T.right.reduce((e,t)=>e+t,0)+p*Math.max(0,u-1)+n.outer.right+y.right,K=Y+y.top+n.outer.top+v.reduce((e,t)=>e+t,0)+n.cellRows.top.reduce((e,t)=>e+t,0)+n.cellRows.bottom.reduce((e,t)=>e+t,0)+p*Math.max(0,A-1)+n.outer.bottom+y.bottom;if(c==="plot")for(let e=0;e<b.length;e+=1){const t=H.get(b[e].id);b[e].x-=t.x,b[e].y-=t.y,b[e].width=W[e].width,b[e].height=W[e].height}return et({...c!=="canvas"?{spacing:c}:{},...$===void 0?{}:{topLane:$},columns:u,rows:A,gap:p,align:N,padding:k,titleHeight:j,headerLayout:n,gridWidth:S,width:S+(O?G:0),height:K+(O?0:G),children:b,...m?{legend:{position:l,x:l==="right"?S+q:0,y:l==="bottom"?K+q:l==="top"?j:j+y.top+30,width:Z,height:B}}:{}})}export{lt as DEFAULT_FACET_LEGEND_GAP,ct as DEFAULT_FACET_LEGEND_HEIGHT,st as DEFAULT_FACET_LEGEND_WIDTH,yt as resolveFacetLayout};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
function intersects(a, b) {
|
|
2
|
+
const tolerance = Number.EPSILON * 16 * Math.max(1,
|
|
3
|
+
...Object.values(a).map(Math.abs), ...Object.values(b).map(Math.abs));
|
|
4
|
+
return Math.min(a.right, b.right) - Math.max(a.left, b.left) > tolerance &&
|
|
5
|
+
Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > tolerance;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export function assertGuideCollisionBlocks(blocks) {
|
|
2
9
|
for (let index = 0; index < blocks.length; index += 1) {
|
|
3
10
|
const a = blocks[index];
|
|
@@ -5,14 +12,13 @@ export function assertGuideCollisionBlocks(blocks) {
|
|
|
5
12
|
if (a.kind === "axis" && b.kind === "axis") {
|
|
6
13
|
if (!(a.namedAxis || b.namedAxis) || a.axisId === b.axisId || !a.textBounds || !b.textBounds) continue;
|
|
7
14
|
if (a.textBounds.some(left => left && b.textBounds.some(right => right &&
|
|
8
|
-
left
|
|
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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{canvasOverflowError as M}from"./canvas.js";import{PLOT_LEGEND_POSITIONS as N}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as P,HORIZONTAL_LEGEND_BLOCK_GAP as C}from"./legendLane.js";function v({blocks:u,group:O,plot:e,canvas:c,allowOverflow:_=!1,deferCanvasFit:G=!1}){if(u.length===0)return[];const{placement:t,blockLayout:f}=O,w=N.includes(t.position),p=["left","right"].includes(t.position),x=f==="column"||f==="wrap"&&p,y=w||p||t.at!==void 0?P:C,l=e.width-(w?t.offset*2:0);let h=0,E=0,m=0;const d=u.map(i=>{const o=i.bounds.right-i.bounds.left,L=i.bounds.bottom-i.bounds.top;if(f==="wrap"&&!x&&!w&&o>l&&!_)throw new Error("Horizontal legend block requires more plot width.");!x&&f==="wrap"&&h>0&&h+o>l&&(h=0,E+=m+y,m=0);const I={id:i.id,x:h,y:E,width:o,height:L};return x?E+=L+y:(h+=o+y,m=Math.max(m,L)),I}),s=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,r;if(t.at!==void 0)n=e.x+t.at.x,r=e.y+t.at.y;else if(w){if(s+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-s,r=t.position.startsWith("top")?e.y+t.offset:e.y+e.height-t.offset-a}else p?(n=t.position==="right"?e.x+e.width+t.offset:e.x-t.offset-s,r=e.y):(n=t.align==="left"?e.x:t.align==="right"?e.x+e.width-s:e.x+(e.width-s)/2,r=t.position==="top"?e.y-t.offset-a:e.y+e.height+t.offset);const g={left:n,top:r,right:n+s,bottom:r+a};if(!G&&(g.left<0||g.top<0||g.right>c.width||g.bottom>c.height))throw t.at!==void 0||O.inferredOffset!==!0?new Error("Legend group requires more Canvas space."):M("Legend group requires more Canvas space.",[g],c);return d.map((i,o)=>({id:i.id,dx:n+i.x-u[o].bounds.left,dy:r+i.y-u[o].bounds.top}))}export{v as resolveLegendGroupPlacement};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
|
|
2
2
|
import { alignLegendStart, resolveLegendGrid, isPlotLegend, legendLabelLineHeight } from "./legend.js";
|
|
3
3
|
import { alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
|
|
4
|
+
import {resolveLegendContentSpacing} from "./legendSpacing.js";
|
|
4
5
|
|
|
5
6
|
// Measure item content before choosing its edge. Family owners supply the
|
|
6
7
|
// sample dimensions and formatted labels; this module does not inspect scales.
|
|
@@ -28,6 +29,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
28
29
|
let symbolX;
|
|
29
30
|
let labelX;
|
|
30
31
|
let itemY;
|
|
32
|
+
let itemRows;
|
|
31
33
|
let title;
|
|
32
34
|
if (config.layout === "legacy-bottom") {
|
|
33
35
|
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
|
|
@@ -41,6 +43,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
41
43
|
});
|
|
42
44
|
labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
|
|
43
45
|
itemY = labels.map(() => canvas.height - 28);
|
|
46
|
+
itemRows = labels.map(() => 0);
|
|
44
47
|
title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
|
|
45
48
|
} else if (side) {
|
|
46
49
|
const grid = resolveLegendGrid({
|
|
@@ -73,6 +76,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
73
76
|
firstY = titleBounds.bottom + 12 - itemTop;
|
|
74
77
|
}
|
|
75
78
|
itemY = grid.cells.map(cell => firstY + cell.row * pitch);
|
|
79
|
+
itemRows = grid.cells.map(cell => cell.row);
|
|
76
80
|
title = { x, y: plot.y + 20, align: "left" };
|
|
77
81
|
} else {
|
|
78
82
|
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
|
|
@@ -95,27 +99,39 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
95
99
|
symbolX = grid.cells.map(cell => gridX + columns[cell.column] - sampleLeft);
|
|
96
100
|
labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
|
|
97
101
|
itemY = grid.cells.map(cell => gridY + grid.rowHeight / 2 + cell.row * (grid.rowHeight + config.itemGap));
|
|
102
|
+
itemRows = grid.cells.map(cell => cell.row);
|
|
98
103
|
title = inline ? { x, y: y + height / 2, align: "left" }
|
|
99
104
|
: { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
|
|
100
105
|
}
|
|
101
106
|
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
102
107
|
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
103
108
|
}, profile);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
...symbolX.map((x, index) => {
|
|
109
|
+
const labelBounds = labels.map((label,index) => textBounds(labelX[index],itemY[index],label,config.labels));
|
|
110
|
+
const symbolItemBounds = symbolX.map((x, index) => {
|
|
107
111
|
const local = sampleBounds[index];
|
|
108
112
|
return { left: x + local.left, right: x + local.right,
|
|
109
113
|
top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
|
|
110
|
-
})
|
|
111
|
-
|
|
114
|
+
});
|
|
115
|
+
const spacingItems = config.rowPadding === undefined && config.titlePadding === undefined ? undefined
|
|
116
|
+
: labels.map((label,index) => ({row:itemRows[index],x:symbolX[index],y:itemY[index],bounds:[
|
|
117
|
+
...(label === "" ? [] : [labelBounds[index]]),
|
|
118
|
+
...(symbol.itemBounds !== undefined && index >= symbol.itemBounds.length ? [] : [symbolItemBounds[index]])
|
|
119
|
+
]}));
|
|
120
|
+
const bodyBounds = spacingItems === undefined
|
|
121
|
+
? [...labelBounds,...symbolItemBounds] : spacingItems.flatMap(item => item.bounds);
|
|
112
122
|
if (titleVisible) title = alignLegendTitle(title, config.title, config.titleStyle, bodyBounds, profile);
|
|
113
|
-
const
|
|
114
|
-
...(titleVisible ? [resolveTextBounds({ x: title.x, y: title.y,
|
|
123
|
+
const titleBounds = titleVisible ? resolveTextBounds({ x: title.x, y: title.y,
|
|
115
124
|
text: config.title, ...config.titleStyle, lines: titleText.lines,
|
|
116
|
-
textAlign: title.align, textBaseline: "middle" }, profile)
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
textAlign: title.align, textBaseline: "middle" }, profile) : undefined;
|
|
126
|
+
let bounds = [...(titleVisible ? [titleBounds] : []),...bodyBounds];
|
|
127
|
+
const spacing = spacingItems === undefined ? undefined : resolveLegendContentSpacing(config,spacingItems,titleBounds);
|
|
128
|
+
if (spacing !== undefined) {
|
|
129
|
+
symbolX = symbolX.map((x,index) => x+spacing.offsets[index].dx);
|
|
130
|
+
labelX = labelX.map((x,index) => x+spacing.offsets[index].dx);
|
|
131
|
+
itemY = itemY.map((y,index) => y+spacing.offsets[index].dy);
|
|
132
|
+
title = {...title,x:title.x+spacing.titleOffset.dx,y:title.y+spacing.titleOffset.dy};
|
|
133
|
+
bounds = spacing.bounds;
|
|
134
|
+
}
|
|
119
135
|
if (config.layout === "legacy-bottom") {
|
|
120
136
|
const itemBounds = titleVisible ? bounds.slice(1) : bounds;
|
|
121
137
|
if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const shift = (bounds, {dx, dy}) => ({
|
|
2
|
+
left:bounds.left + dx, right:bounds.right + dx,
|
|
3
|
+
top:bounds.top + dy, bottom:bounds.bottom + dy
|
|
4
|
+
});
|
|
5
|
+
const union = bounds => ({
|
|
6
|
+
left:Math.min(...bounds.map(b => b.left)), right:Math.max(...bounds.map(b => b.right)),
|
|
7
|
+
top:Math.min(...bounds.map(b => b.top)), bottom:Math.max(...bounds.map(b => b.bottom))
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Family owners supply existing concrete text/sample bounds and logical row
|
|
11
|
+
// identities. This pure function neither reads scales nor changes a recipe.
|
|
12
|
+
export function resolveLegendContentSpacing(config, items, titleBounds) {
|
|
13
|
+
if (config.rowPadding === undefined && config.titlePadding === undefined) return undefined;
|
|
14
|
+
if (items.length === 0) throw new Error("Legend content spacing requires a body item.");
|
|
15
|
+
const offsets = items.map(() => ({dx:0,dy:0}));
|
|
16
|
+
const titleOffset = {dx:0,dy:0};
|
|
17
|
+
const boundsFor = (item, index) => item.bounds.length > 0 ? item.bounds
|
|
18
|
+
: [{left:item.x,right:item.x,top:item.y,bottom:item.y}];
|
|
19
|
+
const originalBody = items.flatMap(boundsFor);
|
|
20
|
+
const original = union([...originalBody, ...(titleBounds === undefined ? [] : [titleBounds])]);
|
|
21
|
+
if (config.rowPadding !== undefined) {
|
|
22
|
+
const rows = new Map();
|
|
23
|
+
items.forEach((item,index) => {
|
|
24
|
+
const row = rows.get(item.row) ?? {indices:[],bounds:[]};
|
|
25
|
+
row.indices.push(index);row.bounds.push(...boundsFor(item,index));rows.set(item.row,row);
|
|
26
|
+
});
|
|
27
|
+
let previousBottom;
|
|
28
|
+
for (const [,row] of [...rows.entries()].sort(([a],[b]) => a-b)) {
|
|
29
|
+
const occupied = union(row.bounds);
|
|
30
|
+
const dy = previousBottom === undefined ? 0 : previousBottom + config.rowPadding - occupied.top;
|
|
31
|
+
for (const index of row.indices) offsets[index].dy = dy;
|
|
32
|
+
previousBottom = occupied.bottom + dy;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const body = () => items.flatMap((item,index) => boundsFor(item,index).map(b => shift(b,offsets[index])));
|
|
36
|
+
if (titleBounds !== undefined && config.titlePadding !== undefined) {
|
|
37
|
+
const occupied = union(body());
|
|
38
|
+
if (config.titlePosition === "left") {
|
|
39
|
+
const dx = titleBounds.right + config.titlePadding - occupied.left;
|
|
40
|
+
for (const offset of offsets) offset.dx += dx;
|
|
41
|
+
} else if (config.titlePosition === "bottom") {
|
|
42
|
+
titleOffset.dy = occupied.bottom + config.titlePadding - titleBounds.top;
|
|
43
|
+
} else {
|
|
44
|
+
const dy = titleBounds.bottom + config.titlePadding - occupied.top;
|
|
45
|
+
for (const offset of offsets) offset.dy += dy;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const occupied = [...body(), ...(titleBounds === undefined ? [] : [shift(titleBounds,titleOffset)])];
|
|
49
|
+
if (occupied.some(bounds => Object.values(bounds).some(value => !Number.isFinite(value)))) {
|
|
50
|
+
throw new RangeError("Legend content spacing requires finite occupied bounds.");
|
|
51
|
+
}
|
|
52
|
+
const beforeAnchor = union(occupied);
|
|
53
|
+
const right = config.position === "left" || config.position?.endsWith("right");
|
|
54
|
+
const horizontal = ["top","bottom"].includes(config.position);
|
|
55
|
+
const align = horizontal ? config.align ?? "center" : right ? "right" : "left";
|
|
56
|
+
const coordinate = (bounds, side) => side === "center" ? (bounds.left+bounds.right)/2 : bounds[side];
|
|
57
|
+
const dx = coordinate(original,align) - coordinate(beforeAnchor,align);
|
|
58
|
+
const bottom = config.position === "top" || config.position?.startsWith("bottom-");
|
|
59
|
+
const dy = bottom ? original.bottom - beforeAnchor.bottom : original.top - beforeAnchor.top;
|
|
60
|
+
for (const offset of offsets) {offset.dx += dx;offset.dy += dy;}
|
|
61
|
+
titleOffset.dx += dx;titleOffset.dy += dy;
|
|
62
|
+
if ([...offsets,titleOffset].some(offset => !Number.isFinite(offset.dx) || !Number.isFinite(offset.dy))) {
|
|
63
|
+
throw new RangeError("Legend content spacing requires finite offsets.");
|
|
64
|
+
}
|
|
65
|
+
return {offsets,titleOffset,bounds:[...body(),...(titleBounds === undefined ? [] : [shift(titleBounds,titleOffset)])]};
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function resolvePlotDrawOrder({ children, grids, border, axes }) {
|
|
2
|
+
const present = new Set(children);
|
|
3
|
+
const gridOrder = grids.filter(grid => present.has(grid.id)).sort((a, b) =>
|
|
4
|
+
(a.zIndex ?? 0) - (b.zIndex ?? 0) || a.index - b.index);
|
|
5
|
+
const axisOrder = axes.map(axis => ({ ...axis, ids: children.filter(id => axis.ids.includes(id)) }));
|
|
6
|
+
const borderId = present.has(border?.id) ? [border.id] : [];
|
|
7
|
+
const owned = new Set([...gridOrder.map(grid => grid.id), ...borderId, ...axisOrder.flatMap(axis => axis.ids)]);
|
|
8
|
+
return [
|
|
9
|
+
...gridOrder.filter(grid => (grid.zIndex ?? 0) <= 0).map(grid => grid.id),
|
|
10
|
+
...(border?.underlay ? borderId : []),
|
|
11
|
+
...axisOrder.filter(axis => axis.underlay).flatMap(axis => axis.ids),
|
|
12
|
+
...children.filter(id => !owned.has(id)),
|
|
13
|
+
...gridOrder.filter(grid => (grid.zIndex ?? 0) > 0).map(grid => grid.id),
|
|
14
|
+
...(border?.underlay ? [] : borderId),
|
|
15
|
+
...axisOrder.filter(axis => !axis.underlay).flatMap(axis => axis.ids)
|
|
16
|
+
];
|
|
17
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { aggregateRows } from "../../grammar/aggregate.js";
|
|
1
2
|
import { resolveBarCellOpacities } from "./resolve.js";
|
|
2
3
|
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
3
4
|
import {
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
validateWeightedNumericFields
|
|
26
27
|
} from "../../grammar/weightedStatistics.js";
|
|
27
28
|
|
|
28
|
-
export function resolveHistogramInput(dataset, xEncoding) {
|
|
29
|
+
export function resolveHistogramInput(dataset, xEncoding, yEncoding) {
|
|
29
30
|
const allValues = readQuantitativeField(dataset.values, xEncoding.field);
|
|
30
31
|
if (xEncoding.weight === undefined) {
|
|
31
32
|
return {
|
|
@@ -33,6 +34,11 @@ export function resolveHistogramInput(dataset, xEncoding) {
|
|
|
33
34
|
values: allValues,
|
|
34
35
|
entries: dataset.values.map((row, index) => ({ row, index })),
|
|
35
36
|
mass(entries) {
|
|
37
|
+
if (yEncoding?.aggregate !== undefined && yEncoding.aggregate !== "count") {
|
|
38
|
+
// Empty/insufficient samples have no visible segment, just like zero
|
|
39
|
+
// count bins. Use the same native aggregate owner as category bars.
|
|
40
|
+
return aggregateRows(entries.map(entry => entry.row), yEncoding.field, yEncoding.aggregate) ?? 0;
|
|
41
|
+
}
|
|
36
42
|
return entries.length;
|
|
37
43
|
}
|
|
38
44
|
};
|
|
@@ -65,7 +71,7 @@ export function deriveHistogramSegments({
|
|
|
65
71
|
xScale,
|
|
66
72
|
resolvedScales
|
|
67
73
|
}) {
|
|
68
|
-
const input = resolveHistogramInput(dataset, xEncoding);
|
|
74
|
+
const input = resolveHistogramInput(dataset, xEncoding, layer.encoding.y);
|
|
69
75
|
const { rows, values: xValues, entries } = input;
|
|
70
76
|
const bins = resolveHistogramBins({
|
|
71
77
|
values: xValues,
|
|
@@ -82,7 +82,7 @@ export function requireCompleteBar(program, id) {
|
|
|
82
82
|
grain === undefined
|
|
83
83
|
) {
|
|
84
84
|
throw new Error(
|
|
85
|
-
`Bar mark "${id}" requires binned x/
|
|
85
|
+
`Bar mark "${id}" requires binned x/aggregate y or ordinal x/aggregate y encodings.`
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
const xScale = findSemanticScale(program, xEncoding.scale);
|
|
@@ -103,6 +103,7 @@ export function attachSnapshotObject(program, snapshot, id, parent) {
|
|
|
103
103
|
value: itemDefinitions(object)
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
+
if (object.clip !== undefined) next = next.editGraphics({target: id, property: "clip", value: object.clip});
|
|
106
107
|
for (const childId of object.children ?? []) {
|
|
107
108
|
next = attachSnapshotObject(next, snapshot, childId, id);
|
|
108
109
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneAndFreeze, freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
1
|
+
import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { namespaceGraphicId } from "../selectors/graphicCapacity.js";
|
|
3
3
|
export { namespaceGraphicId } from "../selectors/graphicCapacity.js";
|
|
4
4
|
import {
|
|
@@ -6,6 +6,17 @@ import {
|
|
|
6
6
|
walkGraphicTreeEvents
|
|
7
7
|
} from "../grammar/schemas/graphicTree.js";
|
|
8
8
|
|
|
9
|
+
const snapshots = new WeakMap();
|
|
10
|
+
|
|
11
|
+
function placeSnapshot(snapshot, placement) {
|
|
12
|
+
const root = snapshot.order[0], canvas = snapshot.objects[root];
|
|
13
|
+
if (canvas.properties.x === placement.x && canvas.properties.y === placement.y) return snapshot;
|
|
14
|
+
return freezeOwned({ order: snapshot.order, objects: freezeOwned({
|
|
15
|
+
...snapshot.objects,
|
|
16
|
+
[root]: freezeOwned({ ...canvas, properties: freezeOwned({ ...canvas.properties, ...placement }) })
|
|
17
|
+
}) });
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
function validateNamespace(namespace) {
|
|
10
21
|
if (typeof namespace !== "string" || namespace.length === 0) {
|
|
11
22
|
throw new TypeError("Graphic snapshot namespace must be a non-empty string.");
|
|
@@ -84,6 +95,8 @@ export function namespaceGraphicSnapshot(
|
|
|
84
95
|
) {
|
|
85
96
|
throw new TypeError("Graphic snapshot requires objects and order.");
|
|
86
97
|
}
|
|
98
|
+
const cached = snapshots.get(graphicSpec)?.get(resolvedNamespace);
|
|
99
|
+
if (cached) return placeSnapshot(cached, placement);
|
|
87
100
|
walkGraphicTreeEvents(graphicSpec, {});
|
|
88
101
|
const { id: canvasId, object: canvas } =
|
|
89
102
|
requireSingleOrderedGraphicByType(graphicSpec, "canvas");
|
|
@@ -102,8 +115,15 @@ export function namespaceGraphicSnapshot(
|
|
|
102
115
|
]
|
|
103
116
|
));
|
|
104
117
|
const rootId = namespaceGraphicId(resolvedNamespace, canvasId);
|
|
105
|
-
|
|
118
|
+
const snapshot = freezeOwned({
|
|
106
119
|
objects: freezeOwned(objects),
|
|
107
120
|
order: freezeOwned([rootId])
|
|
108
121
|
});
|
|
122
|
+
if (isOwned(graphicSpec)) {
|
|
123
|
+
if (!snapshots.has(graphicSpec)) snapshots.set(graphicSpec, new Map());
|
|
124
|
+
const cache = snapshots.get(graphicSpec);
|
|
125
|
+
cache.set(resolvedNamespace, snapshot);
|
|
126
|
+
if (cache.size > 16) cache.delete(cache.keys().next().value);
|
|
127
|
+
}
|
|
128
|
+
return snapshot;
|
|
109
129
|
}
|
|
@@ -59,7 +59,7 @@ export function findUpstreamTransform(program, dataset, type) {
|
|
|
59
59
|
visited.add(current.id);
|
|
60
60
|
if (current.transform?.length !== 1) return undefined;
|
|
61
61
|
const transform = current.transform[0];
|
|
62
|
-
if (transform.type === type) return transform;
|
|
62
|
+
if (Array.isArray(type) ? type.includes(transform.type) : transform.type === type) return transform;
|
|
63
63
|
if (findTransformTopology(transform.type)?.provenanceTransparent !== true) {
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
@@ -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
|
+
}
|