ggaction 0.0.11 → 0.0.13
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 +80 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -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 +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +79 -24
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +153 -11
- package/src/actions/errorBars/edit.js +365 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +138 -14
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +60 -4
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +9 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +86 -23
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
package/src/layout/legendLane.js
CHANGED
|
@@ -4,6 +4,28 @@ export const SIDE_LEGEND_SYMBOL_CENTER = 16;
|
|
|
4
4
|
export const SIDE_LEGEND_LABEL_START = 44;
|
|
5
5
|
export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
|
|
6
6
|
|
|
7
|
+
export function isHorizontalEdgeLegend(config) {
|
|
8
|
+
return config !== undefined &&
|
|
9
|
+
["top", "bottom"].includes(config.position) &&
|
|
10
|
+
config.layout !== "legacy-bottom";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveSingleHorizontalLegendPlacement({ plot, canvas, config, bounds }) {
|
|
14
|
+
if (!isHorizontalEdgeLegend(config)) {
|
|
15
|
+
throw new Error("Single legend placement requires a horizontal edge layout.");
|
|
16
|
+
}
|
|
17
|
+
const dx = config.align === "left" ? plot.x - bounds.left
|
|
18
|
+
: config.align === "right" ? plot.x + plot.width - bounds.right
|
|
19
|
+
: plot.x + plot.width / 2 - midpoint(bounds.left, bounds.right);
|
|
20
|
+
const dy = config.position === "top" ? plot.y - config.offset - bounds.bottom
|
|
21
|
+
: plot.y + plot.height + config.offset - bounds.top;
|
|
22
|
+
const occupied = translateBounds(bounds, dx, dy);
|
|
23
|
+
if (occupied.left < 0 || occupied.right > canvas.width || occupied.top < 0 || occupied.bottom > canvas.height) {
|
|
24
|
+
throw new Error(`Legend layout requires more ${config.position}-margin or Canvas space.`);
|
|
25
|
+
}
|
|
26
|
+
return { dx, dy, occupied };
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
function midpoint(start, end) {
|
|
8
30
|
const sum = start + end;
|
|
9
31
|
const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
|
|
@@ -25,24 +47,25 @@ function decoration(border) {
|
|
|
25
47
|
: border.padding + border.lineWidth / 2;
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
function horizontalExtent(blocks) {
|
|
50
|
+
function horizontalExtent(blocks, labelStart) {
|
|
29
51
|
let left = 0;
|
|
30
52
|
let right = 0;
|
|
31
53
|
for (const block of blocks) {
|
|
32
|
-
|
|
54
|
+
const inset = block.inset ?? 0;
|
|
55
|
+
if (block.title !== undefined) right = Math.max(right, block.title.width + inset);
|
|
33
56
|
left = Math.min(left,
|
|
34
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX
|
|
57
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX - inset
|
|
35
58
|
);
|
|
36
59
|
right = Math.max(
|
|
37
60
|
right,
|
|
38
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX,
|
|
39
|
-
|
|
61
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX + inset,
|
|
62
|
+
labelStart + block.labels.width + inset
|
|
40
63
|
);
|
|
41
64
|
}
|
|
42
65
|
return { left, right };
|
|
43
66
|
}
|
|
44
67
|
|
|
45
|
-
function placeBlock(block, cursor) {
|
|
68
|
+
function placeBlock(block, cursor, labelStart) {
|
|
46
69
|
let dy;
|
|
47
70
|
if (block.title === undefined) {
|
|
48
71
|
dy = cursor - block.bounds.top;
|
|
@@ -65,7 +88,7 @@ function placeBlock(block, cursor) {
|
|
|
65
88
|
? undefined
|
|
66
89
|
: -block.title.x,
|
|
67
90
|
symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
|
|
68
|
-
labelDx:
|
|
91
|
+
labelDx: labelStart - block.labels.x,
|
|
69
92
|
bounds
|
|
70
93
|
};
|
|
71
94
|
}
|
|
@@ -87,7 +110,10 @@ export function resolveSideLegendLane({
|
|
|
87
110
|
}
|
|
88
111
|
const blocks = groups.flatMap(group => group.blocks);
|
|
89
112
|
if (blocks.length < 2) return undefined;
|
|
90
|
-
const
|
|
113
|
+
const labelStart = Math.max(SIDE_LEGEND_LABEL_START, ...blocks.map(
|
|
114
|
+
block => SIDE_LEGEND_SYMBOL_CENTER + Math.abs(block.labels.x - block.symbol.centerX)
|
|
115
|
+
));
|
|
116
|
+
const laneExtent = horizontalExtent(blocks, labelStart);
|
|
91
117
|
const offset = blocks.reduce(
|
|
92
118
|
(maximum, block) => Math.max(maximum, block.offset),
|
|
93
119
|
-Infinity
|
|
@@ -96,7 +122,7 @@ export function resolveSideLegendLane({
|
|
|
96
122
|
? plot.x + plot.width + offset
|
|
97
123
|
: plot.x - offset - laneExtent.right;
|
|
98
124
|
const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
|
|
99
|
-
const labelStartX = titleStartX +
|
|
125
|
+
const labelStartX = titleStartX + labelStart;
|
|
100
126
|
const first = blocks[0];
|
|
101
127
|
let cursor = first.title === undefined
|
|
102
128
|
? plot.y + 12
|
|
@@ -110,7 +136,7 @@ export function resolveSideLegendLane({
|
|
|
110
136
|
if (occupiedGroups.length > 0) cursor += inset;
|
|
111
137
|
const groupPlacements = [];
|
|
112
138
|
for (const block of group.blocks) {
|
|
113
|
-
const placement = placeBlock(block, cursor);
|
|
139
|
+
const placement = placeBlock(block, cursor, labelStart);
|
|
114
140
|
groupPlacements.push(placement);
|
|
115
141
|
placements.push({
|
|
116
142
|
...placement,
|
|
@@ -123,7 +149,7 @@ export function resolveSideLegendLane({
|
|
|
123
149
|
cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
124
150
|
}
|
|
125
151
|
const vertical = unionBounds(groupPlacements.map(item => item.bounds));
|
|
126
|
-
const groupExtent = horizontalExtent(group.blocks);
|
|
152
|
+
const groupExtent = horizontalExtent(group.blocks, labelStart);
|
|
127
153
|
const foreground = {
|
|
128
154
|
left: titleStartX + groupExtent.left,
|
|
129
155
|
right: titleStartX + groupExtent.right,
|
|
@@ -232,6 +258,13 @@ function normalizeHorizontalRow(entries) {
|
|
|
232
258
|
)
|
|
233
259
|
: commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
|
|
234
260
|
return entries.map(({ group, dx }) => {
|
|
261
|
+
if (group.atomic) {
|
|
262
|
+
const contentDy = elementAnchor - group.element.top;
|
|
263
|
+
const foreground = translateBounds(group.content, dx, contentDy);
|
|
264
|
+
return { id: group.id, dx, titleDy: contentDy, contentDy, foreground,
|
|
265
|
+
occupied: expandBounds(foreground, group.inset), padding: group.padding,
|
|
266
|
+
backgroundId: group.backgroundId };
|
|
267
|
+
}
|
|
235
268
|
const contentDy = commonTitleY === undefined
|
|
236
269
|
? elementAnchor - midpoint(
|
|
237
270
|
group.element.top,
|
|
@@ -350,3 +383,59 @@ export function resolveHorizontalLegendLane({
|
|
|
350
383
|
rowCount: packed.length
|
|
351
384
|
};
|
|
352
385
|
}
|
|
386
|
+
|
|
387
|
+
// Compose independently measured content blocks before the outer edge lane
|
|
388
|
+
// treats the whole group as one indivisible item.
|
|
389
|
+
export function resolveHorizontalLegendGroup({ edge, plot, canvas, groups,
|
|
390
|
+
align, offset, border, backgroundId, collisionBounds = [] }) {
|
|
391
|
+
const inset = decoration(border);
|
|
392
|
+
const innerPlot = { ...plot, x: plot.x + inset, width: plot.width - inset * 2 };
|
|
393
|
+
const packed = packHorizontalRows(groups, innerPlot);
|
|
394
|
+
const rows = packed.map(normalizeHorizontalRow);
|
|
395
|
+
let cursor = 0;
|
|
396
|
+
const placements = [];
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
const bounds = unionBounds(row.map(item => item.occupied));
|
|
399
|
+
const dy = edge === "top" ? cursor - bounds.bottom : cursor - bounds.top;
|
|
400
|
+
const translated = row.map(item => translateHorizontalPlacement(item, dy));
|
|
401
|
+
placements.push(...translated);
|
|
402
|
+
const occupied = unionBounds(translated.map(item => item.occupied));
|
|
403
|
+
cursor = edge === "top" ? occupied.top - HORIZONTAL_LEGEND_BLOCK_GAP
|
|
404
|
+
: occupied.bottom + HORIZONTAL_LEGEND_BLOCK_GAP;
|
|
405
|
+
}
|
|
406
|
+
const foreground = unionBounds(placements.map(item => item.occupied));
|
|
407
|
+
const occupied = expandBounds(foreground, inset);
|
|
408
|
+
const width = occupied.right - occupied.left;
|
|
409
|
+
const x = align === "left" ? plot.x : align === "right" ? plot.x + plot.width - width
|
|
410
|
+
: plot.x + (plot.width - width) / 2;
|
|
411
|
+
const dx = x - occupied.left;
|
|
412
|
+
const dy = edge === "top" ? plot.y - offset - occupied.bottom
|
|
413
|
+
: plot.y + plot.height + offset - occupied.top;
|
|
414
|
+
const finalBounds = translateBounds(occupied, dx, dy);
|
|
415
|
+
if (finalBounds.left < 0 || finalBounds.right > canvas.width ||
|
|
416
|
+
finalBounds.top < 0 || finalBounds.bottom > canvas.height) {
|
|
417
|
+
throw new Error(`Combined legend requires more ${edge}-margin or Canvas space.`);
|
|
418
|
+
}
|
|
419
|
+
if (collisionBounds.some(bounds => overlap(finalBounds, bounds))) {
|
|
420
|
+
throw new Error(`Combined ${edge} legend and ${edge === "top" ? "chart titles" : "x-axis guides"} require more margin space.`);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
placements: placements.map(item => ({ ...item, dx: item.dx + dx,
|
|
424
|
+
titleDy: item.titleDy + dy, contentDy: item.contentDy + dy,
|
|
425
|
+
...(item.backgroundId === undefined ? {} : { background: {
|
|
426
|
+
id: item.backgroundId,
|
|
427
|
+
x: item.foreground.left + dx - item.padding,
|
|
428
|
+
y: item.foreground.top + dy - item.padding,
|
|
429
|
+
width: item.foreground.right - item.foreground.left + item.padding * 2,
|
|
430
|
+
height: item.foreground.bottom - item.foreground.top + item.padding * 2
|
|
431
|
+
} }) })),
|
|
432
|
+
occupied: finalBounds,
|
|
433
|
+
background: backgroundId === undefined ? undefined : {
|
|
434
|
+
id: backgroundId,
|
|
435
|
+
x: foreground.left + dx - border.padding,
|
|
436
|
+
y: foreground.top + dy - border.padding,
|
|
437
|
+
width: foreground.right - foreground.left + border.padding * 2,
|
|
438
|
+
height: foreground.bottom - foreground.top + border.padding * 2
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -1,161 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
resolveBarChannels,
|
|
5
|
-
resolveBarColorLayout
|
|
6
|
-
} from "../../grammar/bars/policy.js";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_SERIES_BASELINE,
|
|
9
|
-
layoutSeriesPartition
|
|
10
|
-
} from "../../grammar/seriesLayout.js";
|
|
11
|
-
import {
|
|
12
|
-
isDiscretePositionScaleType,
|
|
13
|
-
mapContinuousScaleValues,
|
|
14
|
-
mapOrdinalPositionValues
|
|
15
|
-
} from "../../grammar/scales/index.js";
|
|
1
|
+
import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
|
|
2
|
+
import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
|
|
3
|
+
import { mapContinuousScaleValues, mapOrdinalPositionValues } from "../../grammar/scales/index.js";
|
|
16
4
|
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_BAR_FILL,
|
|
19
|
-
resolveBarAppearance
|
|
20
|
-
} from "./resolve.js";
|
|
21
|
-
import { deriveGroupedRectangles } from "./grouped.js";
|
|
5
|
+
import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
|
|
22
6
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
7
|
+
import { finiteMidpoint } from "../../grammar/numeric.js";
|
|
23
8
|
|
|
24
9
|
export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
25
10
|
const { dataset, layer } = required;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const colorEncoding = layer.encoding?.color;
|
|
38
|
-
const colorScale = resolved.resolvedScales[colorEncoding?.scale];
|
|
39
|
-
if (colorEncoding !== undefined && colorScale === undefined) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Bar mark "${layer.id}" requires a resolved color scale.`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const seriesDomain = colorScale?.domain ?? [undefined];
|
|
46
|
-
const seriesIndex = new Map(
|
|
47
|
-
seriesDomain.map((value, index) => [value, index])
|
|
48
|
-
);
|
|
49
|
-
const colors = new Map(seriesDomain.map((value, index) => [
|
|
50
|
-
value,
|
|
51
|
-
colorScale?.range[index % colorScale.range.length] ?? DEFAULT_BAR_FILL
|
|
52
|
-
]));
|
|
53
|
-
const cells = deriveBarAggregates(dataset.values, layer).values;
|
|
54
|
-
const discreteCategory = categoryScale.type === "ordinal" ||
|
|
55
|
-
isDiscretePositionScaleType(categoryScale.type);
|
|
56
|
-
const categoryDomain = discreteCategory
|
|
57
|
-
? categoryScale.domain
|
|
58
|
-
: [...new Set(cells.map(cell => cell[channels.category]))]
|
|
59
|
-
.sort((left, right) => left - right);
|
|
60
|
-
const categoryIndex = new Map(
|
|
61
|
-
categoryDomain.map((value, index) => [value, index])
|
|
62
|
-
);
|
|
63
|
-
const cellMap = new Map(cells.map(cell => [
|
|
64
|
-
JSON.stringify([cell[channels.category], cell.color]),
|
|
65
|
-
cell
|
|
66
|
-
]));
|
|
67
|
-
const thickness = resolveBarWidth(
|
|
68
|
-
widthConfig,
|
|
69
|
-
Math.abs(categoryScale.bandwidth ?? categoryScale.step)
|
|
70
|
-
);
|
|
71
|
-
const baseline = DEFAULT_SERIES_BASELINE;
|
|
72
|
-
const continuousColor = colorEncoding?.fieldType === "quantitative";
|
|
73
|
-
if (continuousColor) {
|
|
74
|
-
const mappedColors = mapScaleConsumerValues(
|
|
75
|
-
cells.map(cell => cell.color), colorScale, "color"
|
|
76
|
-
);
|
|
77
|
-
const cellByCategory = new Map(
|
|
78
|
-
cells.map((cell, index) => [cell[channels.category], { cell, index }])
|
|
79
|
-
);
|
|
80
|
-
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
81
|
-
const config = resolved.markConfigs[layer.id] ?? {};
|
|
82
|
-
const appearance = config.barAppearance ?? {};
|
|
83
|
-
return categoryDomain.flatMap(categoryValue => {
|
|
84
|
-
const entry = cellByCategory.get(categoryValue);
|
|
85
|
-
if (entry === undefined) return [];
|
|
86
|
-
const { cell, index } = entry;
|
|
87
|
-
const categoryCenter = discreteCategory
|
|
88
|
-
? mapOrdinalPositionValues([categoryValue], categoryScale)[0]
|
|
89
|
-
: mapContinuousScaleValues(
|
|
90
|
-
[categoryValue],
|
|
91
|
-
categoryScale
|
|
92
|
-
)[0];
|
|
93
|
-
const [segment] = layoutSeriesPartition(
|
|
94
|
-
[cell[channels.measure]],
|
|
95
|
-
layout,
|
|
96
|
-
{ baseline }
|
|
97
|
-
);
|
|
98
|
-
if (segment === undefined) return [];
|
|
99
|
-
const [start, end] = mapContinuousScaleValues(
|
|
100
|
-
[segment.start, segment.end],
|
|
101
|
-
measureScale
|
|
102
|
-
);
|
|
103
|
-
return [{
|
|
104
|
-
x: vertical ? categoryCenter - thickness / 2 : Math.min(start, end),
|
|
105
|
-
y: vertical ? Math.min(start, end) : categoryCenter - thickness / 2,
|
|
106
|
-
width: vertical ? thickness : Math.abs(start - end),
|
|
107
|
-
height: vertical ? Math.abs(start - end) : thickness,
|
|
108
|
-
fill: mappedColors[index],
|
|
109
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
110
|
-
}];
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
const segments = [];
|
|
114
|
-
|
|
115
|
-
for (const category of categoryDomain) {
|
|
116
|
-
const partition = seriesDomain.map(color =>
|
|
117
|
-
cellMap.get(JSON.stringify([category, color]))?.[channels.measure] ?? 0
|
|
118
|
-
);
|
|
119
|
-
for (const segment of layoutSeriesPartition(partition, layout, { baseline })) {
|
|
120
|
-
const color = seriesDomain[segment.index];
|
|
121
|
-
const cell = cellMap.get(JSON.stringify([category, color]));
|
|
122
|
-
if (cell === undefined) continue;
|
|
123
|
-
segments.push({ category, color, ...segment });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
11
|
+
const channels = resolveBarChannels(layer), vertical = channels.orientation === "vertical";
|
|
12
|
+
const categoryScale = resolved.resolvedScales[layer.encoding[channels.category].scale];
|
|
13
|
+
const measureScale = resolved.resolvedScales[layer.encoding[channels.measure].scale];
|
|
14
|
+
const color = layer.encoding.color, colorScale = resolved.resolvedScales[color?.scale];
|
|
15
|
+
const grouped = resolveBarColorLayout(layer) === "group";
|
|
16
|
+
const offsetScale = resolved.resolvedScales[layer.encoding[resolveBarOffsetChannel(layer)]?.scale];
|
|
17
|
+
if (grouped && offsetScale === undefined) throw new Error("Grouped bar requires a resolved offset scale.");
|
|
18
|
+
const temporal = categoryScale.type === "time";
|
|
19
|
+
const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
|
|
20
|
+
grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
|
|
21
|
+
const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
|
|
128
22
|
const config = resolved.markConfigs[layer.id] ?? {};
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
23
|
+
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
24
|
+
const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
|
|
25
|
+
const indices = new Map(categoryScale.domain.map((value, index) => [value, index]));
|
|
26
|
+
return segments.map(({ cell, start, end, seriesIndex }, index) => {
|
|
27
|
+
const categoryValue = cell[channels.category];
|
|
28
|
+
const categoryCenter = (temporal ? mapContinuousScaleValues : mapOrdinalPositionValues)([categoryValue], categoryScale)[0];
|
|
29
|
+
let position = !grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
|
|
30
|
+
? (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step + categoryScale.bandwidth / 2 - width / 2
|
|
31
|
+
: categoryCenter - width / 2;
|
|
32
|
+
if (grouped) {
|
|
33
|
+
const direction = Math.sign(categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]) || 1;
|
|
34
|
+
const offsetCenter = offsetScale.start + seriesIndex * offsetScale.step + (Math.sign(offsetScale.step) || 1) * offsetScale.bandwidth / 2;
|
|
35
|
+
if (vertical && direction > 0 && offsetScale.step > 0) {
|
|
36
|
+
const categoryStart = temporal ? categoryCenter - categoryScale.bandwidth / 2
|
|
37
|
+
: (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step;
|
|
38
|
+
position = categoryStart + offsetScale.start + seriesIndex * offsetScale.step + (offsetScale.bandwidth - width) / 2;
|
|
39
|
+
} else position = categoryCenter + direction * (offsetCenter - finiteMidpoint(...offsetScale.range)) - width / 2;
|
|
135
40
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)[0];
|
|
142
|
-
const [start, end] = mapContinuousScaleValues(
|
|
143
|
-
[segment.start, segment.end],
|
|
144
|
-
measureScale
|
|
145
|
-
);
|
|
146
|
-
return {
|
|
147
|
-
x: vertical
|
|
148
|
-
? categoryCenter - thickness / 2
|
|
149
|
-
: Math.min(start, end),
|
|
150
|
-
y: vertical
|
|
151
|
-
? Math.min(start, end)
|
|
152
|
-
: categoryCenter - thickness / 2,
|
|
153
|
-
width: vertical ? thickness : Math.abs(start - end),
|
|
154
|
-
height: vertical ? Math.abs(start - end) : thickness,
|
|
155
|
-
fill: segment.color === undefined
|
|
156
|
-
? appearance.fill ?? config.fill ?? colors.get(segment.color)
|
|
157
|
-
: colors.get(segment.color),
|
|
158
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
159
|
-
};
|
|
41
|
+
const [a, b] = mapContinuousScaleValues([start, end], measureScale);
|
|
42
|
+
return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
|
|
43
|
+
width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
|
|
44
|
+
fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
|
|
45
|
+
...resolveBarAppearance(config, existing[index]?.properties) };
|
|
160
46
|
});
|
|
161
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
1
2
|
import {
|
|
2
3
|
countHistogramBins,
|
|
3
4
|
findHistogramBinIndex,
|
|
@@ -34,6 +35,29 @@ export function deriveHistogramSegments({
|
|
|
34
35
|
const colorEncoding = layer.encoding?.color;
|
|
35
36
|
const layout = resolveBarColorLayout(layer);
|
|
36
37
|
|
|
38
|
+
if (layer.encoding?.group !== undefined) {
|
|
39
|
+
const identity = readSeriesIdentity(dataset.values, layer);
|
|
40
|
+
const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
|
|
41
|
+
const index = new Map(identity.domain.map((value, i) => [value, i]));
|
|
42
|
+
xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
|
|
43
|
+
if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(dataset.values[i]); });
|
|
44
|
+
return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(rows => rows.length), layout).map(segment => {
|
|
45
|
+
const members = cells[segment.index];
|
|
46
|
+
let color, colorValue;
|
|
47
|
+
if (colorEncoding !== undefined) {
|
|
48
|
+
const values = readNominalField(members, colorEncoding.field);
|
|
49
|
+
if (new Set(values).size !== 1) throw new Error("Histogram color requires one value within each bin/series cell.");
|
|
50
|
+
colorValue = values[0];
|
|
51
|
+
const scale = resolvedScales[colorEncoding.scale];
|
|
52
|
+
color = mapOrdinalValues([colorValue], scale.domain, scale.range)[0];
|
|
53
|
+
}
|
|
54
|
+
return { bin, start: bins.boundaries[bin], end: bins.boundaries[bin + 1],
|
|
55
|
+
category: segment.index, categoryCount: identity.domain.length,
|
|
56
|
+
stackStart: segment.start, stackEnd: segment.end, members,
|
|
57
|
+
...(color === undefined ? {} : { color, colorValue }) };
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
if (colorEncoding?.scale === undefined) {
|
|
38
62
|
return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
|
|
39
63
|
layoutSeriesPartition([count], layout).map(segment => ({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function resolveCategorySlotOffset(program, layer) {
|
|
2
|
+
const policy = program.markConfigs[layer.id]?.categorySlotOffset;
|
|
3
|
+
if (policy === undefined) return undefined;
|
|
4
|
+
if (!["x", "y"].includes(policy.channel)) {
|
|
5
|
+
throw new Error(`Category slot offset for "${layer.id}" requires channel x or y.`);
|
|
6
|
+
}
|
|
7
|
+
if (!Number.isFinite(policy.band) || policy.band < -0.5 || policy.band > 0.5) {
|
|
8
|
+
throw new RangeError(`Category slot offset for "${layer.id}" must be within [-0.5, 0.5] band.`);
|
|
9
|
+
}
|
|
10
|
+
const encoding = layer.encoding?.[policy.channel];
|
|
11
|
+
if (!["nominal", "ordinal"].includes(encoding?.fieldType)) {
|
|
12
|
+
throw new Error(`Category slot offset for "${layer.id}" requires a categorical position.`);
|
|
13
|
+
}
|
|
14
|
+
const scale = program.resolvedScales[encoding.scale];
|
|
15
|
+
if (scale?.type !== "band" || !Number.isFinite(scale.bandwidth) || scale.bandwidth <= 0) {
|
|
16
|
+
throw new Error(`Category slot offset for "${layer.id}" requires a positive band scale.`);
|
|
17
|
+
}
|
|
18
|
+
return { channel: policy.channel, pixels: scale.bandwidth * policy.band };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function offsetCategoryPositions(program, layer, positions) {
|
|
22
|
+
const offset = resolveCategorySlotOffset(program, layer);
|
|
23
|
+
if (offset === undefined || offset.pixels === 0) return positions;
|
|
24
|
+
const shift = values => values === undefined
|
|
25
|
+
? undefined
|
|
26
|
+
: values.map(value => Number.isFinite(value) ? value + offset.pixels : value);
|
|
27
|
+
return {
|
|
28
|
+
...positions,
|
|
29
|
+
[offset.channel]: shift(positions[offset.channel])
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function offsetCategoryRectangles(program, layer, rectangles) {
|
|
34
|
+
const offset = resolveCategorySlotOffset(program, layer);
|
|
35
|
+
if (offset === undefined || offset.pixels === 0) return rectangles;
|
|
36
|
+
return rectangles.map(rectangle => ({
|
|
37
|
+
...rectangle,
|
|
38
|
+
[offset.channel]: rectangle[offset.channel] + offset.pixels
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
@@ -32,7 +32,7 @@ export function namespaceGraphicId(namespace, id) {
|
|
|
32
32
|
const encode = value => Array.from(value, character =>
|
|
33
33
|
character.codePointAt(0).toString(16).padStart(6, "0")
|
|
34
34
|
).join("");
|
|
35
|
-
return `g${encode(namespace)}_${
|
|
35
|
+
return `g${encode(namespace)}_${id}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function rewriteItem(item, namespace) {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { withGuideLayoutTransaction } from "./guides/layout.js";
|
|
1
2
|
import {
|
|
2
3
|
canDeferScaleConsumerApplication,
|
|
4
|
+
getLayerScaleIds,
|
|
3
5
|
getExistingMarkRematerializationStep,
|
|
4
6
|
getMarkMaterializationStep,
|
|
5
|
-
|
|
7
|
+
getScaleConsumerMarkSteps,
|
|
8
|
+
getSourceDependentMarkSteps,
|
|
9
|
+
reusePlannedMarkScales
|
|
6
10
|
} from "./marks/index.js";
|
|
7
|
-
import { requireLayer } from "../selectors/layers.js";
|
|
11
|
+
import { findLayer, requireLayer } from "../selectors/layers.js";
|
|
8
12
|
import {
|
|
9
|
-
applyMaterializationPlan,
|
|
13
|
+
applyMaterializationPlan as executeMaterializationPlan,
|
|
10
14
|
buildMaterializationPlan
|
|
11
15
|
} from "./planner.js";
|
|
12
16
|
import { hasMaterializedLegend } from "./legends.js";
|
|
@@ -16,13 +20,6 @@ import {
|
|
|
16
20
|
} from "./scaleGuideDependencies.js";
|
|
17
21
|
import { planLayoutRematerialization } from "./layout.js";
|
|
18
22
|
|
|
19
|
-
function layerScaleIds(layer) {
|
|
20
|
-
return [
|
|
21
|
-
...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
|
|
22
|
-
...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
|
|
23
|
-
].filter(scale => scale !== undefined);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
23
|
export function planCanvasRematerialization(program) {
|
|
27
24
|
const marks = [];
|
|
28
25
|
for (const layer of program.semanticSpec.layers) {
|
|
@@ -38,7 +35,7 @@ export function planCanvasRematerialization(program) {
|
|
|
38
35
|
if (needsCanvasScaleRematerialization(program, scale)) {
|
|
39
36
|
const deferredConsumers = program.semanticSpec.layers.filter(layer =>
|
|
40
37
|
canDeferScaleConsumerApplication(layer) &&
|
|
41
|
-
|
|
38
|
+
getLayerScaleIds(layer).includes(scale.id)
|
|
42
39
|
);
|
|
43
40
|
const canDeferMarks = deferredConsumers.length === 0 ||
|
|
44
41
|
deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
|
|
@@ -66,7 +63,7 @@ export function planCanvasRematerialization(program) {
|
|
|
66
63
|
|
|
67
64
|
export function planLayerDataRematerialization(program, id) {
|
|
68
65
|
const layer = requireLayer(program, id);
|
|
69
|
-
const scaleIds = [...new Set(
|
|
66
|
+
const scaleIds = [...new Set(getLayerScaleIds(layer))];
|
|
70
67
|
const markStep = getMarkMaterializationStep(program, layer);
|
|
71
68
|
const scales = scaleIds.map(scale => ({
|
|
72
69
|
op: "rematerializeScale",
|
|
@@ -76,15 +73,19 @@ export function planLayerDataRematerialization(program, id) {
|
|
|
76
73
|
...(markStep === undefined ? {} : { marks: false })
|
|
77
74
|
}
|
|
78
75
|
}));
|
|
79
|
-
const
|
|
76
|
+
const directMarks = [
|
|
80
77
|
...(markStep === undefined ? [] : [markStep]),
|
|
81
|
-
...
|
|
78
|
+
...getScaleConsumerMarkSteps(program, scaleIds)
|
|
79
|
+
];
|
|
80
|
+
const marks = [
|
|
81
|
+
...directMarks,
|
|
82
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
82
83
|
];
|
|
83
84
|
if (scales.length > 0 || marks.length > 0) {
|
|
84
85
|
const guides = scaleIds.flatMap(scale =>
|
|
85
86
|
planScaleGuideRematerialization(program, scale)
|
|
86
87
|
);
|
|
87
|
-
return buildMaterializationPlan({ scales, marks, guides });
|
|
88
|
+
return buildMaterializationPlan({ scales, marks: reusePlannedMarkScales(program, marks, scaleIds), guides });
|
|
88
89
|
}
|
|
89
90
|
const existingStep = getExistingMarkRematerializationStep(program, layer);
|
|
90
91
|
return buildMaterializationPlan({
|
|
@@ -99,5 +100,45 @@ export function applyLayerDataRematerialization(program, id) {
|
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
export
|
|
103
|
+
export function applyLayerEmptyDataView(program, id) {
|
|
104
|
+
requireLayer(program, id);
|
|
105
|
+
const targets = [
|
|
106
|
+
id,
|
|
107
|
+
...getSourceDependentMarkSteps(program, id).map(step => step.args.id)
|
|
108
|
+
];
|
|
109
|
+
let next = program;
|
|
110
|
+
for (const target of new Set(targets)) {
|
|
111
|
+
const graphic = next.graphicSpec.objects[target];
|
|
112
|
+
if (graphic === undefined) continue;
|
|
113
|
+
next = graphic.type === "collection"
|
|
114
|
+
? next.editGraphics({ target, property: "items", value: [] })
|
|
115
|
+
: next.editGraphics({ target, property: "length", value: 0 });
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function applyDetachedScaleRematerialization(program, previousLayers) {
|
|
121
|
+
const retained = new Set(program.semanticSpec.layers.flatMap(getLayerScaleIds));
|
|
122
|
+
const scaleIds = [...new Set(previousLayers.flatMap(layer => {
|
|
123
|
+
const current = findLayer(program, layer.id);
|
|
124
|
+
const currentIds = current === undefined ? [] : getLayerScaleIds(current);
|
|
125
|
+
return getLayerScaleIds(layer).filter(id => !currentIds.includes(id) && retained.has(id));
|
|
126
|
+
}))];
|
|
127
|
+
if (scaleIds.length === 0) return program;
|
|
128
|
+
const directMarks = getScaleConsumerMarkSteps(program, scaleIds);
|
|
129
|
+
return applyMaterializationPlan(program, buildMaterializationPlan({
|
|
130
|
+
scales: scaleIds.map(id => ({
|
|
131
|
+
op: "rematerializeScale", args: { id, guides: false, marks: false }
|
|
132
|
+
})),
|
|
133
|
+
marks: [
|
|
134
|
+
...directMarks,
|
|
135
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
136
|
+
],
|
|
137
|
+
guides: scaleIds.flatMap(id => planScaleGuideRematerialization(program, id))
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function applyMaterializationPlan(program, plan) {
|
|
142
|
+
return withGuideLayoutTransaction(program, next => executeMaterializationPlan(next, plan));
|
|
143
|
+
}
|
|
103
144
|
export { planScaleGuideRematerialization };
|
|
@@ -25,7 +25,7 @@ export function planEncodingRematerialization(program, {
|
|
|
25
25
|
);
|
|
26
26
|
const marks = [
|
|
27
27
|
...directMarks,
|
|
28
|
-
...getSourceDependentMarkSteps(program,
|
|
28
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
const guides = [];
|
|
@@ -69,7 +69,13 @@ export function resolveLegacyCategoricalLegend(program) {
|
|
|
69
69
|
return {
|
|
70
70
|
mode: "legacyCategorical",
|
|
71
71
|
compatibility,
|
|
72
|
-
reservation: {
|
|
72
|
+
reservation: {
|
|
73
|
+
position: "right",
|
|
74
|
+
align: "center",
|
|
75
|
+
gap: 18,
|
|
76
|
+
width: 132,
|
|
77
|
+
height: 0
|
|
78
|
+
},
|
|
73
79
|
legend: {
|
|
74
80
|
field: config.field,
|
|
75
81
|
mark: encodings[0].layer.mark.type,
|
|
@@ -33,12 +33,27 @@ function removeNamespacedGraphics(program, namespace, ids) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function legendTranslation(prepared, plot, layout) {
|
|
36
|
+
const position = prepared.reservation.position;
|
|
37
|
+
if (["top", "bottom"].includes(position)) {
|
|
38
|
+
const width = prepared.bounds.right - prepared.bounds.left;
|
|
39
|
+
const left = prepared.reservation.align === "left"
|
|
40
|
+
? plot.x
|
|
41
|
+
: prepared.reservation.align === "right"
|
|
42
|
+
? plot.x + plot.width - width
|
|
43
|
+
: plot.x + (plot.width - width) / 2;
|
|
44
|
+
return {
|
|
45
|
+
x: left - prepared.bounds.left,
|
|
46
|
+
y: layout.legend.y - prepared.bounds.top
|
|
47
|
+
};
|
|
48
|
+
}
|
|
36
49
|
const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
|
|
37
50
|
if (strips?.items?.[0] !== undefined) {
|
|
38
51
|
const properties = strips.items[0].properties;
|
|
39
52
|
const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
|
|
40
53
|
return {
|
|
41
|
-
x: layout.legend.x -
|
|
54
|
+
x: layout.legend.x - (position === "left"
|
|
55
|
+
? prepared.bounds.left
|
|
56
|
+
: properties.x),
|
|
42
57
|
y: plot.y + (plot.height - length) / 2 - properties.y
|
|
43
58
|
};
|
|
44
59
|
}
|