ggaction 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +3 -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 +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- 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 +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- 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 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- 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 +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { materializeTextLines } from "../../../materialization/textLines.js";
|
|
2
|
+
import { axisThemeTypography } from "../../theme/state.js";
|
|
3
|
+
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
1
4
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
5
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
3
6
|
import { action } from "../../../core/action.js";
|
|
@@ -7,13 +10,13 @@ import {
|
|
|
7
10
|
validateNonEmptyString,
|
|
8
11
|
validateNonNegativeFinite
|
|
9
12
|
} from "../../../core/validation.js";
|
|
10
|
-
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
13
|
+
import { axisScaleBindings, resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
11
14
|
import {
|
|
12
15
|
isTransformedScaleType,
|
|
13
16
|
mapContinuousScaleValues,
|
|
14
17
|
mapOrdinalPositionValues
|
|
15
18
|
} from "../../../grammar/scales/index.js";
|
|
16
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
19
|
+
import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
17
20
|
"../../../theme/defaults.js";
|
|
18
21
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
19
22
|
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
@@ -28,6 +31,7 @@ import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
|
28
31
|
"../../../materialization/graphicHierarchy.js";
|
|
29
32
|
import {
|
|
30
33
|
resolveTextBounds,
|
|
34
|
+
resolveTextLines,
|
|
31
35
|
textBoundsFitCanvas,
|
|
32
36
|
textBoundsIntersect
|
|
33
37
|
} from "../../../core/textMetrics.js";
|
|
@@ -36,10 +40,19 @@ import { resolveConcreteGraphicBounds } from
|
|
|
36
40
|
import { validateAxisTextStyle } from "./labels.js";
|
|
37
41
|
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
38
42
|
|
|
43
|
+
function titleLines(text, config) {
|
|
44
|
+
if (!/[\r\n]/u.test(text)) return undefined;
|
|
45
|
+
const lines = resolveTextLines(text, {
|
|
46
|
+
fontSize: config.fontSize, align: "center", baseline: "middle", blockAlign: "middle"
|
|
47
|
+
});
|
|
48
|
+
return lines.length > 1 ? lines : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
function titleBounds(geometry, config, text, profile) {
|
|
40
52
|
return resolveTextBounds({
|
|
41
53
|
...geometry,
|
|
42
54
|
text,
|
|
55
|
+
lines: titleLines(text, config),
|
|
43
56
|
fontSize: config.fontSize,
|
|
44
57
|
fontFamily: config.fontFamily,
|
|
45
58
|
fontWeight: config.fontWeight,
|
|
@@ -49,6 +62,34 @@ function titleBounds(geometry, config, text, profile) {
|
|
|
49
62
|
}, profile);
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
function resolveTitleAlong(scale, config) {
|
|
66
|
+
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
67
|
+
let along;
|
|
68
|
+
if (config.at === "center") {
|
|
69
|
+
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
70
|
+
} else if (config.at === "start" || config.at === "end") {
|
|
71
|
+
along = scale.range[config.at === "start" ? 0 : 1];
|
|
72
|
+
} else {
|
|
73
|
+
if (discrete) {
|
|
74
|
+
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
75
|
+
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
76
|
+
} else {
|
|
77
|
+
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
78
|
+
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
79
|
+
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return along;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function axisLabelBounds(program, channel) {
|
|
86
|
+
const id = `${channel}AxisLabels`, labels = program.graphicSpec.objects[id];
|
|
87
|
+
const bounds = target => resolveConcreteGraphicBounds(
|
|
88
|
+
program.graphicSpec, target, program.materializationConfigs.textMetrics
|
|
89
|
+
);
|
|
90
|
+
return labels?.items?.map(item => bounds(item.id)) ?? (labels ? [bounds(id)] : []);
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
function inferredTitleOffset(program, channel, config) {
|
|
53
94
|
const plot = resolveGuideCoordinateBounds(program, {
|
|
54
95
|
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
@@ -78,7 +119,26 @@ function inferredTitleOffset(program, channel, config) {
|
|
|
78
119
|
const available = positive
|
|
79
120
|
? canvas[horizontal ? "height" : "width"] - bounds[far]
|
|
80
121
|
: bounds[near];
|
|
81
|
-
|
|
122
|
+
// Font ink can extend beyond the one-em layout box, especially after
|
|
123
|
+
// rotating a middle-baseline title. Keep inferred edge-clamped placement
|
|
124
|
+
// inside the Canvas; explicit offsets retain their exact geometry.
|
|
125
|
+
const inkClearance = channel === "y" &&
|
|
126
|
+
Math.abs(Math.cos(config.rotation)) < 1e-10
|
|
127
|
+
? config.fontSize / 4 : 0;
|
|
128
|
+
if (program.materializationConfigs.canvas?.plot !== undefined) {
|
|
129
|
+
return preferred + Math.max(0, needed);
|
|
130
|
+
}
|
|
131
|
+
const legacyAdjustment = Math.min(available, Math.max(0, needed));
|
|
132
|
+
if (inkClearance === 0) return preferred + legacyAdjustment;
|
|
133
|
+
const adjustment = Math.min(available - inkClearance, Math.max(0, needed));
|
|
134
|
+
const candidateBounds = titleBounds(resolveAxisTitleGeometry({
|
|
135
|
+
bounds: plot, channel, position: config.position,
|
|
136
|
+
along: resolveTitleAlong(program.resolvedScales[config.scale], config),
|
|
137
|
+
offset: preferred + adjustment
|
|
138
|
+
}), config, program.semanticSpec.guides.axis?.[channel]?.title ?? "", program.materializationConfigs.textMetrics);
|
|
139
|
+
// Optional ink reserve must not introduce a label collision.
|
|
140
|
+
return preferred + (axisLabelBounds(program, channel).some(label => label && textBoundsIntersect(candidateBounds, label))
|
|
141
|
+
? legacyAdjustment : adjustment);
|
|
82
142
|
}
|
|
83
143
|
|
|
84
144
|
const CREATE_OPTIONS = [
|
|
@@ -88,7 +148,7 @@ const CREATE_OPTIONS = [
|
|
|
88
148
|
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
89
149
|
const DEFAULTS = {
|
|
90
150
|
color: DEFAULT_COLORS.text,
|
|
91
|
-
fontSize:
|
|
151
|
+
fontSize: AXIS_TITLE_FONT_SIZE,
|
|
92
152
|
fontFamily: DEFAULT_FONT_FAMILY,
|
|
93
153
|
fontWeight: 600
|
|
94
154
|
};
|
|
@@ -149,6 +209,7 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
149
209
|
}
|
|
150
210
|
}
|
|
151
211
|
if (primaryTitles.size === 1) return [...primaryTitles][0];
|
|
212
|
+
if (titles.size === 0 && axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) return scaleId;
|
|
152
213
|
if (titles.size !== 1) throw new Error(`Axis title text cannot be inferred for scale "${scaleId}".`);
|
|
153
214
|
return [...titles][0];
|
|
154
215
|
}
|
|
@@ -160,26 +221,11 @@ function resolveGeometry(program, channel, config) {
|
|
|
160
221
|
scale: config.scale,
|
|
161
222
|
channel
|
|
162
223
|
});
|
|
163
|
-
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
164
224
|
if ((
|
|
165
225
|
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
166
226
|
!isTransformedScaleType(scale?.type)
|
|
167
227
|
) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
|
|
168
|
-
|
|
169
|
-
if (config.at === "center") {
|
|
170
|
-
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
171
|
-
} else if (config.at === "start" || config.at === "end") {
|
|
172
|
-
along = scale.range[config.at === "start" ? 0 : 1];
|
|
173
|
-
} else {
|
|
174
|
-
if (discrete) {
|
|
175
|
-
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
176
|
-
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
177
|
-
} else {
|
|
178
|
-
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
179
|
-
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
180
|
-
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
228
|
+
const along = resolveTitleAlong(scale, config);
|
|
183
229
|
const geometry = resolveAxisTitleGeometry({
|
|
184
230
|
bounds,
|
|
185
231
|
channel,
|
|
@@ -194,12 +240,10 @@ function resolveGeometry(program, channel, config) {
|
|
|
194
240
|
, program.materializationConfigs.textMetrics);
|
|
195
241
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
196
242
|
if (!canvas || !textBoundsFitCanvas(resolvedBounds, canvas)) {
|
|
197
|
-
throw
|
|
243
|
+
throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
|
|
198
244
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
: undefined;
|
|
202
|
-
if (labelsBounds && textBoundsIntersect(resolvedBounds, labelsBounds)) {
|
|
245
|
+
const labelBounds = axisLabelBounds(program, channel);
|
|
246
|
+
if (labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
|
|
203
247
|
throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
|
|
204
248
|
}
|
|
205
249
|
return geometry;
|
|
@@ -265,7 +309,7 @@ function makeEdit(channel) {
|
|
|
265
309
|
rotation: config.rotation
|
|
266
310
|
};
|
|
267
311
|
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: operation.graphic, property, value });
|
|
268
|
-
return next;
|
|
312
|
+
return materializeTextLines(next, operation.graphic, titleLines(resolvedText, config));
|
|
269
313
|
}));
|
|
270
314
|
}
|
|
271
315
|
|
|
@@ -309,6 +353,7 @@ function makeCreate(channel) {
|
|
|
309
353
|
fontSize: DEFAULTS.fontSize,
|
|
310
354
|
fontFamily: DEFAULTS.fontFamily,
|
|
311
355
|
fontWeight: DEFAULTS.fontWeight,
|
|
356
|
+
...axisThemeTypography(this.materializationConfigs.theme, "title"),
|
|
312
357
|
...appearance
|
|
313
358
|
};
|
|
314
359
|
let next = this
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerStrokeColorLegendActions } from "./legends/continuous/stroke.js";
|
|
1
2
|
import { registerBasicCartesianAxisActions } from "./axes/index.js";
|
|
2
3
|
import { registerGuideCollectionActions } from "./guides.js";
|
|
3
4
|
import { registerBasicGridActions } from "./grids/index.js";
|
|
@@ -15,6 +16,7 @@ export function registerBasicGuideActions(ProgramClass) {
|
|
|
15
16
|
registerBasicCategoricalLegendActions(ProgramClass);
|
|
16
17
|
registerGradientLegendActions(ProgramClass);
|
|
17
18
|
registerIntervalLegendActions(ProgramClass);
|
|
19
|
+
registerStrokeColorLegendActions(ProgramClass);
|
|
18
20
|
registerSizeLegendActions(ProgramClass);
|
|
19
21
|
registerBasicGridActions(ProgramClass);
|
|
20
22
|
registerGuideCollectionActions(ProgramClass);
|
|
@@ -17,10 +17,18 @@ function compatibleLegend(program, kind, requested, existing, explicit) {
|
|
|
17
17
|
if (requested.symbol?.layers !== undefined && !sameGuideValue(
|
|
18
18
|
requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
|
|
19
19
|
)) guideConflict(`${kind} legend uses a different symbol recipe`);
|
|
20
|
+
if (explicit.representation !== undefined && (requested.representation ?? "gradient") !== (existing.representation ?? "gradient")) guideConflict(`${kind} legend uses a different color representation`);
|
|
21
|
+
if (requested.representation === "symbols") {
|
|
22
|
+
if (explicit.values !== undefined && !sameGuideValue(requested.sampling.values, existing.sampling?.values)) guideConflict(`${kind} legend uses different exact samples`);
|
|
23
|
+
if (explicit.count !== undefined && (existing.sampling?.mode !== "auto" || requested.sampling.count !== existing.sampling?.count)) guideConflict(`${kind} legend uses a different sample count`);
|
|
24
|
+
}
|
|
20
25
|
const appearance = {};
|
|
21
26
|
for (const [key, value] of Object.entries(explicit)) {
|
|
22
|
-
if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
|
|
23
|
-
if (key === "
|
|
27
|
+
if (["target", "channels", "representation", "values"].includes(key) || requested[key] === undefined) continue;
|
|
28
|
+
if (key === "title") {
|
|
29
|
+
appearance.titleVisible = value !== false;
|
|
30
|
+
if (value !== false) appearance.title = value;
|
|
31
|
+
} else if (key === "symbol" && requested.symbol?.layers !== undefined) {
|
|
24
32
|
appearance.symbol = value === "auto" ? requested.symbol
|
|
25
33
|
: value.layers !== undefined ? value
|
|
26
34
|
: { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
refreshGridConfig,
|
|
9
9
|
resolveGridConfig,
|
|
10
10
|
resolveGridGeometry,
|
|
11
|
+
resolveGridPlacement,
|
|
11
12
|
resolveGridResources,
|
|
12
13
|
validateGridCreateArgs,
|
|
13
14
|
validateGridEditArgs
|
|
@@ -60,7 +61,12 @@ function makeRematerialize(direction) {
|
|
|
60
61
|
|
|
61
62
|
const config = refreshGridConfig(this, storedConfig);
|
|
62
63
|
const geometry = resolveGridGeometry(this, config);
|
|
63
|
-
let next = this._withGridConfig(direction, config)
|
|
64
|
+
let next = this._withGridConfig(direction, config);
|
|
65
|
+
if (config.zIndex !== undefined) {
|
|
66
|
+
const placement = resolveGridPlacement(next, config);
|
|
67
|
+
next = next.editGraphics({ target: operation.graphic, remove: true }).createGraphics({ id: operation.graphic, type: "line", length: 0, ...placement });
|
|
68
|
+
}
|
|
69
|
+
next = next.editGraphics({
|
|
64
70
|
target: operation.graphic,
|
|
65
71
|
property: "length",
|
|
66
72
|
value: geometry.values.length
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNominalValue } from "../../../grammar/scales/fields.js";
|
|
1
2
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
3
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
4
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
} from "../../../core/validation.js";
|
|
9
10
|
import {
|
|
10
11
|
isTransformedScaleType,
|
|
12
|
+
mapOrdinalPositionValues,
|
|
11
13
|
mapContinuousScaleValues
|
|
12
14
|
} from "../../../grammar/scales/index.js";
|
|
13
15
|
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
@@ -20,14 +22,15 @@ import {
|
|
|
20
22
|
} from "../tickValues.js";
|
|
21
23
|
import {
|
|
22
24
|
findGraphicParent,
|
|
25
|
+
graphicSiblings,
|
|
23
26
|
walkGraphicDrawOrder
|
|
24
27
|
} from "../../../grammar/schemas/graphicTree.js";
|
|
25
28
|
|
|
26
29
|
const GRID_OPTIONS = Object.freeze([
|
|
27
|
-
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash"
|
|
30
|
+
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "zIndex", "bandPosition", "alongOffset"
|
|
28
31
|
]);
|
|
29
32
|
const GRID_EDIT_OPTIONS = Object.freeze([
|
|
30
|
-
"count", "values", "color", "lineWidth", "strokeDash"
|
|
33
|
+
"count", "values", "color", "lineWidth", "strokeDash", "zIndex", "bandPosition", "alongOffset"
|
|
31
34
|
]);
|
|
32
35
|
const DEFAULT_STYLE = Object.freeze({
|
|
33
36
|
color: DEFAULT_COLORS.grid,
|
|
@@ -75,16 +78,19 @@ export function validateGridCreateArgs(args, operation) {
|
|
|
75
78
|
if (Object.hasOwn(args, "values") && (
|
|
76
79
|
!Array.isArray(args.values) ||
|
|
77
80
|
args.values.length === 0 ||
|
|
78
|
-
!args.values.every(
|
|
81
|
+
!args.values.every(isNominalValue)
|
|
79
82
|
)) {
|
|
80
|
-
throw new TypeError("Grid values must be a non-empty finite number array.");
|
|
83
|
+
throw new TypeError("Grid values must be a non-empty finite number array or nominal category array; continuous grids require finite numbers.");
|
|
81
84
|
}
|
|
85
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
|
|
82
86
|
if (Object.hasOwn(args, "color")) {
|
|
83
87
|
validateNonEmptyString(args.color, "Grid color");
|
|
84
88
|
}
|
|
85
89
|
if (Object.hasOwn(args, "lineWidth")) {
|
|
86
90
|
validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
|
|
87
91
|
}
|
|
92
|
+
if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
|
|
93
|
+
if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
|
|
88
94
|
if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -109,18 +115,21 @@ export function validateGridEditArgs(args, operation) {
|
|
|
109
115
|
if (Object.hasOwn(args, "values") && args.values !== "auto" && (
|
|
110
116
|
!Array.isArray(args.values) ||
|
|
111
117
|
args.values.length === 0 ||
|
|
112
|
-
!args.values.every(
|
|
118
|
+
!args.values.every(isNominalValue)
|
|
113
119
|
)) {
|
|
114
120
|
throw new TypeError(
|
|
115
121
|
'Grid values must be "auto" or a non-empty finite number array.'
|
|
116
122
|
);
|
|
117
123
|
}
|
|
124
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
|
|
118
125
|
if (Object.hasOwn(args, "color")) {
|
|
119
126
|
validateNonEmptyString(args.color, "Grid color");
|
|
120
127
|
}
|
|
121
128
|
if (Object.hasOwn(args, "lineWidth")) {
|
|
122
129
|
validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
|
|
123
130
|
}
|
|
131
|
+
if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
|
|
132
|
+
if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
|
|
124
133
|
if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
|
|
125
134
|
}
|
|
126
135
|
|
|
@@ -163,10 +172,10 @@ export function resolveGridResources(program, direction, args) {
|
|
|
163
172
|
}
|
|
164
173
|
const scale = requestedScale ?? scaleIds[0];
|
|
165
174
|
if (
|
|
166
|
-
!["linear", "time"].includes(program.resolvedScales[scale]?.type) &&
|
|
175
|
+
!["linear", "time", "band", "point"].includes(program.resolvedScales[scale]?.type) &&
|
|
167
176
|
!isTransformedScaleType(program.resolvedScales[scale]?.type)
|
|
168
177
|
) {
|
|
169
|
-
throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
|
|
178
|
+
throw new Error(`${direction} grid requires resolved continuous, band or point scale "${scale}".`);
|
|
170
179
|
}
|
|
171
180
|
const sourceIds = new Set(layers.map(layer => layer.id));
|
|
172
181
|
const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
|
|
@@ -177,8 +186,9 @@ export function resolveGridResources(program, direction, args) {
|
|
|
177
186
|
return leader === undefined ? [layer.id] : [leader, layer.id];
|
|
178
187
|
}));
|
|
179
188
|
let before;
|
|
189
|
+
let after;
|
|
180
190
|
walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
|
|
181
|
-
if (
|
|
191
|
+
if (related.has(id)) { if (before === undefined) before = id; after = id; }
|
|
182
192
|
});
|
|
183
193
|
if (before === undefined) throw new Error(`${direction} grid requires related mark graphics.`);
|
|
184
194
|
const owner = findGraphicParent(program.graphicSpec, before);
|
|
@@ -187,6 +197,7 @@ export function resolveGridResources(program, direction, args) {
|
|
|
187
197
|
coordinate,
|
|
188
198
|
scale,
|
|
189
199
|
before,
|
|
200
|
+
after,
|
|
190
201
|
...(owner?.kind === "parent" ? { parent: owner.id } : {})
|
|
191
202
|
};
|
|
192
203
|
}
|
|
@@ -201,6 +212,9 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
201
212
|
: inferGridTickConfig(program, resources.channel, resources.scale);
|
|
202
213
|
return {
|
|
203
214
|
direction,
|
|
215
|
+
...(args.bandPosition === undefined ? {} : { bandPosition: args.bandPosition }),
|
|
216
|
+
...(args.zIndex === undefined ? {} : { zIndex: args.zIndex }),
|
|
217
|
+
...(args.alongOffset === undefined ? {} : { alongOffset: args.alongOffset }),
|
|
204
218
|
scale: resources.scale,
|
|
205
219
|
coordinate: resources.coordinate,
|
|
206
220
|
...explicit,
|
|
@@ -255,23 +269,56 @@ export function resolveGridGeometry(program, config) {
|
|
|
255
269
|
channel
|
|
256
270
|
});
|
|
257
271
|
if ((
|
|
258
|
-
!["linear", "time"].includes(scale?.type) &&
|
|
272
|
+
!["linear", "time", "band", "point"].includes(scale?.type) &&
|
|
259
273
|
!isTransformedScaleType(scale?.type)
|
|
260
274
|
) || bounds === undefined) {
|
|
261
|
-
throw new Error("Grid materialization requires a continuous scale and Canvas bounds.");
|
|
275
|
+
throw new Error("Grid materialization requires a continuous, band or point scale and Canvas bounds.");
|
|
276
|
+
}
|
|
277
|
+
if (config.bandPosition !== undefined && scale.type !== "band") throw new Error("Grid bandPosition requires a band scale.");
|
|
278
|
+
if (["band", "point"].includes(scale.type)) {
|
|
279
|
+
if (config.mode === "count") throw new Error("Categorical grid values cannot use count.");
|
|
280
|
+
const values = valuesFromTickConfig(program, config);
|
|
281
|
+
if (!values.every(value => scale.domain.includes(value))) throw new RangeError("Grid values must be inside the scale domain.");
|
|
282
|
+
const centers = mapOrdinalPositionValues(values, scale);
|
|
283
|
+
const direction = scale.range[1] >= scale.range[0] ? 1 : -1;
|
|
284
|
+
const positions = config.bandPosition === "boundary"
|
|
285
|
+
? [...new Set(centers.flatMap(center => [center - direction * scale.bandwidth / 2, center + direction * scale.bandwidth / 2]))].sort((a, b) => direction * (a - b))
|
|
286
|
+
: centers;
|
|
287
|
+
validateGeneratedItemLimit(positions.length, "Grid value count");
|
|
288
|
+
return resolveGridLineGeometry({ direction: config.direction, values: positions, positions: positions.map(value => value + (config.alongOffset ?? 0)), bounds });
|
|
262
289
|
}
|
|
263
290
|
const values = valuesFromTickConfig(program, config);
|
|
264
291
|
validateGeneratedItemLimit(values.length, "Grid value count");
|
|
265
292
|
const low = Math.min(...scale.domain);
|
|
266
293
|
const high = Math.max(...scale.domain);
|
|
267
|
-
if (!values.every(value => value >= low && value <= high)) {
|
|
294
|
+
if (!values.every(value => Number.isFinite(value) && value >= low && value <= high)) {
|
|
268
295
|
throw new RangeError("Grid values must be inside the scale domain.");
|
|
269
296
|
}
|
|
270
297
|
const positions = mapContinuousScaleValues(values, scale);
|
|
271
298
|
return resolveGridLineGeometry({
|
|
272
299
|
direction: config.direction,
|
|
273
300
|
values,
|
|
274
|
-
positions,
|
|
301
|
+
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
275
302
|
bounds
|
|
276
303
|
});
|
|
277
304
|
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
export function resolveGridPlacement(program, config) {
|
|
308
|
+
const resources = resolveGridResources(program, config.direction, { scale: config.scale, coordinate: config.coordinate });
|
|
309
|
+
const id = gridNames(config.direction).graphic;
|
|
310
|
+
const siblings = graphicSiblings(program.graphicSpec, resources.before);
|
|
311
|
+
const rank = direction => [program.guideConfigs.grid?.[direction]?.zIndex ?? 0, direction === "horizontal" ? 0 : 1];
|
|
312
|
+
const ownRank = [config.zIndex ?? 0, config.direction === "horizontal" ? 0 : 1];
|
|
313
|
+
const compare = (a, b) => a[0] - b[0] || a[1] - b[1];
|
|
314
|
+
const neighbors = ["horizontal", "vertical"].filter(direction => {
|
|
315
|
+
const other = gridNames(direction).graphic;
|
|
316
|
+
return other !== id && siblings.includes(other) && ((rank(direction)[0] > 0) === (ownRank[0] > 0));
|
|
317
|
+
}).sort((a, b) => compare(rank(a), rank(b)));
|
|
318
|
+
const greater = neighbors.find(direction => compare(rank(direction), ownRank) > 0);
|
|
319
|
+
const lower = neighbors.filter(direction => compare(rank(direction), ownRank) < 0).at(-1);
|
|
320
|
+
return {
|
|
321
|
+
...(resources.parent === undefined ? {} : { parent: resources.parent }),
|
|
322
|
+
...(greater ? { before: gridNames(greater).graphic } : lower ? { after: gridNames(lower).graphic } : ownRank[0] > 0 ? { after: resources.after } : { before: resources.before })
|
|
323
|
+
};
|
|
324
|
+
}
|
|
@@ -78,6 +78,7 @@ function validateSymbolSupport(descriptor, property) {
|
|
|
78
78
|
}
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
if (kind === "gradient" && property === "fill") throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
81
82
|
if (kind === "size" && property === "size") {
|
|
82
83
|
throw new Error("editLegendBlock symbol.size conflicts with the size mapping.");
|
|
83
84
|
}
|
|
@@ -239,7 +240,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
|
239
240
|
blockSymbol: override.symbol
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
|
-
if (kind === "opacity") {
|
|
243
|
+
if (kind === "opacity" || kind === "gradient" && config.representation === "symbols") {
|
|
243
244
|
return {
|
|
244
245
|
...effective,
|
|
245
246
|
symbol: {
|
|
@@ -289,6 +290,7 @@ export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
|
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
const SPECIALIZED_SYMBOLS = Object.freeze({
|
|
293
|
+
gradient: Object.freeze({ id: "colorGradientSymbols", type: "circle" }),
|
|
292
294
|
size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
|
|
293
295
|
opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
|
|
294
296
|
strokeWidth: Object.freeze({ id: "strokeWidthLegendSymbols", type: "line" }),
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { recordLegendOffsetIntent } from "../group.js";
|
|
2
|
+
import { prepareLegendGroup, creationLegendTarget, validateLegendGroupOptions } from "../group.js";
|
|
3
|
+
import { PLOT_LEGEND_POSITIONS } from "../../../../layout/legend.js";
|
|
4
|
+
import { normalizeInitialLegendTitle } from "../continuous/common.js";
|
|
5
|
+
import { isContinuousColorScaleType } from "../../../../grammar/scales/types.js";
|
|
1
6
|
import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
|
|
2
7
|
import { sameGuideValue } from "../../reuse.js";
|
|
3
8
|
import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
|
|
@@ -17,8 +22,7 @@ import { legendResourcePolicies } from
|
|
|
17
22
|
"../../../../materialization/guides/resources.js";
|
|
18
23
|
import {
|
|
19
24
|
hasHorizontalLegendLane,
|
|
20
|
-
hasLegendLane
|
|
21
|
-
hasMultiSideLegendLane
|
|
25
|
+
hasLegendLane
|
|
22
26
|
} from "../lane.js";
|
|
23
27
|
import {
|
|
24
28
|
resolveCurrentDefinition,
|
|
@@ -33,7 +37,8 @@ import {
|
|
|
33
37
|
} from "../blocks.js";
|
|
34
38
|
|
|
35
39
|
function finishLegend(program) {
|
|
36
|
-
return hasLegendLane(program)
|
|
40
|
+
return hasLegendLane(program) || Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0
|
|
41
|
+
? program.rematerializeLegend() : program;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
function requestedCandidate(program, target, candidates) {
|
|
@@ -162,9 +167,6 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
162
167
|
next = next[policy.rematerializeOp]();
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
|
-
if (hasMultiSideLegendLane(next)) {
|
|
166
|
-
next = next.rematerializeSideLegendLane();
|
|
167
|
-
}
|
|
168
170
|
if (hasHorizontalLegendLane(next)) {
|
|
169
171
|
next = next.rematerializeHorizontalLegendLane();
|
|
170
172
|
}
|
|
@@ -178,7 +180,7 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
178
180
|
program,
|
|
179
181
|
layer,
|
|
180
182
|
args.channels,
|
|
181
|
-
args.title,
|
|
183
|
+
args.title === false ? undefined : args.title,
|
|
182
184
|
args.order === undefined ? undefined : normalizeLegendOrder(args.order)
|
|
183
185
|
);
|
|
184
186
|
const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
|
|
@@ -194,7 +196,8 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
194
196
|
const config = {
|
|
195
197
|
target: layer.id,
|
|
196
198
|
...definition,
|
|
197
|
-
|
|
199
|
+
...normalizeInitialLegendTitle(args.title, definition.title),
|
|
200
|
+
inferredTitle: args.title === false || !Object.hasOwn(args, "title"),
|
|
198
201
|
inferredSymbol,
|
|
199
202
|
position: options.position,
|
|
200
203
|
align: options.align,
|
|
@@ -207,8 +210,8 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
207
210
|
titleStyle: options.titleStyle,
|
|
208
211
|
itemGap: options.itemGap,
|
|
209
212
|
layout: options.layout,
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
...(options.overflow === undefined ? {} : { overflow: options.overflow }),
|
|
214
|
+
border: options.border
|
|
212
215
|
};
|
|
213
216
|
return config;
|
|
214
217
|
}
|
|
@@ -228,7 +231,7 @@ export const createCategoricalLegend = /* @__PURE__ */ action(
|
|
|
228
231
|
})
|
|
229
232
|
);
|
|
230
233
|
|
|
231
|
-
|
|
234
|
+
function resolveRawLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
|
|
232
235
|
validateOptionObject(args, undefined, "createLegend");
|
|
233
236
|
const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
|
|
234
237
|
const channels = args.channels;
|
|
@@ -252,15 +255,23 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
const continuousColorCandidates = candidates.filter(layer => {
|
|
255
|
-
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
|
|
258
|
+
const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type)
|
|
256
259
|
? layer.encoding?.color
|
|
257
260
|
: undefined;
|
|
258
261
|
const scale = findSemanticScale(program, encoding?.scale);
|
|
259
|
-
return scale?.type
|
|
262
|
+
return isContinuousColorScaleType(scale?.type);
|
|
260
263
|
});
|
|
261
264
|
const continuousColor = requestedCandidate(
|
|
262
265
|
program, args.target, continuousColorCandidates
|
|
263
266
|
);
|
|
267
|
+
if (args.representation !== undefined && channels !== undefined && channels.length > 1) {
|
|
268
|
+
if (!continuousColor || !channels.includes("color")) throw new Error("Legend representation requires selected continuous color.");
|
|
269
|
+
const { representation, values, count, symbol, gradient, ...common } = args;
|
|
270
|
+
const steps = channels.flatMap(channel => resolveRawLegendCreationPlan(program,
|
|
271
|
+
channel === "color" ? { ...args, channels: [channel] }
|
|
272
|
+
: { ...common, channels: [channel] }, layers).steps);
|
|
273
|
+
return { steps, finish: "always" };
|
|
274
|
+
}
|
|
264
275
|
if (
|
|
265
276
|
(channels?.length === 1 && channels[0] === "color" && continuousColor) ||
|
|
266
277
|
(channels === undefined && continuousColor)
|
|
@@ -268,7 +279,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
268
279
|
return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
|
|
269
280
|
}
|
|
270
281
|
const intervalColorCandidates = candidates.filter(layer => {
|
|
271
|
-
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
|
|
282
|
+
const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
|
|
272
283
|
const scale = findSemanticScale(program, encoding?.scale);
|
|
273
284
|
return ["quantize", "quantile", "threshold"].includes(scale?.type);
|
|
274
285
|
});
|
|
@@ -281,7 +292,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
281
292
|
}
|
|
282
293
|
const continuousStrokeCandidates = candidates.filter(layer => {
|
|
283
294
|
const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
|
|
284
|
-
return scale?.type
|
|
295
|
+
return isContinuousColorScaleType(scale?.type);
|
|
285
296
|
});
|
|
286
297
|
const continuousStroke = requestedCandidate(
|
|
287
298
|
program,
|
|
@@ -365,6 +376,9 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
365
376
|
if (combined) steps.push({ op: "createSizeLegend", args: {
|
|
366
377
|
target: requestedPoint.id,
|
|
367
378
|
...(count === undefined ? {} : { count }),
|
|
379
|
+
...(categoricalArgs.overflow !== undefined &&
|
|
380
|
+
findSemanticScale(program, requestedPoint.encoding.size.scale)?.type === "ordinal"
|
|
381
|
+
? { overflow: categoricalArgs.overflow } : {}),
|
|
368
382
|
...(categoricalArgs.labels === undefined ? {} : { labels: {
|
|
369
383
|
...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
|
|
370
384
|
...(sizeFormat === undefined ? {} : { format: sizeFormat })
|
|
@@ -389,11 +403,50 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
389
403
|
return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
|
|
390
404
|
}
|
|
391
405
|
|
|
406
|
+
export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
|
|
407
|
+
validateLegendGroupOptions(args);
|
|
408
|
+
const { blockLayout: _layout, at: _at, ...raw } = args;
|
|
409
|
+
const groups = program.guideConfigs.legendGroups ?? {};
|
|
410
|
+
if (Object.keys(groups).length > 0 && raw.position === undefined) {
|
|
411
|
+
const target = creationLegendTarget(program, args);
|
|
412
|
+
const placement = groups[target]?.placement;
|
|
413
|
+
if (placement?.position !== undefined) {
|
|
414
|
+
raw.position = placement.position;
|
|
415
|
+
raw.align = args.align ?? placement.align;
|
|
416
|
+
raw.offset = args.offset ?? placement.offset;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (raw.position === undefined && args.direction === "horizontal" &&
|
|
420
|
+
(args.at !== undefined || Object.keys(groups).length > 0)) raw.position = "top";
|
|
421
|
+
const initial = resolveRawLegendCreationPlan(program, raw, layers);
|
|
422
|
+
const inherited = Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0;
|
|
423
|
+
const corner = PLOT_LEGEND_POSITIONS.includes(args.position) &&
|
|
424
|
+
(initial.steps.length > 1 || initial.steps[0]?.op !== "createCategoricalLegend" ||
|
|
425
|
+
Object.keys(program.guideConfigs.legend ?? {}).length > 0);
|
|
426
|
+
if (args.blockLayout === undefined && args.at === undefined && !inherited && !corner) return initial;
|
|
427
|
+
const target = creationLegendTarget(program, args);
|
|
428
|
+
const first = initial.steps[0];
|
|
429
|
+
const nativeOffset = program.guideConfigs.legendGroups?.[target] !== undefined ? 30 : first?.op === "createCategoricalLegend"
|
|
430
|
+
? resolveCategoricalLegendConfig(program, first.args).offset
|
|
431
|
+
: first?.op === "createSizeLegend" ? resolveSizeLegendConfig(program, first.args).offset : 30;
|
|
432
|
+
const prepared = prepareLegendGroup(program, args, target, { creation: true, corner, nativeOffset });
|
|
433
|
+
if (!prepared.grouped) return initial;
|
|
434
|
+
if (prepared.options.position === undefined && raw.position !== undefined) {
|
|
435
|
+
prepared.options.position = raw.position;
|
|
436
|
+
}
|
|
437
|
+
return { ...resolveRawLegendCreationPlan(prepared.program, prepared.options, layers),
|
|
438
|
+
legendGroup: { target, config: prepared.program.guideConfigs.legendGroups[target] } };
|
|
439
|
+
}
|
|
440
|
+
|
|
392
441
|
export function applyLegendCreationPlan(program, plan) {
|
|
393
442
|
if (plan.steps.length === 0) return program;
|
|
394
|
-
let next = program
|
|
443
|
+
let next = plan.legendGroup === undefined ? program : program._withMaterializationConfig(
|
|
444
|
+
["guides", "legendGroups", plan.legendGroup.target], plan.legendGroup.config);
|
|
395
445
|
for (const step of plan.steps) {
|
|
396
446
|
const existingSize = next.guideConfigs.legend?.size;
|
|
447
|
+
if (plan.combined && existingSize !== undefined && step.op === "createCategoricalLegend") {
|
|
448
|
+
next = next._withLegendConfig("size", { ...existingSize, inheritPosition: true });
|
|
449
|
+
}
|
|
397
450
|
// The plan already validated that a retained size block is compatible.
|
|
398
451
|
if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
|
|
399
452
|
next = next[step.op](step.args);
|
|
@@ -406,7 +459,7 @@ export function applyLegendCreationPlan(program, plan) {
|
|
|
406
459
|
export const createLegend = /* @__PURE__ */ action(
|
|
407
460
|
{ op: "createLegend", description: "Create an inferred legend for selected channels." },
|
|
408
461
|
withGuideLayoutValidation(function (args = {}) {
|
|
409
|
-
return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
|
|
462
|
+
return recordLegendOffsetIntent(this, applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args)), args, { creation: true });
|
|
410
463
|
})
|
|
411
464
|
);
|
|
412
465
|
|