ggaction 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -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 +20 -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 +8 -7
- 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/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- 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 +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- 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 +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- 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 +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- 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/guides/tickValues.js +8 -5
- 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 +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- 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/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- 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/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- 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 +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- 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 +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -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/axisTextPlacement.js +1 -0
- 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/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -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/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- 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 +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- 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/guideCollisions.js +9 -1
- 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/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- 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/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -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 +7 -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/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- 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/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- 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 +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -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.`);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cloneAndFreeze, freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { namespaceGraphicId } from "../selectors/graphicCapacity.js";
|
|
3
|
+
export { namespaceGraphicId } from "../selectors/graphicCapacity.js";
|
|
2
4
|
import {
|
|
3
5
|
requireSingleOrderedGraphicByType,
|
|
4
6
|
walkGraphicTreeEvents
|
|
@@ -28,13 +30,6 @@ function validateCanvas(canvas, id) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export function namespaceGraphicId(namespace, id) {
|
|
32
|
-
const encode = value => Array.from(value, character =>
|
|
33
|
-
character.codePointAt(0).toString(16).padStart(6, "0")
|
|
34
|
-
).join("");
|
|
35
|
-
return `g${encode(namespace)}_${id}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
function rewriteItem(item, namespace) {
|
|
39
34
|
if (!isPlainObject(item) || typeof item.id !== "string" || item.id.length === 0) {
|
|
40
35
|
throw new TypeError("Graphic snapshot items require non-empty string ids.");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cartesianAxisOwners } from "../selectors/axes.js";
|
|
1
2
|
import { isSourceOwnedText } from "../grammar/text.js";
|
|
2
3
|
import { resolvePolarFrame } from "../grammar/polar.js";
|
|
3
4
|
import { resolveEffectiveBounds } from "../layout/aspect.js";
|
|
@@ -202,3 +203,23 @@ export function resolveGuideCoordinateBounds(program, {
|
|
|
202
203
|
}
|
|
203
204
|
return resolveCoordinateBounds(program, target, { resolvedScales });
|
|
204
205
|
}
|
|
206
|
+
|
|
207
|
+
// Axis consumers use a displaced frame; marks and grids retain the plot.
|
|
208
|
+
export function resolveCartesianAxisFrameBounds(program, channel, position, scale, axisId = channel) {
|
|
209
|
+
const plot = resolveGuideCoordinateBounds(program, {
|
|
210
|
+
coordinate: program.semanticSpec.guides.axis?.[axisId]?.coordinate, scale, channel
|
|
211
|
+
});
|
|
212
|
+
if (plot === undefined) return undefined;
|
|
213
|
+
const offset = program.guideConfigs.axis?.[axisId]?.frame?.offset ?? 0;
|
|
214
|
+
if (offset === 0) return plot;
|
|
215
|
+
const shift = ["bottom", "right"].includes(position) ? offset : -offset;
|
|
216
|
+
return channel === "x" ? { ...plot, y: plot.y + shift } : { ...plot, x: plot.x + shift };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Cartesian guide bindings are semantic scale consumers without data rows.
|
|
220
|
+
export function axisScaleBindings(program, id) {
|
|
221
|
+
return cartesianAxisOwners(program).flatMap(owner =>
|
|
222
|
+
owner.scale === id && owner.coordinate !== undefined &&
|
|
223
|
+
owner.config?.binding?.standalone === true
|
|
224
|
+
? [{ channel: owner.channel, coordinate: owner.coordinate }] : []);
|
|
225
|
+
}
|
|
@@ -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,30 @@
|
|
|
1
|
+
import { prepareSharedFacetLegend } from "./preparation.js";
|
|
2
|
+
import { facetLegendIntent, intrinsicFacetLegendSource, restoreFacetLegendIntent,
|
|
3
|
+
prepareAuthoredFacetLegend } from "./owner.js";
|
|
4
|
+
|
|
5
|
+
export function editFacetParentLegend(program, operation, args) {
|
|
6
|
+
if (program.compositionSpec?.type !== "facet") {
|
|
7
|
+
throw new Error(`${operation} supports only a unit chart or a facet parent shared legend.`);
|
|
8
|
+
}
|
|
9
|
+
const current = program.compositionSpec.facet.guides.legend;
|
|
10
|
+
if (current === false) throw new Error(`${operation} requires an existing facet parent legend.`);
|
|
11
|
+
const prepared = current === "shared" ? prepareSharedFacetLegend(program) : prepareAuthoredFacetLegend(program);
|
|
12
|
+
let source;
|
|
13
|
+
if (prepared.source !== undefined) {
|
|
14
|
+
source = restoreFacetLegendIntent(intrinsicFacetLegendSource(program), facetLegendIntent(prepared.source));
|
|
15
|
+
} else {
|
|
16
|
+
const entries = prepared.compatibility.entries;
|
|
17
|
+
if (entries.length !== 1) throw new Error("Facet parent inferred legend requires an unambiguous native owner.");
|
|
18
|
+
const config = entries[0].config;
|
|
19
|
+
source = intrinsicFacetLegendSource(program).createLegend({ target: config.target, channels: ["color"],
|
|
20
|
+
itemGap: 26, titleStyle: { fontSize: 12, fontWeight: 700 },
|
|
21
|
+
...(prepared.legend.mark === "point" ? { symbol: { layers: [{ type: "point", size: 5.5, stroke: "#ffffff", strokeWidth: 0.35 }] } } : {}) });
|
|
22
|
+
}
|
|
23
|
+
source = source[operation](args);
|
|
24
|
+
const intent = Object.keys(source.guideConfigs.legend ?? {}).length === 0 ? false : facetLegendIntent(source);
|
|
25
|
+
const spec = program.compositionSpec;
|
|
26
|
+
return program._clone({ trace: source.trace, actionStack: source.actionStack, actionSequence: source._actionSequence })
|
|
27
|
+
._withCompositionState({ children: program.children,
|
|
28
|
+
compositionSpec: { ...spec, facet: { ...spec.facet, guides: { ...spec.facet.guides, legend: intent } } } })
|
|
29
|
+
.materializeComposition();
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveAuthoredFacetLegendLayout as p}from"./ownerLayout.js";import{cartesianAxisOwners as B}from"../../selectors/axes.js";import{PLOT_LEGEND_POSITIONS as F}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as G}from"../../grammar/schemas/graphicBounds.js";import{resolveFacetOuterSides as I}from"../../grammar/facets/guides.js";import{resolveTextBounds as P,textBoundsIntersect as C}from"../../core/textMetrics.js";import{axisGraphicIds as E}from"../guides/resources.js";import{resolveFacetHeaderItems as T}from"../facetHeaders.js";function b(o,t,e){return{left:o.left+t,right:o.right+t,top:o.top+e,bottom:o.bottom+e}}function M(o,t,e,f){const r=o.materializationConfigs.textMetrics,h=I({placements:t.children}),i=[];for(const s of t.children){const m=o.children[s.id];for(const a of B(m))for(const[g,w]of[[2,"labels"],[3,"title"]]){const n=a.config?.[w]?.position;if(o.compositionSpec.facet.guides.axes==="outer"&&!h[n]?.includes(s.id))continue;const u=E(a.channel,a.id)[g];if(m.graphicSpec.objects[u]===void 0)continue;const x=G(m.graphicSpec,u,r);x!==void 0&&i.push(b(x,s.x,s.y))}}const{items:d}=T(t,e,f);for(const s of d)s.properties.text!==""&&i.push(P(s.properties,r));return i}function O(o,t,e,f,r){const h=o.right-o.left,i=o.bottom-o.top,d=e==="right"?t.x+t.width+r:e==="left"?t.x-r-h:f==="left"?t.x:f==="right"?t.x+t.width-h:t.x+(t.width-h)/2,s=["left","right"].includes(e)?t.y:e==="top"?t.y-r-i:t.y+t.height+r;return{left:d,right:d+h,top:s,bottom:s+i}}function q(o,{layout:t,plot:e,plots:f,title:r,preparedHeaders:h,prepared:i}){if(i?.mode==="authored")return p(o,{layout:t,plot:e,title:r,prepared:i,guides:M(o,t,f,h)});if(i?.mode!=="promoted")return{layout:t,plot:e};const d=i.occupiedGroup;if(d?.placement.at!==void 0||F.includes(d?.placement.position))return{layout:t,plot:e};const s=i.reservation.position,m=i.reservation.align,a=i.kinds.map(c=>i.source.guideConfigs.legend[c]),g=i.reservation.gap,w=d===void 0?a.every(c=>c.inferredOffset===!0):d.inferredOffset===!0;let n=O(i.bounds,e,s,m,g);const u=M(o,t,f,h),x=u.map(c=>s==="right"?c.right-(e.x+e.width):s==="left"?e.x-c.left:s==="top"?e.y-c.top:c.bottom-(e.y+e.height));if(w)n=O(i.bounds,e,s,m,Math.max(g,...x.map(c=>c+g)));else if(u.some(c=>C(n,c)))throw new Error("Facet shared legend overlaps retained axes or headers.");const l=Math.max(0,t.padding.left-n.left),v=Math.max(0,r.height+t.padding.top-n.top),L=t.children.map(c=>({...c,x:c.x+l,y:c.y+v})),S={...e,x:e.x+l,y:e.y+v};return n=b(n,l,v),{plot:S,layout:{...t,children:L,width:Math.ceil(Math.max(t.width+l,n.right+t.padding.right)),height:Math.ceil(Math.max(t.height+v,n.bottom+t.padding.bottom)),legend:{position:s,x:n.left,y:n.top,width:n.right-n.left,height:n.bottom-n.top,translation:{x:n.left-i.bounds.left,y:n.top-i.bounds.top}}}}}export{q as resolvePromotedFacetLegendLayout};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
2
|
+
import { isAuthoredFacetLegend } from "../../grammar/facets/legendIntent.js";
|
|
3
|
+
export { isAuthoredFacetLegend };
|
|
4
|
+
import { freezeOwned } from "../../core/immutable.js";
|
|
5
|
+
import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
6
|
+
import { describeLegendBlock } from "../../grammar/legendBlocks.js";
|
|
7
|
+
import { legendResourcePolicy, allLegendGraphicIds } from "../guides/resources.js";
|
|
8
|
+
import { resolveSharedFacetLegends, sameFacetGuideValue } from "../../grammar/facets/guides.js";
|
|
9
|
+
|
|
10
|
+
export function facetLegendIntent(source) {
|
|
11
|
+
return freezeOwned({ type: "authored-legend", blocks: Object.fromEntries(
|
|
12
|
+
Object.entries(source.guideConfigs.legend ?? {}).map(([kind, config]) => [kind, {
|
|
13
|
+
config,
|
|
14
|
+
...(source.semanticSpec.guides.legend?.[legendResourcePolicy(kind).semanticKind] === undefined ? {} : {
|
|
15
|
+
guide: source.semanticSpec.guides.legend[legendResourcePolicy(kind).semanticKind]
|
|
16
|
+
})
|
|
17
|
+
}])
|
|
18
|
+
), groups: source.guideConfigs.legendGroups ?? {} });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function intrinsicFacetLegendSource(program) {
|
|
22
|
+
const first = program.children[program.compositionSpec.children[0]];
|
|
23
|
+
let source = new program.constructor({
|
|
24
|
+
semanticSpec: first.semanticSpec, graphicSpec: first.graphicSpec,
|
|
25
|
+
resolvedScales: first.resolvedScales, materializationConfigs: first.materializationConfigs,
|
|
26
|
+
context: { ...first.context, deferGuideLayoutValidation: true, facetLegendIntrinsic: true },
|
|
27
|
+
trace: program.trace, actionStack: program.actionStack, actionSequence: program._actionSequence
|
|
28
|
+
});
|
|
29
|
+
for (const id of allLegendGraphicIds(Object.keys(source.guideConfigs.legend ?? {}))) {
|
|
30
|
+
if (source.graphicSpec.objects[id] !== undefined) source = source.editGraphics({ target: id, remove: true });
|
|
31
|
+
}
|
|
32
|
+
for (const key of Object.keys(source.semanticSpec.guides.legend ?? {})) {
|
|
33
|
+
source = source.editSemantic({ property: `guide.legend.${key}`, remove: true });
|
|
34
|
+
}
|
|
35
|
+
return source._withoutMaterializationConfig(["guides", "legend"])
|
|
36
|
+
._withoutMaterializationConfig(["guides", "legendGroups"]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function restoreFacetLegendIntent(source, intent) {
|
|
40
|
+
const scaffolds = [];
|
|
41
|
+
for (const [kind, { config }] of Object.entries(intent.blocks)) {
|
|
42
|
+
const descriptor = describeLegendBlock(source, kind, config);
|
|
43
|
+
const entry = { target: config.target, channels: [...descriptor.channels] };
|
|
44
|
+
if (descriptor.channels.includes("shape") && descriptor.channels.includes("size")) entry.combine = "size-shape";
|
|
45
|
+
if (kind === "colorSize") entry.combine = "color-size";
|
|
46
|
+
if (descriptor.family === "categorical" && config.inferredSymbol === false && entry.combine === undefined) entry.symbol = config.symbol;
|
|
47
|
+
if (kind === "gradient" && config.representation === "symbols") entry.representation = "symbols";
|
|
48
|
+
scaffolds.push(entry);
|
|
49
|
+
}
|
|
50
|
+
// Build unclipped resources first; each authored block restores its own window below.
|
|
51
|
+
// A global scaffold option would also reach gradient strips or overwrite sibling windows.
|
|
52
|
+
for (const args of scaffolds) source = source.createLegend(args);
|
|
53
|
+
for (const [kind, { config, guide }] of Object.entries(intent.blocks)) {
|
|
54
|
+
source = source._withLegendConfig(kind, config);
|
|
55
|
+
const background = legendResourcePolicy(kind).graphicIds.find(id => id.endsWith("Background"));
|
|
56
|
+
if (config.border === false && source.graphicSpec.objects[background] !== undefined) {
|
|
57
|
+
source = source.editGraphics({ target: background, remove: true });
|
|
58
|
+
} else if (config.border !== false && background !== undefined && source.graphicSpec.objects[background] === undefined) {
|
|
59
|
+
const roots = new Set(legendResourcePolicy(kind).graphicIds);
|
|
60
|
+
const before = source.graphicSpec.objects.canvas.children.find(id => roots.has(id));
|
|
61
|
+
source = source.createGraphics({ id: background, type: "rect", parent: "canvas", ...(before === undefined ? {} : { before }) });
|
|
62
|
+
}
|
|
63
|
+
if (guide !== undefined) {
|
|
64
|
+
for (const [property, value] of Object.entries(guide)) {
|
|
65
|
+
source = source.editSemantic({ property: `guide.legend.${legendResourcePolicy(kind).semanticKind}.${property}`, value });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
source = source._withMaterializationConfig(["guides", "legendGroups"], intent.groups);
|
|
70
|
+
return source.rematerializeLegend();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function prepareAuthoredFacetLegend(program, intent = program.compositionSpec.facet.guides.legend) {
|
|
74
|
+
let source = intrinsicFacetLegendSource(program);
|
|
75
|
+
source = isAuthoredFacetLegend(intent) ? restoreFacetLegendIntent(source, intent) : source.createLegend(intent);
|
|
76
|
+
const kinds = Object.keys(source.guideConfigs.legend ?? {});
|
|
77
|
+
for (const [kind, config] of Object.entries(source.guideConfigs.legend)) {
|
|
78
|
+
const descriptor = describeLegendBlock(source, kind, config);
|
|
79
|
+
const owner = findLayer(source, config.target);
|
|
80
|
+
for (const channel of descriptor.channels) {
|
|
81
|
+
if (program.compositionSpec.facet.scales[channel] === "independent") {
|
|
82
|
+
throw new Error("Facet parent legend requires shared encoded scales.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const child of Object.values(program.children)) {
|
|
86
|
+
const candidate = findLayer(child, config.target);
|
|
87
|
+
if (candidate === undefined || candidate.mark.type !== owner.mark.type ||
|
|
88
|
+
descriptor.channels.some(channel => !sameFacetGuideValue(candidate.encoding?.[channel], owner.encoding?.[channel])) ||
|
|
89
|
+
!sameFacetGuideValue(child.markConfigs[config.target], source.markConfigs[config.target])) {
|
|
90
|
+
throw new Error("Facet parent legend requires compatible source mark bindings and recipes in every child.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const compatibility = resolveSharedFacetLegends(program.compositionSpec.children.map(id => ({
|
|
95
|
+
id, guideConfigs: source.guideConfigs, resolvedScales: program.children[id].resolvedScales
|
|
96
|
+
})));
|
|
97
|
+
const blocks = kinds.map(kind => {
|
|
98
|
+
const ids = new Set(allLegendGraphicIds([kind]));
|
|
99
|
+
const roots = source.graphicSpec.objects.canvas.children.filter(id => ids.has(id));
|
|
100
|
+
const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots, source.materializationConfigs.textMetrics);
|
|
101
|
+
if (bounds === undefined) throw new Error("Facet parent legend requires measurable block bounds.");
|
|
102
|
+
return { kind, roots, bounds, config: source.guideConfigs.legend[kind] };
|
|
103
|
+
});
|
|
104
|
+
return { mode: "authored", source, kinds, blocks, compatibility,
|
|
105
|
+
intent: facetLegendIntent(source),
|
|
106
|
+
roots: blocks.flatMap(block => block.roots),
|
|
107
|
+
bounds: unionConcreteGraphicBounds(source.graphicSpec, blocks.flatMap(block => block.roots), source.materializationConfigs.textMetrics),
|
|
108
|
+
reservation: { position: "right", align: "center", gap: 0, width: 0, height: 0 } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Theme reconciliation changes the derived native appearance in one transaction.
|
|
112
|
+
// Adopt only that transaction's configs; ordinary layout/data replay reads intent.
|
|
113
|
+
export function synchronizeFacetLegendAppearance(program) {
|
|
114
|
+
const intent = program.compositionSpec?.type === "facet" ? program.compositionSpec.facet.guides.legend : undefined;
|
|
115
|
+
if (!isAuthoredFacetLegend(intent)) return program;
|
|
116
|
+
const blocks = Object.fromEntries(Object.entries(intent.blocks).map(([kind, block]) => {
|
|
117
|
+
const config = program.guideConfigs.legend?.[kind];
|
|
118
|
+
if (config === undefined || config.target !== block.config.target) throw new Error("Parent theme reconciliation lost its legend owner.");
|
|
119
|
+
return [kind, { ...block, config }];
|
|
120
|
+
}));
|
|
121
|
+
const spec = program.compositionSpec;
|
|
122
|
+
return program._withCompositionState({ children: program.children,
|
|
123
|
+
compositionSpec: { ...spec, facet: { ...spec.facet,
|
|
124
|
+
guides: { ...spec.facet.guides, legend: { ...intent, blocks, groups: program.guideConfigs.legendGroups ?? {} } } } } });
|
|
125
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { localLegendPlacement } from "../../grammar/legendGroups.js";
|
|
2
|
+
import { PLOT_LEGEND_POSITIONS } from "../../core/vocabulary.js";
|
|
3
|
+
import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
|
|
4
|
+
import { textBoundsIntersect } from "../../core/textMetrics.js";
|
|
5
|
+
|
|
6
|
+
const shift = (bounds, x, y) => ({ left: bounds.left + x, right: bounds.right + x,
|
|
7
|
+
top: bounds.top + y, bottom: bounds.bottom + y });
|
|
8
|
+
|
|
9
|
+
function nativeGroup(blocks) {
|
|
10
|
+
const config = blocks[0].config;
|
|
11
|
+
const positions = new Set(blocks.map(block => block.config.position));
|
|
12
|
+
if (positions.size > 1) throw new Error("Parent legend inherited blocks require one common anchor.");
|
|
13
|
+
const placement = { position: config.position ?? "right", align: config.align ?? "center",
|
|
14
|
+
offset: Math.max(...blocks.map(block => block.config.offset ?? 0)) };
|
|
15
|
+
return { placement, blockLayout: "wrap", inferredOffset: blocks.every(block => block.config.inferredOffset === true) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveAuthoredFacetLegendLayout(program, { layout, plot, title, prepared, guides }) {
|
|
19
|
+
const partitions = [];
|
|
20
|
+
const targets = [...new Set(prepared.blocks.map(block => block.config.target))];
|
|
21
|
+
for (const target of targets) {
|
|
22
|
+
const owned = prepared.blocks.filter(block => block.config.target === target);
|
|
23
|
+
const inherited = owned.filter(block => localLegendPlacement(block.config) === undefined);
|
|
24
|
+
if (inherited.length) partitions.push({ blocks: inherited,
|
|
25
|
+
group: prepared.source.guideConfigs.legendGroups?.[target] ?? nativeGroup(inherited) });
|
|
26
|
+
for (const block of owned) {
|
|
27
|
+
const placement = localLegendPlacement(block.config);
|
|
28
|
+
if (placement !== undefined) partitions.push({ blocks: [block],
|
|
29
|
+
group: { placement, blockLayout: "column", inferredOffset: placement.inferredOffset } });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const placements = [];
|
|
33
|
+
for (const { blocks, group } of partitions) {
|
|
34
|
+
const position = group.placement.position;
|
|
35
|
+
const inside = group.placement.at !== undefined || PLOT_LEGEND_POSITIONS.includes(position);
|
|
36
|
+
let effective = group;
|
|
37
|
+
if (!inside && group.inferredOffset === true) {
|
|
38
|
+
const base = group.placement.offset;
|
|
39
|
+
const distances = guides.map(bound => position === "right" ? bound.right - plot.x - plot.width
|
|
40
|
+
: position === "left" ? plot.x - bound.left : position === "top" ? plot.y - bound.top
|
|
41
|
+
: bound.bottom - plot.y - plot.height);
|
|
42
|
+
effective = { ...group, placement: { ...group.placement, offset: Math.max(base, ...distances.map(value => value + base)) } };
|
|
43
|
+
}
|
|
44
|
+
const plans = resolveLegendGroupPlacement({ blocks: blocks.map(block => ({ id: block.kind, bounds: block.bounds })),
|
|
45
|
+
group: effective, plot, canvas: layout, deferCanvasFit: true });
|
|
46
|
+
for (const plan of plans) {
|
|
47
|
+
const block = blocks.find(block => block.kind === plan.id);
|
|
48
|
+
const bounds = shift(block.bounds, plan.dx, plan.dy);
|
|
49
|
+
if (inside && (bounds.left < plot.x || bounds.right > plot.x + plot.width ||
|
|
50
|
+
bounds.top < plot.y || bounds.bottom > plot.y + plot.height)) {
|
|
51
|
+
throw new Error("Facet parent legend does not fit the final plot union.");
|
|
52
|
+
}
|
|
53
|
+
if (guides.some(bound => textBoundsIntersect(bounds, bound))) {
|
|
54
|
+
throw new Error("Facet parent legend overlaps retained axes or headers.");
|
|
55
|
+
}
|
|
56
|
+
if (placements.some(other => textBoundsIntersect(bounds, other.bounds))) {
|
|
57
|
+
throw new Error("Facet parent legend blocks overlap.");
|
|
58
|
+
}
|
|
59
|
+
placements.push({ kind: block.kind, dx: plan.dx, dy: plan.dy, bounds, inside });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const dx = Math.max(0, ...placements.map(item => layout.padding.left - item.bounds.left));
|
|
63
|
+
const dy = Math.max(0, ...placements.map(item => title.height + layout.padding.top - item.bounds.top));
|
|
64
|
+
const moved = placements.map(item => ({ ...item, dx: item.dx + dx, dy: item.dy + dy,
|
|
65
|
+
bounds: shift(item.bounds, dx, dy) }));
|
|
66
|
+
return { plot: { ...plot, x: plot.x + dx, y: plot.y + dy },
|
|
67
|
+
layout: { ...layout, children: layout.children.map(cell => ({ ...cell, x: cell.x + dx, y: cell.y + dy })),
|
|
68
|
+
width: Math.ceil(Math.max(layout.width + dx, ...moved.map(item => item.bounds.right + layout.padding.right))),
|
|
69
|
+
height: Math.ceil(Math.max(layout.height + dy, ...moved.map(item => item.bounds.bottom + layout.padding.bottom))),
|
|
70
|
+
parentLegendPlacements: moved } };
|
|
71
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { translateGraphic } from "../graphicTranslation.js";
|
|
2
|
+
import { cartesianAxisOwners } from "../../selectors/axes.js";
|
|
3
|
+
import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
|
|
1
4
|
import { isPlainObject } from "../../core/immutable.js";
|
|
2
5
|
import { resolveConcreteGraphicBounds } from
|
|
3
6
|
"../../grammar/schemas/graphicBounds.js";
|
|
@@ -18,6 +21,9 @@ function copyLegendConfig(program, source) {
|
|
|
18
21
|
for (const [kind, config] of Object.entries(source.guideConfigs.legend ?? {})) {
|
|
19
22
|
next = next._withLegendConfig(kind, config);
|
|
20
23
|
}
|
|
24
|
+
for (const [target, group] of Object.entries(source.guideConfigs.legendGroups ?? {})) {
|
|
25
|
+
next = next._withMaterializationConfig(["guides", "legendGroups", target], group);
|
|
26
|
+
}
|
|
21
27
|
return next;
|
|
22
28
|
}
|
|
23
29
|
|
|
@@ -33,6 +39,12 @@ function removeNamespacedGraphics(program, namespace, ids) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function legendTranslation(prepared, plot, layout) {
|
|
42
|
+
if (layout.legend?.translation !== undefined) return layout.legend.translation;
|
|
43
|
+
if (prepared.occupiedGroup !== undefined) {
|
|
44
|
+
const [placement] = resolveLegendGroupPlacement({ blocks: [{ id: "shared", bounds: prepared.bounds }],
|
|
45
|
+
group: prepared.occupiedGroup, plot, canvas: layout });
|
|
46
|
+
return { x: placement.dx, y: placement.dy };
|
|
47
|
+
}
|
|
36
48
|
const position = prepared.reservation.position;
|
|
37
49
|
if (["top", "bottom"].includes(position)) {
|
|
38
50
|
const width = prepared.bounds.right - prepared.bounds.left;
|
|
@@ -55,18 +67,16 @@ function legendTranslation(prepared, plot, layout) {
|
|
|
55
67
|
const strips = prepared.source.graphicSpec.objects[`${prefix}Strips`];
|
|
56
68
|
if (strips?.items?.[0] !== undefined) {
|
|
57
69
|
const properties = strips.items[0].properties;
|
|
58
|
-
const length = prepared.source.guideConfigs.legend[gradientKind].gradient.length;
|
|
59
70
|
return {
|
|
60
71
|
x: layout.legend.x - (position === "left"
|
|
61
72
|
? prepared.bounds.left
|
|
62
73
|
: properties.x),
|
|
63
|
-
y: plot.y
|
|
74
|
+
y: plot.y - prepared.bounds.top
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
|
-
const height = prepared.bounds.bottom - prepared.bounds.top;
|
|
67
77
|
return {
|
|
68
78
|
x: layout.legend.x - prepared.bounds.left,
|
|
69
|
-
y: plot.y
|
|
79
|
+
y: plot.y - prepared.bounds.top
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -91,7 +101,7 @@ function attachParentLegend(program, prepared, plot, layout) {
|
|
|
91
101
|
return next;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
function assertSharedLegendFits(program, id, layout, plot) {
|
|
104
|
+
function assertSharedLegendFits(program, id, layout, plot, occupiedGroup) {
|
|
95
105
|
const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
|
|
96
106
|
if (bounds === undefined) {
|
|
97
107
|
throw new Error("Facet shared legend requires measurable concrete bounds.");
|
|
@@ -102,7 +112,7 @@ function assertSharedLegendFits(program, id, layout, plot) {
|
|
|
102
112
|
Math.max(bounds.left, plot.x) &&
|
|
103
113
|
Math.min(bounds.bottom, plot.y + plot.height) >
|
|
104
114
|
Math.max(bounds.top, plot.y);
|
|
105
|
-
if (outside || overlapsPlot) {
|
|
115
|
+
if (outside || overlapsPlot && occupiedGroup === undefined) {
|
|
106
116
|
throw new Error(
|
|
107
117
|
"Facet shared legend requires more space than the composed Canvas provides."
|
|
108
118
|
);
|
|
@@ -110,7 +120,32 @@ function assertSharedLegendFits(program, id, layout, plot) {
|
|
|
110
120
|
return program;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
function attachAuthoredParentLegend(program, prepared, layout, synchronizeColorSizeMeaning) {
|
|
124
|
+
const meaning = prepared.source.semanticSpec.guides.legend?.colorSize;
|
|
125
|
+
if (meaning !== undefined || program.semanticSpec.guides.legend?.colorSize !== undefined) {
|
|
126
|
+
program = synchronizeColorSizeMeaning(program, meaning);
|
|
127
|
+
}
|
|
128
|
+
const id = `${program.compositionSpec.id}-shared-legend`;
|
|
129
|
+
let next = program._withoutMaterializationConfig(["guides", "legend"])
|
|
130
|
+
._withoutMaterializationConfig(["guides", "legendGroups"]);
|
|
131
|
+
next = copyLegendConfig(next, prepared.source).createGraphics({ id, type: "canvas", parent: "canvas" });
|
|
132
|
+
for (const [property, value] of Object.entries({ x: 0, y: 0, width: layout.width, height: layout.height, background: "transparent" })) {
|
|
133
|
+
next = next.editGraphics({ target: id, property, value });
|
|
134
|
+
}
|
|
135
|
+
for (const block of prepared.blocks) {
|
|
136
|
+
const placement = layout.parentLegendPlacements.find(item => item.kind === block.kind);
|
|
137
|
+
for (const root of block.roots) {
|
|
138
|
+
next = attachSnapshotObject(next, prepared.source.graphicSpec, root, id);
|
|
139
|
+
next = translateGraphic(next, root, placement.dx, placement.dy);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (prepared.keepShared === true) return next;
|
|
143
|
+
const spec = next.compositionSpec;
|
|
144
|
+
return next._withCompositionState({ children: next.children,
|
|
145
|
+
compositionSpec: { ...spec, facet: { ...spec.facet, guides: { ...spec.facet.guides, legend: prepared.intent } } } });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function applyFacetGuideComposition(program, { layout, plot, synchronizeColorSizeMeaning } = {}) {
|
|
114
149
|
if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
|
|
115
150
|
throw new TypeError("Facet guide composition requires a resolved layout.");
|
|
116
151
|
}
|
|
@@ -122,8 +157,10 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
|
|
|
122
157
|
const child = program.children[cell.id];
|
|
123
158
|
return {
|
|
124
159
|
id: cell.id,
|
|
125
|
-
axes:
|
|
126
|
-
|
|
160
|
+
axes: cartesianAxisOwners(child).map(owner => owner.id),
|
|
161
|
+
axisOwners: cartesianAxisOwners(child).map(owner => ({ id: owner.id, channel: owner.channel,
|
|
162
|
+
components: Object.fromEntries(["line", "ticks", "labels", "title"].flatMap(component =>
|
|
163
|
+
owner.config?.[component] === undefined ? [] : [[component, owner.config[component].position]])) })),
|
|
127
164
|
legendKinds: legendKinds(child)
|
|
128
165
|
};
|
|
129
166
|
});
|
|
@@ -138,8 +175,15 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
|
|
|
138
175
|
for (const cell of layout.children) {
|
|
139
176
|
const namespace = `${program.compositionSpec.id}-${cell.id}`;
|
|
140
177
|
const childPlan = ownership.children[cell.id];
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
const axisOwners = cartesianAxisOwners(program.children[cell.id]);
|
|
179
|
+
for (const axisId of childPlan.removeAxes) {
|
|
180
|
+
const owner = axisOwners.find(owner => owner.id === axisId);
|
|
181
|
+
next = removeNamespacedGraphics(next, namespace, axisGraphicIds(owner.channel, axisId));
|
|
182
|
+
}
|
|
183
|
+
for (const [axisId, components] of Object.entries(childPlan.removeAxisComponents ?? {})) {
|
|
184
|
+
const owner = axisOwners.find(owner => owner.id === axisId);
|
|
185
|
+
const ids = axisGraphicIds(owner.channel, axisId);
|
|
186
|
+
next = removeNamespacedGraphics(next, namespace, components.map(component => ids[["line", "ticks", "labels", "title"].indexOf(component)]));
|
|
143
187
|
}
|
|
144
188
|
if (childPlan.removeLegends.length > 0) {
|
|
145
189
|
next = removeNamespacedGraphics(
|
|
@@ -149,14 +193,27 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
|
|
|
149
193
|
);
|
|
150
194
|
}
|
|
151
195
|
}
|
|
152
|
-
if (prepared === undefined)
|
|
196
|
+
if (prepared === undefined) {
|
|
197
|
+
if (next.semanticSpec.guides.legend?.colorSize !== undefined) {
|
|
198
|
+
next = synchronizeColorSizeMeaning(next, undefined);
|
|
199
|
+
}
|
|
200
|
+
return next._withoutMaterializationConfig(["guides", "legend"])
|
|
201
|
+
._withoutMaterializationConfig(["guides", "legendGroups"]);
|
|
202
|
+
}
|
|
203
|
+
if (prepared.mode === "authored") {
|
|
204
|
+
next = attachAuthoredParentLegend(next, prepared, layout, synchronizeColorSizeMeaning);
|
|
205
|
+
return assertSharedLegendFits(next, `${program.compositionSpec.id}-shared-legend`, layout, plot, {});
|
|
206
|
+
}
|
|
153
207
|
let id;
|
|
154
208
|
if (prepared.mode === "legacyCategorical") {
|
|
155
209
|
id = `${program.compositionSpec.id}-legend`;
|
|
156
|
-
next = materializeLegacyCategoricalLegend(next, prepared,
|
|
210
|
+
next = materializeLegacyCategoricalLegend(next, prepared, {
|
|
211
|
+
...layout,
|
|
212
|
+
legend: { ...layout.legend, y: plot.y }
|
|
213
|
+
});
|
|
157
214
|
} else {
|
|
158
215
|
id = `${program.compositionSpec.id}-shared-legend`;
|
|
159
216
|
next = attachParentLegend(next, prepared, plot, layout);
|
|
160
217
|
}
|
|
161
|
-
return assertSharedLegendFits(next, id, layout, plot);
|
|
218
|
+
return assertSharedLegendFits(next, id, layout, plot, prepared.occupiedGroup);
|
|
162
219
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { localLegendPosition } from "../../grammar/legendGroups.js";
|
|
2
|
+
import { prepareAuthoredFacetLegend, facetLegendIntent } from "./owner.js";
|
|
3
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
4
|
+
import { PLOT_LEGEND_POSITIONS } from "../../layout/legend.js";
|
|
5
|
+
import { isContinuousColorScaleType } from "../../grammar/scales/types.js";
|
|
1
6
|
import {
|
|
2
7
|
unionConcreteGraphicBounds
|
|
3
8
|
} from "../../grammar/schemas/graphicBounds.js";
|
|
@@ -109,7 +114,7 @@ function prepareAutoLegendSource(child) {
|
|
|
109
114
|
request.channels.includes("color") ? "color" : "stroke"
|
|
110
115
|
]?.scale
|
|
111
116
|
);
|
|
112
|
-
const gradient = scale?.type
|
|
117
|
+
const gradient = isContinuousColorScaleType(scale?.type);
|
|
113
118
|
const gradientPrefix = request.channels.includes("color")
|
|
114
119
|
? "colorGradient"
|
|
115
120
|
: "strokeGradient";
|
|
@@ -123,7 +128,7 @@ function prepareAutoLegendSource(child) {
|
|
|
123
128
|
titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
|
|
124
129
|
} : {})
|
|
125
130
|
});
|
|
126
|
-
if (gradient) {
|
|
131
|
+
if (gradient && scale.type === "sequential") {
|
|
127
132
|
const resolved = source.resolvedScales[scale.id];
|
|
128
133
|
source = source
|
|
129
134
|
.editGraphics({
|
|
@@ -176,6 +181,7 @@ function promotedLegendPlacement(source, kinds) {
|
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
export function prepareSharedFacetLegend(program) {
|
|
184
|
+
if (isPlainObject(program.compositionSpec.facet.guides.legend)) return prepareAuthoredFacetLegend(program);
|
|
179
185
|
if (program.compositionSpec.facet.guides.legend !== "shared") {
|
|
180
186
|
return undefined;
|
|
181
187
|
}
|
|
@@ -188,6 +194,9 @@ export function prepareSharedFacetLegend(program) {
|
|
|
188
194
|
}
|
|
189
195
|
const source = inferred ? prepareAutoLegendSource(first) : first;
|
|
190
196
|
const kinds = legendKinds(source);
|
|
197
|
+
if (kinds.some(kind => localLegendPosition(source.guideConfigs.legend[kind]) !== undefined)) {
|
|
198
|
+
return { ...prepareAuthoredFacetLegend(program, facetLegendIntent(source)), keepShared: true };
|
|
199
|
+
}
|
|
191
200
|
const compatibility = resolveSharedFacetLegends(
|
|
192
201
|
compatibleLegendChildren(program, source, inferred)
|
|
193
202
|
);
|
|
@@ -201,8 +210,13 @@ export function prepareSharedFacetLegend(program) {
|
|
|
201
210
|
if (bounds === undefined) {
|
|
202
211
|
throw new Error("Shared facet legend has no measurable concrete bounds.");
|
|
203
212
|
}
|
|
204
|
-
const
|
|
205
|
-
const
|
|
213
|
+
const targets = new Set(kinds.map(kind => source.guideConfigs.legend[kind].target));
|
|
214
|
+
const group = targets.size === 1 ? source.guideConfigs.legendGroups?.[[...targets][0]] : undefined;
|
|
215
|
+
const insideGroup = group !== undefined && (group.placement.at !== undefined || PLOT_LEGEND_POSITIONS.includes(group.placement.position));
|
|
216
|
+
const placement = group === undefined ? promotedLegendPlacement(source, kinds)
|
|
217
|
+
: { position: insideGroup ? "right" : group.placement.position,
|
|
218
|
+
align: group.placement.align ?? "left" };
|
|
219
|
+
|
|
206
220
|
const width = Math.ceil(bounds.right - bounds.left);
|
|
207
221
|
return {
|
|
208
222
|
mode: "promoted",
|
|
@@ -212,11 +226,12 @@ export function prepareSharedFacetLegend(program) {
|
|
|
212
226
|
roots,
|
|
213
227
|
bounds,
|
|
214
228
|
compatibility,
|
|
229
|
+
...(group === undefined ? {} : { occupiedGroup: group }),
|
|
215
230
|
reservation: {
|
|
216
231
|
...placement,
|
|
217
|
-
gap:
|
|
218
|
-
width:
|
|
219
|
-
height: Math.ceil(bounds.bottom - bounds.top)
|
|
232
|
+
gap: insideGroup ? 0 : group?.placement.offset ?? Math.max(...kinds.map(kind => source.guideConfigs.legend[kind].offset)),
|
|
233
|
+
width: insideGroup ? 0 : width,
|
|
234
|
+
height: insideGroup ? 0 : Math.ceil(bounds.bottom - bounds.top)
|
|
220
235
|
}
|
|
221
236
|
};
|
|
222
237
|
}
|