ggaction 0.0.17 → 0.0.18
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 +46 -0
- package/README.md +2 -2
- 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/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/regression.js +2 -1
- package/src/actions/data/shared.js +3 -2
- package/src/actions/data/stack.js +3 -1
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +8 -4
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +3 -3
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +8 -3
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +81 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +3 -7
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +7 -1
- package/src/grammar/regression/models.js +83 -55
- package/src/grammar/regression/parameters.js +14 -3
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +3 -26
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +5 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +18 -5
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +347 -85
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import { readScaleField } from "../../grammar/scales/index.js";
|
|
2
|
+
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
3
|
+
|
|
4
|
+
// Appearance belongs to a final bar cell and never introduces another group.
|
|
5
|
+
export function resolveBarCellOpacities(program, layer, rowsByCell) {
|
|
6
|
+
const encoding = layer.encoding?.opacity;
|
|
7
|
+
if (encoding === undefined) return undefined;
|
|
8
|
+
const scale = program.resolvedScales[encoding.scale];
|
|
9
|
+
const values = rowsByCell.map(rows => {
|
|
10
|
+
const values = readScaleField(rows, encoding.field, encoding.fieldType, {
|
|
11
|
+
allowUnknown: Object.hasOwn(scale, "unknown")
|
|
12
|
+
});
|
|
13
|
+
if (new Set(values).size !== 1) {
|
|
14
|
+
throw new Error("Bar opacity requires one value within each final cell.");
|
|
15
|
+
}
|
|
16
|
+
return values[0];
|
|
17
|
+
});
|
|
18
|
+
return mapScaleConsumerValues(values, scale, "opacity");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import { hasRectRadius } from "../../grammar/roundedRect.js";
|
|
1
22
|
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
2
23
|
import { findDataset } from "../../selectors/datasets.js";
|
|
3
24
|
import { findLayer } from "../../selectors/layers.js";
|
|
@@ -45,7 +66,7 @@ export function requireCompleteBar(program, id) {
|
|
|
45
66
|
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
46
67
|
const graphic = program.graphicSpec.objects[id];
|
|
47
68
|
const roundedCollection = graphic?.type === "collection" &&
|
|
48
|
-
|
|
69
|
+
hasRectRadius(program.markConfigs[id]?.barAppearance) &&
|
|
49
70
|
graphic.items?.every(child => ["rect", "path"].includes(child.type));
|
|
50
71
|
if (graphic?.type !== "rect" && !roundedCollection) {
|
|
51
72
|
throw new Error(`Bar mark "${id}" requires rect graphics.`);
|
|
@@ -202,3 +202,11 @@ export function resolveGuideCoordinateBounds(program, {
|
|
|
202
202
|
}
|
|
203
203
|
return resolveCoordinateBounds(program, target, { resolvedScales });
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
// Cartesian guide bindings are semantic scale consumers without data rows.
|
|
207
|
+
export function axisScaleBindings(program, id) {
|
|
208
|
+
return Object.entries(program.semanticSpec.guides.axis ?? {}).flatMap(([channel, guide]) =>
|
|
209
|
+
["x", "y"].includes(channel) && guide.scale === id && guide.coordinate !== undefined &&
|
|
210
|
+
program.materializationConfigs?.guides?.axis?.[channel]?.binding?.standalone === true
|
|
211
|
+
? [{ channel, coordinate: guide.coordinate }] : []);
|
|
212
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { withGuideLayoutTransaction } from "./guides/layout.js";
|
|
2
2
|
import {
|
|
3
|
-
canDeferScaleConsumerApplication,
|
|
4
3
|
getLayerScaleIds,
|
|
5
4
|
getExistingMarkRematerializationStep,
|
|
6
5
|
getMarkMaterializationStep,
|
|
@@ -13,54 +12,13 @@ import {
|
|
|
13
12
|
applyMaterializationPlan as executeMaterializationPlan,
|
|
14
13
|
buildMaterializationPlan
|
|
15
14
|
} from "./planner.js";
|
|
16
|
-
import { hasMaterializedLegend } from "./legends.js";
|
|
17
15
|
import {
|
|
18
|
-
needsCanvasScaleRematerialization,
|
|
19
16
|
planScaleGuideRematerialization
|
|
20
17
|
} from "./scaleGuideDependencies.js";
|
|
21
18
|
import { planLayoutRematerialization } from "./layout.js";
|
|
22
19
|
import { requireCoordinate } from "../selectors/coordinates.js";
|
|
23
20
|
|
|
24
|
-
export
|
|
25
|
-
const marks = [];
|
|
26
|
-
for (const layer of program.semanticSpec.layers) {
|
|
27
|
-
const step = getMarkMaterializationStep(program, layer);
|
|
28
|
-
if (step !== undefined) marks.push(step);
|
|
29
|
-
}
|
|
30
|
-
const deferredMarkIds = new Set(
|
|
31
|
-
marks
|
|
32
|
-
.map(step => step.args.id)
|
|
33
|
-
);
|
|
34
|
-
const scales = [];
|
|
35
|
-
for (const scale of program.semanticSpec.scales) {
|
|
36
|
-
if (needsCanvasScaleRematerialization(program, scale)) {
|
|
37
|
-
const deferredConsumers = program.semanticSpec.layers.filter(layer =>
|
|
38
|
-
canDeferScaleConsumerApplication(layer) &&
|
|
39
|
-
getLayerScaleIds(layer).includes(scale.id)
|
|
40
|
-
);
|
|
41
|
-
const canDeferMarks = deferredConsumers.length === 0 ||
|
|
42
|
-
deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
|
|
43
|
-
scales.push({
|
|
44
|
-
op: "rematerializeScale",
|
|
45
|
-
args: {
|
|
46
|
-
id: scale.id,
|
|
47
|
-
guides: false,
|
|
48
|
-
...(canDeferMarks ? { marks: false } : {})
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const guides = program.semanticSpec.scales.flatMap(scale =>
|
|
54
|
-
needsCanvasScaleRematerialization(program, scale)
|
|
55
|
-
? planScaleGuideRematerialization(program, scale.id)
|
|
56
|
-
: []
|
|
57
|
-
);
|
|
58
|
-
if (hasMaterializedLegend(program)) {
|
|
59
|
-
guides.push({ op: "rematerializeLegend" });
|
|
60
|
-
}
|
|
61
|
-
const layout = planLayoutRematerialization(program);
|
|
62
|
-
return buildMaterializationPlan({ scales, marks, guides, layout });
|
|
63
|
-
}
|
|
21
|
+
export { planCanvasRematerialization } from "./layout.js";
|
|
64
22
|
|
|
65
23
|
export function planCoordinateRematerialization(program, target) {
|
|
66
24
|
requireCoordinate(program, target);
|
|
@@ -74,7 +32,9 @@ export function planCoordinateRematerialization(program, target) {
|
|
|
74
32
|
...(layer.encoding?.parallel?.dimensions ?? []).map(
|
|
75
33
|
dimension => dimension.scale
|
|
76
34
|
)
|
|
77
|
-
]).filter(id => id !== undefined))
|
|
35
|
+
]).filter(id => id !== undefined)),
|
|
36
|
+
...Object.values(program.semanticSpec.guides.axis ?? {}).filter(guide => guide.coordinate === target)
|
|
37
|
+
.map(guide => guide.scale).filter(id => id !== undefined)];
|
|
78
38
|
const directMarks = layers
|
|
79
39
|
.map(layer => getMarkMaterializationStep(program, layer))
|
|
80
40
|
.filter(step => step !== undefined);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{PLOT_LEGEND_POSITIONS as B}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as G}from"../../grammar/schemas/graphicBounds.js";import{resolveFacetOuterAxes as I}from"../../grammar/facets/guides.js";import{resolveTextBounds as L,textBoundsIntersect as S}from"../../core/textMetrics.js";import{axisGraphicIds as y}from"../guides/resources.js";import{resolveFacetHeaderItems as F}from"../facetHeaders.js";function p(s,t,e){return{left:s.left+t,right:s.right+t,top:s.top+e,bottom:s.bottom+e}}function P(s,t,e,m){const c=s.materializationConfigs.textMetrics,d=I({placements:t.children}),i=[];for(const n of t.children){const a=s.children[n.id];for(const g of["x","y"])if(!(s.compositionSpec.facet.guides.axes==="outer"&&!d.children[n.id][g]))for(const f of y(g)){if(!/Labels|Title/.test(f)||a.graphicSpec.objects[f]===void 0)continue;const x=G(a.graphicSpec,f,c);x!==void 0&&i.push(p(x,n.x,n.y))}}const{items:h}=F(t,e,m);for(const n of h)n.properties.text!==""&&i.push(L(n.properties,c));return i}function w(s,t,e,m,c){const d=s.right-s.left,i=s.bottom-s.top,h=e==="right"?t.x+t.width+c:e==="left"?t.x-c-d:m==="left"?t.x:m==="right"?t.x+t.width-d:t.x+(t.width-d)/2,n=["left","right"].includes(e)?t.y:e==="top"?t.y-c-i:t.y+t.height+c;return{left:h,right:h+d,top:n,bottom:n+i}}function j(s,{layout:t,plot:e,plots:m,title:c,preparedHeaders:d,prepared:i}){if(i?.mode!=="promoted")return{layout:t,plot:e};const h=i.occupiedGroup;if(h?.placement.at!==void 0||B.includes(h?.placement.position))return{layout:t,plot:e};const n=i.reservation.position,a=i.reservation.align,g=i.kinds.map(r=>i.source.guideConfigs.legend[r]),f=i.reservation.gap,x=h===void 0?g.every(r=>r.inferredOffset===!0):h.inferredOffset===!0;let o=w(i.bounds,e,n,a,f);const v=P(s,t,m,d),b=v.map(r=>n==="right"?r.right-(e.x+e.width):n==="left"?e.x-r.left:n==="top"?e.y-r.top:r.bottom-(e.y+e.height));if(x)o=w(i.bounds,e,n,a,Math.max(f,...b.map(r=>r+f)));else if(v.some(r=>S(o,r)))throw new Error("Facet shared legend overlaps retained axes or headers.");const u=Math.max(0,t.padding.left-o.left),l=Math.max(0,c.height+t.padding.top-o.top),M=t.children.map(r=>({...r,x:r.x+u,y:r.y+l})),O={...e,x:e.x+u,y:e.y+l};return o=p(o,u,l),{plot:O,layout:{...t,children:M,width:Math.ceil(Math.max(t.width+u,o.right+t.padding.right)),height:Math.ceil(Math.max(t.height+l,o.bottom+t.padding.bottom)),legend:{position:n,x:o.left,y:o.top,width:o.right-o.left,height:o.bottom-o.top,translation:{x:o.left-i.bounds.left,y:o.top-i.bounds.top}}}}}export{j as resolvePromotedFacetLegendLayout};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
|
|
1
2
|
import { isPlainObject } from "../../core/immutable.js";
|
|
2
3
|
import { resolveConcreteGraphicBounds } from
|
|
3
4
|
"../../grammar/schemas/graphicBounds.js";
|
|
@@ -18,6 +19,9 @@ function copyLegendConfig(program, source) {
|
|
|
18
19
|
for (const [kind, config] of Object.entries(source.guideConfigs.legend ?? {})) {
|
|
19
20
|
next = next._withLegendConfig(kind, config);
|
|
20
21
|
}
|
|
22
|
+
for (const [target, group] of Object.entries(source.guideConfigs.legendGroups ?? {})) {
|
|
23
|
+
next = next._withMaterializationConfig(["guides", "legendGroups", target], group);
|
|
24
|
+
}
|
|
21
25
|
return next;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -33,6 +37,12 @@ function removeNamespacedGraphics(program, namespace, ids) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
function legendTranslation(prepared, plot, layout) {
|
|
40
|
+
if (layout.legend?.translation !== undefined) return layout.legend.translation;
|
|
41
|
+
if (prepared.occupiedGroup !== undefined) {
|
|
42
|
+
const [placement] = resolveLegendGroupPlacement({ blocks: [{ id: "shared", bounds: prepared.bounds }],
|
|
43
|
+
group: prepared.occupiedGroup, plot, canvas: layout });
|
|
44
|
+
return { x: placement.dx, y: placement.dy };
|
|
45
|
+
}
|
|
36
46
|
const position = prepared.reservation.position;
|
|
37
47
|
if (["top", "bottom"].includes(position)) {
|
|
38
48
|
const width = prepared.bounds.right - prepared.bounds.left;
|
|
@@ -55,18 +65,16 @@ function legendTranslation(prepared, plot, layout) {
|
|
|
55
65
|
const strips = prepared.source.graphicSpec.objects[`${prefix}Strips`];
|
|
56
66
|
if (strips?.items?.[0] !== undefined) {
|
|
57
67
|
const properties = strips.items[0].properties;
|
|
58
|
-
const length = prepared.source.guideConfigs.legend[gradientKind].gradient.length;
|
|
59
68
|
return {
|
|
60
69
|
x: layout.legend.x - (position === "left"
|
|
61
70
|
? prepared.bounds.left
|
|
62
71
|
: properties.x),
|
|
63
|
-
y: plot.y
|
|
72
|
+
y: plot.y - prepared.bounds.top
|
|
64
73
|
};
|
|
65
74
|
}
|
|
66
|
-
const height = prepared.bounds.bottom - prepared.bounds.top;
|
|
67
75
|
return {
|
|
68
76
|
x: layout.legend.x - prepared.bounds.left,
|
|
69
|
-
y: plot.y
|
|
77
|
+
y: plot.y - prepared.bounds.top
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -91,7 +99,7 @@ function attachParentLegend(program, prepared, plot, layout) {
|
|
|
91
99
|
return next;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
function assertSharedLegendFits(program, id, layout, plot) {
|
|
102
|
+
function assertSharedLegendFits(program, id, layout, plot, occupiedGroup) {
|
|
95
103
|
const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
|
|
96
104
|
if (bounds === undefined) {
|
|
97
105
|
throw new Error("Facet shared legend requires measurable concrete bounds.");
|
|
@@ -102,7 +110,7 @@ function assertSharedLegendFits(program, id, layout, plot) {
|
|
|
102
110
|
Math.max(bounds.left, plot.x) &&
|
|
103
111
|
Math.min(bounds.bottom, plot.y + plot.height) >
|
|
104
112
|
Math.max(bounds.top, plot.y);
|
|
105
|
-
if (outside || overlapsPlot) {
|
|
113
|
+
if (outside || overlapsPlot && occupiedGroup === undefined) {
|
|
106
114
|
throw new Error(
|
|
107
115
|
"Facet shared legend requires more space than the composed Canvas provides."
|
|
108
116
|
);
|
|
@@ -153,10 +161,13 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
|
|
|
153
161
|
let id;
|
|
154
162
|
if (prepared.mode === "legacyCategorical") {
|
|
155
163
|
id = `${program.compositionSpec.id}-legend`;
|
|
156
|
-
next = materializeLegacyCategoricalLegend(next, prepared,
|
|
164
|
+
next = materializeLegacyCategoricalLegend(next, prepared, {
|
|
165
|
+
...layout,
|
|
166
|
+
legend: { ...layout.legend, y: plot.y }
|
|
167
|
+
});
|
|
157
168
|
} else {
|
|
158
169
|
id = `${program.compositionSpec.id}-shared-legend`;
|
|
159
170
|
next = attachParentLegend(next, prepared, plot, layout);
|
|
160
171
|
}
|
|
161
|
-
return assertSharedLegendFits(next, id, layout, plot);
|
|
172
|
+
return assertSharedLegendFits(next, id, layout, plot, prepared.occupiedGroup);
|
|
162
173
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PLOT_LEGEND_POSITIONS } from "../../layout/legend.js";
|
|
2
|
+
import { isContinuousColorScaleType } from "../../grammar/scales/types.js";
|
|
1
3
|
import {
|
|
2
4
|
unionConcreteGraphicBounds
|
|
3
5
|
} from "../../grammar/schemas/graphicBounds.js";
|
|
@@ -109,7 +111,7 @@ function prepareAutoLegendSource(child) {
|
|
|
109
111
|
request.channels.includes("color") ? "color" : "stroke"
|
|
110
112
|
]?.scale
|
|
111
113
|
);
|
|
112
|
-
const gradient = scale?.type
|
|
114
|
+
const gradient = isContinuousColorScaleType(scale?.type);
|
|
113
115
|
const gradientPrefix = request.channels.includes("color")
|
|
114
116
|
? "colorGradient"
|
|
115
117
|
: "strokeGradient";
|
|
@@ -123,7 +125,7 @@ function prepareAutoLegendSource(child) {
|
|
|
123
125
|
titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
|
|
124
126
|
} : {})
|
|
125
127
|
});
|
|
126
|
-
if (gradient) {
|
|
128
|
+
if (gradient && scale.type === "sequential") {
|
|
127
129
|
const resolved = source.resolvedScales[scale.id];
|
|
128
130
|
source = source
|
|
129
131
|
.editGraphics({
|
|
@@ -201,8 +203,13 @@ export function prepareSharedFacetLegend(program) {
|
|
|
201
203
|
if (bounds === undefined) {
|
|
202
204
|
throw new Error("Shared facet legend has no measurable concrete bounds.");
|
|
203
205
|
}
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
+
const targets = new Set(kinds.map(kind => source.guideConfigs.legend[kind].target));
|
|
207
|
+
const group = targets.size === 1 ? source.guideConfigs.legendGroups?.[[...targets][0]] : undefined;
|
|
208
|
+
const insideGroup = group !== undefined && (group.placement.at !== undefined || PLOT_LEGEND_POSITIONS.includes(group.placement.position));
|
|
209
|
+
const placement = group === undefined ? promotedLegendPlacement(source, kinds)
|
|
210
|
+
: { position: insideGroup ? "right" : group.placement.position,
|
|
211
|
+
align: group.placement.align ?? "left" };
|
|
212
|
+
|
|
206
213
|
const width = Math.ceil(bounds.right - bounds.left);
|
|
207
214
|
return {
|
|
208
215
|
mode: "promoted",
|
|
@@ -212,11 +219,12 @@ export function prepareSharedFacetLegend(program) {
|
|
|
212
219
|
roots,
|
|
213
220
|
bounds,
|
|
214
221
|
compatibility,
|
|
222
|
+
...(group === undefined ? {} : { occupiedGroup: group }),
|
|
215
223
|
reservation: {
|
|
216
224
|
...placement,
|
|
217
|
-
gap:
|
|
218
|
-
width:
|
|
219
|
-
height: Math.ceil(bounds.bottom - bounds.top)
|
|
225
|
+
gap: insideGroup ? 0 : group?.placement.offset ?? Math.max(...kinds.map(kind => source.guideConfigs.legend[kind].offset)),
|
|
226
|
+
width: insideGroup ? 0 : width,
|
|
227
|
+
height: insideGroup ? 0 : Math.ceil(bounds.bottom - bounds.top)
|
|
220
228
|
}
|
|
221
229
|
};
|
|
222
230
|
}
|
|
@@ -1,347 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
formatVisibleText,
|
|
4
|
-
measureTextWidth,
|
|
5
|
-
resolveTextBounds,
|
|
6
|
-
textBoundsIntersect
|
|
7
|
-
} from "../core/textMetrics.js";
|
|
8
|
-
import { resolveDisplayLabel } from "../grammar/displayLabels.js";
|
|
9
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
|
|
10
|
-
|
|
11
|
-
const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
12
|
-
const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
13
|
-
|
|
14
|
-
export function createDefaultFacetHeaders() {
|
|
15
|
-
return {
|
|
16
|
-
mode: "legacy",
|
|
17
|
-
common: {
|
|
18
|
-
fontSize: 12,
|
|
19
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
20
|
-
fontWeight: 600,
|
|
21
|
-
color: DEFAULT_COLORS.strongText,
|
|
22
|
-
offset: 10,
|
|
23
|
-
align: "center"
|
|
24
|
-
},
|
|
25
|
-
row: { side: "left" },
|
|
26
|
-
column: { side: "top" }
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function normalizeFacetHeadersConfig(value) {
|
|
31
|
-
if (!isPlainObject(value)) {
|
|
32
|
-
throw new TypeError("Facet headers config must be a plain object.");
|
|
33
|
-
}
|
|
34
|
-
if (value.common === undefined && value.mode === undefined) {
|
|
35
|
-
return {
|
|
36
|
-
...createDefaultFacetHeaders(),
|
|
37
|
-
common: {
|
|
38
|
-
...createDefaultFacetHeaders().common,
|
|
39
|
-
...value
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (!["legacy", "roles"].includes(value.mode) ||
|
|
44
|
-
!isPlainObject(value.common) || !isPlainObject(value.row) ||
|
|
45
|
-
!isPlainObject(value.column)) {
|
|
46
|
-
throw new Error("Facet headers config must contain canonical mode and role objects.");
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
mode: value.mode,
|
|
50
|
-
common: { ...value.common },
|
|
51
|
-
row: { ...value.row },
|
|
52
|
-
column: { ...value.column }
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function resolveFacetHeaderConfig(headers, role) {
|
|
57
|
-
const normalized = normalizeFacetHeadersConfig(headers);
|
|
58
|
-
return role === "all"
|
|
59
|
-
? { ...normalized.common }
|
|
60
|
-
: { ...normalized.common, ...normalized[role] };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function mappedText(value, config) {
|
|
64
|
-
return resolveDisplayLabel(value, config.labelMap, formatVisibleText);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function gridCellById(spec) {
|
|
68
|
-
return new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function legacyDescriptors(program, headers) {
|
|
72
|
-
const spec = program.compositionSpec;
|
|
73
|
-
const config = resolveFacetHeaderConfig(headers, "all");
|
|
74
|
-
const cells = gridCellById(spec);
|
|
75
|
-
return spec.children.map((id, index) => {
|
|
76
|
-
const cell = cells.get(id);
|
|
77
|
-
const text = cell === undefined
|
|
78
|
-
? mappedText(spec.facet.values[index], config)
|
|
79
|
-
: `${mappedText(cell.rowValue, config)} · ${mappedText(cell.columnValue, config)}`;
|
|
80
|
-
return { topology: "legacy", role: "column", id, text, config };
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function uniqueGridGroups(spec, role) {
|
|
85
|
-
const coordinate = role === "column" ? "column" : "row";
|
|
86
|
-
const valueKey = role === "column" ? "columnValue" : "rowValue";
|
|
87
|
-
const groups = new Map();
|
|
88
|
-
for (const cell of spec.facet.grid.cells) {
|
|
89
|
-
const existing = groups.get(cell[coordinate]);
|
|
90
|
-
if (existing === undefined) {
|
|
91
|
-
groups.set(cell[coordinate], {
|
|
92
|
-
coordinate: cell[coordinate],
|
|
93
|
-
value: cell[valueKey],
|
|
94
|
-
ids: [cell.id]
|
|
95
|
-
});
|
|
96
|
-
} else {
|
|
97
|
-
if (existing.value !== cell[valueKey]) {
|
|
98
|
-
throw new Error(`Facet ${role} header values conflict at coordinate ${cell[coordinate]}.`);
|
|
99
|
-
}
|
|
100
|
-
existing.ids.push(cell.id);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return [...groups.values()].sort((left, right) =>
|
|
104
|
-
left.coordinate - right.coordinate
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function roleDescriptors(program, headers) {
|
|
109
|
-
const spec = program.compositionSpec;
|
|
110
|
-
const column = resolveFacetHeaderConfig(headers, "column");
|
|
111
|
-
if (spec.facet.grid === undefined) {
|
|
112
|
-
return spec.children.map((id, index) => ({
|
|
113
|
-
topology: "cell-column",
|
|
114
|
-
role: "column",
|
|
115
|
-
id,
|
|
116
|
-
ids: [id],
|
|
117
|
-
row: Math.floor(index / spec.columns),
|
|
118
|
-
text: mappedText(spec.facet.values[index], column),
|
|
119
|
-
config: column
|
|
120
|
-
}));
|
|
121
|
-
}
|
|
122
|
-
const row = resolveFacetHeaderConfig(headers, "row");
|
|
123
|
-
const columns = uniqueGridGroups(spec, "column").map(group => ({
|
|
124
|
-
topology: "grid-column",
|
|
125
|
-
role: "column",
|
|
126
|
-
ids: group.ids,
|
|
127
|
-
coordinate: group.coordinate,
|
|
128
|
-
text: mappedText(group.value, column),
|
|
129
|
-
config: column
|
|
130
|
-
}));
|
|
131
|
-
const rows = uniqueGridGroups(spec, "row").map(group => ({
|
|
132
|
-
topology: "grid-row",
|
|
133
|
-
role: "row",
|
|
134
|
-
ids: group.ids,
|
|
135
|
-
coordinate: group.coordinate,
|
|
136
|
-
text: mappedText(group.value, row),
|
|
137
|
-
config: row
|
|
138
|
-
}));
|
|
139
|
-
return [...columns, ...rows];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function laneThickness(descriptors, role, profile) {
|
|
143
|
-
const visible = descriptors.filter(item => item.role === role && item.text !== "");
|
|
144
|
-
if (visible.length === 0) return 0;
|
|
145
|
-
if (role === "column") {
|
|
146
|
-
return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
|
|
147
|
-
}
|
|
148
|
-
return Math.max(...visible.map(item => measureTextWidth(item.text, item.config, profile) +
|
|
149
|
-
item.config.offset));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function prepareFacetHeaders(program, headersConfig) {
|
|
153
|
-
const headers = normalizeFacetHeadersConfig(headersConfig);
|
|
154
|
-
const descriptors = headers.mode === "legacy"
|
|
155
|
-
? legacyDescriptors(program, headers)
|
|
156
|
-
: roleDescriptors(program, headers);
|
|
157
|
-
const rows = program.compositionSpec.facet.grid === undefined
|
|
158
|
-
? Math.ceil(program.compositionSpec.children.length / program.compositionSpec.columns)
|
|
159
|
-
: Math.max(...program.compositionSpec.facet.grid.cells.map(cell => cell.row)) + 1;
|
|
160
|
-
const headerLayout = {
|
|
161
|
-
outer: { ...ZERO_SIDES },
|
|
162
|
-
cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
|
|
163
|
-
};
|
|
164
|
-
if (headers.mode === "roles") {
|
|
165
|
-
const column = resolveFacetHeaderConfig(headers, "column");
|
|
166
|
-
const columnThickness = laneThickness(descriptors, "column", program.materializationConfigs.textMetrics);
|
|
167
|
-
if (program.compositionSpec.facet.grid === undefined) {
|
|
168
|
-
for (const descriptor of descriptors) {
|
|
169
|
-
if (descriptor.text !== "") {
|
|
170
|
-
headerLayout.cellRows[column.side][descriptor.row] = Math.max(
|
|
171
|
-
headerLayout.cellRows[column.side][descriptor.row],
|
|
172
|
-
descriptor.config.fontSize + descriptor.config.offset
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
headerLayout.outer[column.side] = columnThickness;
|
|
178
|
-
const row = resolveFacetHeaderConfig(headers, "row");
|
|
179
|
-
headerLayout.outer[row.side] = laneThickness(descriptors, "row", program.materializationConfigs.textMetrics);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return { headers, descriptors, headerLayout };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function horizontalAnchor(bounds, align) {
|
|
186
|
-
if (align === "start") return { x: bounds.left, textAlign: "left" };
|
|
187
|
-
if (align === "end") return { x: bounds.right, textAlign: "right" };
|
|
188
|
-
return { x: (bounds.left + bounds.right) / 2, textAlign: "center" };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function verticalAnchor(bounds, align) {
|
|
192
|
-
if (align === "start") return { y: bounds.top, textBaseline: "top" };
|
|
193
|
-
if (align === "end") return { y: bounds.bottom, textBaseline: "bottom" };
|
|
194
|
-
return { y: (bounds.top + bounds.bottom) / 2, textBaseline: "middle" };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function union(items) {
|
|
198
|
-
return {
|
|
199
|
-
left: Math.min(...items.map(item => item.left)),
|
|
200
|
-
right: Math.max(...items.map(item => item.right)),
|
|
201
|
-
top: Math.min(...items.map(item => item.top)),
|
|
202
|
-
bottom: Math.max(...items.map(item => item.bottom))
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function placedPlot(cell, plot) {
|
|
207
|
-
return {
|
|
208
|
-
left: cell.x + plot.x,
|
|
209
|
-
right: cell.x + plot.x + plot.width,
|
|
210
|
-
top: cell.y + plot.y,
|
|
211
|
-
bottom: cell.y + plot.y + plot.height
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function placedCell(cell) {
|
|
216
|
-
return {
|
|
217
|
-
left: cell.x,
|
|
218
|
-
right: cell.x + cell.width,
|
|
219
|
-
top: cell.y,
|
|
220
|
-
bottom: cell.y + cell.height
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function roleItem(descriptor, cells, plots) {
|
|
225
|
-
const selectedCells = descriptor.ids.map(id => cells.get(id));
|
|
226
|
-
const missingCell = descriptor.ids.find((id, index) =>
|
|
227
|
-
selectedCells[index] === undefined
|
|
228
|
-
);
|
|
229
|
-
const missingPlot = descriptor.ids.find(id => plots.get(id) === undefined);
|
|
230
|
-
if (missingCell !== undefined || missingPlot !== undefined) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Facet header requires cell and plot bounds for "${missingCell ?? missingPlot}".`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const selectedPlots = descriptor.ids.map((id, index) =>
|
|
236
|
-
placedPlot(selectedCells[index], plots.get(id))
|
|
237
|
-
);
|
|
238
|
-
const plot = union(selectedPlots);
|
|
239
|
-
const snapshots = union(selectedCells.map(placedCell));
|
|
240
|
-
const config = descriptor.config;
|
|
241
|
-
let anchor;
|
|
242
|
-
if (descriptor.role === "column") {
|
|
243
|
-
anchor = horizontalAnchor(plot, config.align);
|
|
244
|
-
const top = config.side === "top";
|
|
245
|
-
anchor.y = top
|
|
246
|
-
? snapshots.top - config.offset - config.fontSize / 2
|
|
247
|
-
: snapshots.bottom + config.offset + config.fontSize / 2;
|
|
248
|
-
anchor.textBaseline = "middle";
|
|
249
|
-
} else {
|
|
250
|
-
anchor = verticalAnchor(plot, config.align);
|
|
251
|
-
const left = config.side === "left";
|
|
252
|
-
anchor.x = left
|
|
253
|
-
? snapshots.left - config.offset
|
|
254
|
-
: snapshots.right + config.offset;
|
|
255
|
-
anchor.textAlign = left ? "right" : "left";
|
|
256
|
-
}
|
|
257
|
-
return { descriptor, snapshots, properties: anchor };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function legacyItem(descriptor, cell, plot) {
|
|
261
|
-
if (cell === undefined || plot === undefined) {
|
|
262
|
-
throw new Error(`Facet header requires cell and plot bounds for "${descriptor.id}".`);
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
descriptor,
|
|
266
|
-
snapshots: placedCell(cell),
|
|
267
|
-
properties: {
|
|
268
|
-
...horizontalAnchor(placedPlot(cell, plot), descriptor.config.align),
|
|
269
|
-
y: cell.y + descriptor.config.offset,
|
|
270
|
-
textBaseline: "middle"
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function styledItem(item) {
|
|
276
|
-
const { config } = item.descriptor;
|
|
277
|
-
return {
|
|
278
|
-
type: "text",
|
|
279
|
-
properties: {
|
|
280
|
-
...item.properties,
|
|
281
|
-
text: item.descriptor.text,
|
|
282
|
-
fill: config.color,
|
|
283
|
-
fontSize: config.fontSize,
|
|
284
|
-
fontFamily: config.fontFamily,
|
|
285
|
-
fontWeight: config.fontWeight
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function assertRoleHeadersFit(records, items, layout, profile) {
|
|
291
|
-
const bounds = items.map((item, index) => records[index].descriptor.text === ""
|
|
292
|
-
? undefined
|
|
293
|
-
: resolveTextBounds(item.properties, profile));
|
|
294
|
-
for (const [index, current] of bounds.entries()) {
|
|
295
|
-
if (current === undefined) continue;
|
|
296
|
-
if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
|
|
297
|
-
current.top < -1e-9 || current.bottom > layout.height + 1e-9 ||
|
|
298
|
-
bounds.some((other, otherIndex) => otherIndex < index &&
|
|
299
|
-
other !== undefined && textBoundsIntersect(other, current)) ||
|
|
300
|
-
layout.children.some(cell => textBoundsIntersect(current, placedCell(cell)))) {
|
|
301
|
-
throw new Error(
|
|
302
|
-
"Facet role headers require sufficient non-overlapping reserved space."
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function assertLegacyHeadersFit(items, layout, plotById, profile) {
|
|
309
|
-
const previous = [];
|
|
310
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
311
|
-
if (items[index].properties.text === "") continue;
|
|
312
|
-
const bounds = resolveTextBounds(items[index].properties, profile);
|
|
313
|
-
const cell = layout.children[index];
|
|
314
|
-
const plot = plotById.get(cell.id);
|
|
315
|
-
if (bounds.left < 0 || bounds.right > layout.width ||
|
|
316
|
-
bounds.top < 0 || bounds.bottom > layout.height ||
|
|
317
|
-
previous.some(item => textBoundsIntersect(item, bounds)) ||
|
|
318
|
-
textBoundsIntersect(bounds, placedPlot(cell, plot))) {
|
|
319
|
-
throw new Error(
|
|
320
|
-
"Facet headers require sufficient non-overlapping space above every child plot."
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
previous.push(bounds);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function materializeFacetHeaders(program, layout, plots, prepared) {
|
|
328
|
-
const profile = program.materializationConfigs.textMetrics;
|
|
329
|
-
const cells = new Map(layout.children.map(cell => [cell.id, cell]));
|
|
330
|
-
const plotById = new Map(plots.map(plot => [plot.id, plot]));
|
|
331
|
-
const records = prepared.descriptors.map(descriptor => {
|
|
332
|
-
if (descriptor.topology === "legacy") {
|
|
333
|
-
return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
|
|
334
|
-
}
|
|
335
|
-
return roleItem(descriptor, cells, plotById);
|
|
336
|
-
});
|
|
337
|
-
const items = records.map(styledItem);
|
|
338
|
-
if (prepared.headers.mode === "legacy") {
|
|
339
|
-
assertLegacyHeadersFit(items, layout, plotById, profile);
|
|
340
|
-
} else {
|
|
341
|
-
assertRoleHeadersFit(records, items, layout, profile);
|
|
342
|
-
}
|
|
343
|
-
const id = `${program.compositionSpec.id}-headers`;
|
|
344
|
-
return program
|
|
345
|
-
.createGraphics({ id, type: "collection", parent: "canvas" })
|
|
346
|
-
.editGraphics({ target: id, property: "items", value: items });
|
|
347
|
-
}
|
|
1
|
+
import{isPlainObject as x}from"../core/immutable.js";import{formatVisibleText as E,measureTextWidth as S,resolveTextBounds as b,textBoundsIntersect as p}from"../core/textMetrics.js";import{resolveDisplayLabel as T}from"../grammar/displayLabels.js";import{DEFAULT_COLORS as I,DEFAULT_FONT_FAMILY as B}from"../theme/defaults.js";const K=Object.freeze(["top","right","bottom","left"]),D=Object.freeze({top:0,right:0,bottom:0,left:0});function F(){return{mode:"legacy",common:{fontSize:12,fontFamily:B,fontWeight:600,color:I.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function M(e){if(!x(e))throw new TypeError("Facet headers config must be a plain object.");if(e.common===void 0&&e.mode===void 0)return{...F(),common:{...F().common,...e}};if(!["legacy","roles"].includes(e.mode)||!x(e.common)||!x(e.row)||!x(e.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:e.mode,common:{...e.common},row:{...e.row},column:{...e.column}}}function g(e,t){const n=M(e);return t==="all"?{...n.common}:{...n.common,...n[t]}}function u(e,t){return T(e,t.labelMap,E)}function H(e){return new Map((e.facet.grid?.cells??[]).map(t=>[t.id,t]))}function O(e,t){const n=e.compositionSpec,s=g(t,"all"),c=H(n);return n.children.map((i,o)=>{const r=c.get(i),f=r===void 0?u(n.facet.values[o],s):`${u(r.rowValue,s)} \xB7 ${u(r.columnValue,s)}`;return{topology:"legacy",role:"column",id:i,text:f,config:s}})}function z(e,t){const n=t==="column"?"column":"row",s=t==="column"?"columnValue":"rowValue",c=new Map;for(const i of e.facet.grid.cells){const o=c.get(i[n]);if(o===void 0)c.set(i[n],{coordinate:i[n],value:i[s],ids:[i.id]});else{if(o.value!==i[s])throw new Error(`Facet ${t} header values conflict at coordinate ${i[n]}.`);o.ids.push(i.id)}}return[...c.values()].sort((i,o)=>i.coordinate-o.coordinate)}function L(e,t){const n=e.compositionSpec,s=g(t,"column");if(n.facet.grid===void 0)return n.children.map((r,f)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(f/n.columns),column:f%n.columns,text:u(n.facet.values[f],s),config:s}));const c=g(t,"row"),i=z(n,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:u(r.value,s),config:s})),o=z(n,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:u(r.value,c),config:c}));return[...i,...o]}function v(e,t,n){const s=e.filter(c=>c.role===t&&c.text!=="");return s.length===0?0:Math.max(...t==="column"?s.map(c=>c.config.fontSize+c.config.offset):s.map(c=>S(c.text,c.config,n)+c.config.offset))}function N(e,t){const n=M(t),s=n.mode==="legacy"?O(e,n):L(e,n),c=e.compositionSpec.facet.grid===void 0?Math.ceil(e.compositionSpec.children.length/e.compositionSpec.columns):Math.max(...e.compositionSpec.facet.grid.cells.map(o=>o.row))+1,i={outer:{...D},cellRows:{top:Array(c).fill(0),bottom:Array(c).fill(0)}};if(n.mode==="legacy"&&e.compositionSpec.spacing==="plot")for(const o of s){if(o.text==="")continue;const r=e.compositionSpec.children.indexOf(o.id),f=e.compositionSpec.facet.grid?.cells[r]?.row??Math.floor(r/e.compositionSpec.columns);i.cellRows.top[f]=Math.max(i.cellRows.top[f],o.config.offset+o.config.fontSize)}if(n.mode==="roles"){const o=g(n,"column"),r=v(s,"column",e.materializationConfigs.textMetrics);if(e.compositionSpec.facet.grid===void 0){const f=["left","right"].includes(o.side);f&&(i.cellColumns={left:Array(e.compositionSpec.columns).fill(0),right:Array(e.compositionSpec.columns).fill(0)});for(const l of s)if(l.text!==""){const a=f?i.cellColumns:i.cellRows,m=f?l.column:l.row,d=f?S(l.text,l.config,e.materializationConfigs.textMetrics):l.config.fontSize;a[o.side][m]=Math.max(a[o.side][m],d+l.config.offset)}}else{i.outer[o.side]=r;const f=g(n,"row");i.outer[f.side]=v(s,"row",e.materializationConfigs.textMetrics)}}return{headers:n,descriptors:s,headerLayout:i}}function A(e,t){return t==="start"?{x:e.left,textAlign:"left"}:t==="end"?{x:e.right,textAlign:"right"}:{x:(e.left+e.right)/2,textAlign:"center"}}function R(e,t){return t==="start"?{y:e.top,textBaseline:"top"}:t==="end"?{y:e.bottom,textBaseline:"bottom"}:{y:(e.top+e.bottom)/2,textBaseline:"middle"}}function C(e){return{left:Math.min(...e.map(t=>t.left)),right:Math.max(...e.map(t=>t.right)),top:Math.min(...e.map(t=>t.top)),bottom:Math.max(...e.map(t=>t.bottom))}}function h(e,t){return{left:e.x+t.x,right:e.x+t.x+t.width,top:e.y+t.y,bottom:e.y+t.y+t.height}}function y(e){return{left:e.x,right:e.x+e.width,top:e.y,bottom:e.y+e.height}}function $(e,t,n,s){const c=e.ids.map(d=>t.get(d)),i=e.ids.find((d,w)=>c[w]===void 0),o=e.ids.find(d=>n.get(d)===void 0);if(i!==void 0||o!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${i??o}".`);const r=e.ids.map((d,w)=>h(c[w],n.get(d))),f=C(r),l=s==="plot"?f:C(c.map(y)),a=e.config;let m;if(["top","bottom"].includes(a.side)){m=A(f,a.align);const d=a.side==="top";m.y=d?l.top-a.offset-a.fontSize/2:l.bottom+a.offset+a.fontSize/2,m.textBaseline="middle"}else{m=R(f,a.align);const d=a.side==="left";m.x=d?l.left-a.offset:l.right+a.offset,m.textAlign=d?"right":"left"}return{descriptor:e,snapshots:l,properties:m}}function j(e,t,n,s){if(t===void 0||n===void 0)throw new Error(`Facet header requires cell and plot bounds for "${e.id}".`);return{descriptor:e,snapshots:y(t),properties:{...A(h(t,n),e.config.align),y:s==="plot"?t.y+n.y-e.config.offset-e.config.fontSize/2:t.y+e.config.offset,textBaseline:"middle"}}}function q(e){const{config:t}=e.descriptor;return{type:"text",properties:{...e.properties,text:e.descriptor.text,fill:t.color,fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight}}}function V(e,t,n,s,c){const i=t.map((o,r)=>e[r].descriptor.text===""?void 0:b(o.properties,s));for(const[o,r]of i.entries())if(r!==void 0&&(r.left<-1e-9||r.right>n.width+1e-9||r.top<-1e-9||r.bottom>n.height+1e-9||i.some((f,l)=>l<o&&f!==void 0&&p(f,r))||n.children.some(f=>p(r,n.spacing==="plot"?h(f,c.get(f.id)):y(f)))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function G(e,t,n,s){const c=[];for(let i=0;i<e.length;i+=1){if(e[i].properties.text==="")continue;const o=b(e[i].properties,s),r=t.children[i],f=n.get(r.id);if(o.left<0||o.right>t.width||o.top<0||o.bottom>t.height||c.some(l=>p(l,o))||(t.spacing==="plot"?t.children.some(l=>p(o,h(l,n.get(l.id)))):p(o,h(r,f))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");c.push(o)}}function P(e,t,n){const s=new Map(e.children.map(o=>[o.id,o])),c=new Map(t.map(o=>[o.id,o])),i=n.descriptors.map(o=>o.topology==="legacy"?j(o,s.get(o.id),c.get(o.id),e.spacing):$(o,s,c,e.spacing));return{records:i,items:i.map(q),plotById:c}}function Y(e,t,n,s){const c=e.materializationConfigs.textMetrics,{records:i,items:o,plotById:r}=P(t,n,s);s.headers.mode==="legacy"?G(o,t,r,c):V(i,o,t,c,r);const f=`${e.compositionSpec.id}-headers`;return e.createGraphics({id:f,type:"collection",parent:"canvas"}).editGraphics({target:f,property:"items",value:o})}export{F as createDefaultFacetHeaders,Y as materializeFacetHeaders,M as normalizeFacetHeadersConfig,N as prepareFacetHeaders,g as resolveFacetHeaderConfig,P as resolveFacetHeaderItems};
|