ggaction 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +79 -24
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +153 -11
- package/src/actions/errorBars/edit.js +365 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +138 -14
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +60 -4
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +9 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +86 -23
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { editLegendBackground, resolveLegendBackgroundFromBounds } from "./continuous/common.js";
|
|
1
2
|
import { action } from "../../../core/action.js";
|
|
2
3
|
import { noOptions } from "../../../core/validation.js";
|
|
3
4
|
import {
|
|
@@ -8,6 +9,9 @@ import { finiteMidpoint } from "../../../grammar/numeric.js";
|
|
|
8
9
|
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
9
10
|
import {
|
|
10
11
|
resolveHorizontalLegendLane,
|
|
12
|
+
resolveHorizontalLegendGroup,
|
|
13
|
+
resolveSingleHorizontalLegendPlacement,
|
|
14
|
+
isHorizontalEdgeLegend,
|
|
11
15
|
resolveSideLegendLane
|
|
12
16
|
} from "../../../layout/legendLane.js";
|
|
13
17
|
import { findCanvasGraphic } from
|
|
@@ -33,16 +37,13 @@ function categoricalFor(program, target) {
|
|
|
33
37
|
|
|
34
38
|
function legendPosition(program, kind, config) {
|
|
35
39
|
if (kind === "size") {
|
|
36
|
-
return categoricalFor(program, config.target)?.position ??
|
|
40
|
+
return categoricalFor(program, config.target)?.position ?? config.position;
|
|
37
41
|
}
|
|
38
|
-
if (kind === "strokeWidth" || kind === "interval") return "right";
|
|
39
42
|
return config.position;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function requestedOffset(
|
|
43
|
-
return
|
|
44
|
-
? 30
|
|
45
|
-
: config.offset;
|
|
45
|
+
function requestedOffset(config) {
|
|
46
|
+
return config.offset;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
function existingIds(program, kind) {
|
|
@@ -117,7 +118,8 @@ function blockDescriptor(program, kind, config) {
|
|
|
117
118
|
id: kind,
|
|
118
119
|
kind,
|
|
119
120
|
target: config.target,
|
|
120
|
-
|
|
121
|
+
border: borderFor(kind, config),
|
|
122
|
+
offset: requestedOffset(config),
|
|
121
123
|
bounds,
|
|
122
124
|
title,
|
|
123
125
|
symbol: {
|
|
@@ -130,19 +132,17 @@ function blockDescriptor(program, kind, config) {
|
|
|
130
132
|
x: textAnchor(program, components.labelId),
|
|
131
133
|
width: labels.right - labels.left
|
|
132
134
|
},
|
|
133
|
-
occupiedBounds:
|
|
134
|
-
|
|
135
|
-
:
|
|
136
|
-
|
|
137
|
-
components.backgroundId
|
|
138
|
-
),
|
|
135
|
+
occupiedBounds: unionConcreteGraphicBounds(program.graphicSpec, [
|
|
136
|
+
...foregroundIds,
|
|
137
|
+
...(components.backgroundId === undefined ? [] : [components.backgroundId])
|
|
138
|
+
]),
|
|
139
139
|
...components,
|
|
140
140
|
foregroundIds
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function borderFor(kind, config) {
|
|
145
|
-
return ["series", "color", "gradient", "opacity"].includes(kind)
|
|
145
|
+
return ["series", "color", "gradient", "opacity", "interval", "strokeWidth", "size"].includes(kind)
|
|
146
146
|
? config.border
|
|
147
147
|
: false;
|
|
148
148
|
}
|
|
@@ -159,7 +159,8 @@ function groupBlocks(blocks, configs) {
|
|
|
159
159
|
) {
|
|
160
160
|
groups.push({
|
|
161
161
|
id: `${block.kind}+size`,
|
|
162
|
-
blocks: [block, next
|
|
162
|
+
blocks: [block, next.backgroundId === undefined ? next : { ...next,
|
|
163
|
+
bounds: next.occupiedBounds, inset: next.border.padding + next.border.lineWidth / 2 }],
|
|
163
164
|
border: borderFor(block.kind, config),
|
|
164
165
|
backgroundId: block.backgroundId
|
|
165
166
|
});
|
|
@@ -193,13 +194,15 @@ export function hasMultiSideLegendLane(program) {
|
|
|
193
194
|
return ["right", "left"].some(side => edgeKinds(program, side).length > 1);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
export function
|
|
197
|
-
return ["top", "bottom"].some(edge => edgeKinds(program, edge).
|
|
197
|
+
export function hasHorizontalLegendLane(program) {
|
|
198
|
+
return ["top", "bottom"].some(edge => edgeKinds(program, edge).some(
|
|
199
|
+
([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
|
|
200
|
+
));
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
export function
|
|
203
|
+
export function hasLegendLane(program) {
|
|
201
204
|
return hasMultiSideLegendLane(program) ||
|
|
202
|
-
|
|
205
|
+
hasHorizontalLegendLane(program);
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
function translateCommands(commands, dx, dy) {
|
|
@@ -269,35 +272,20 @@ function translateGraphic(program, id, dx, dy) {
|
|
|
269
272
|
return next;
|
|
270
273
|
}
|
|
271
274
|
|
|
272
|
-
function yAxisBounds(program) {
|
|
273
|
-
const ids = ["yAxisLine", "yAxisTicks", "yAxisLabels", "yAxisTitle"]
|
|
274
|
-
.filter(id => program.graphicSpec.objects[id] !== undefined);
|
|
275
|
-
return ids.length === 0
|
|
276
|
-
? undefined
|
|
277
|
-
: unionConcreteGraphicBounds(program.graphicSpec, ids);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function horizontalCollisionBounds(program, edge) {
|
|
281
|
-
const ids = edge === "top"
|
|
282
|
-
? ["chartTitle", "chartSubtitle"]
|
|
283
|
-
: ["xAxisLine", "xAxisTicks", "xAxisLabels", "xAxisTitle"];
|
|
284
|
-
return ids
|
|
285
|
-
.filter(id => program.graphicSpec.objects[id] !== undefined)
|
|
286
|
-
.map(id => resolveConcreteGraphicBounds(program.graphicSpec, id))
|
|
287
|
-
.filter(bounds => bounds !== undefined);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
275
|
function horizontalGroups(program, groups) {
|
|
291
276
|
return groups.map(group => {
|
|
292
277
|
const representative = group.blocks[0];
|
|
293
278
|
const config = program.guideConfigs.legend?.[representative.kind];
|
|
294
|
-
const
|
|
279
|
+
const atomic = group.blocks.length > 1;
|
|
280
|
+
const titleId = atomic ? undefined : representative.titleId;
|
|
295
281
|
const contentIds = group.blocks.flatMap(block => block.foregroundIds)
|
|
296
282
|
.filter(id => id !== titleId);
|
|
283
|
+
if (atomic) contentIds.push(...group.blocks.slice(1).flatMap(block =>
|
|
284
|
+
block.backgroundId === undefined ? [] : [block.backgroundId]));
|
|
297
285
|
const content = unionConcreteGraphicBounds(program.graphicSpec, contentIds);
|
|
298
286
|
const foreground = unionConcreteGraphicBounds(
|
|
299
287
|
program.graphicSpec,
|
|
300
|
-
|
|
288
|
+
[...contentIds, ...(titleId === undefined ? [] : [titleId])]
|
|
301
289
|
);
|
|
302
290
|
if (content === undefined || foreground === undefined) {
|
|
303
291
|
throw new Error(`Legend lane could not measure ${group.id} content.`);
|
|
@@ -308,11 +296,12 @@ function horizontalGroups(program, groups) {
|
|
|
308
296
|
: border.padding + border.lineWidth / 2;
|
|
309
297
|
return {
|
|
310
298
|
id: group.id,
|
|
299
|
+
atomic,
|
|
311
300
|
titleId,
|
|
312
301
|
contentIds,
|
|
313
|
-
title: representative.title,
|
|
302
|
+
title: atomic ? undefined : representative.title,
|
|
314
303
|
inline: config?.titlePosition === "left",
|
|
315
|
-
element: representative.symbol.bounds,
|
|
304
|
+
element: atomic ? foreground : representative.symbol.bounds,
|
|
316
305
|
content,
|
|
317
306
|
horizontal: { left: foreground.left, right: foreground.right,
|
|
318
307
|
top: foreground.top, bottom: foreground.bottom },
|
|
@@ -323,6 +312,33 @@ function horizontalGroups(program, groups) {
|
|
|
323
312
|
});
|
|
324
313
|
}
|
|
325
314
|
|
|
315
|
+
function applyHorizontalPlan(program, groups, plan) {
|
|
316
|
+
let next = program;
|
|
317
|
+
const byId = new Map(groups.map(group => [group.id, group]));
|
|
318
|
+
for (const placement of plan.placements) {
|
|
319
|
+
const group = byId.get(placement.id);
|
|
320
|
+
if (group.titleId !== undefined) {
|
|
321
|
+
next = translateGraphic(next, group.titleId, placement.dx, placement.titleDy);
|
|
322
|
+
}
|
|
323
|
+
for (const id of group.contentIds) {
|
|
324
|
+
next = translateGraphic(next, id, placement.dx, placement.contentDy);
|
|
325
|
+
}
|
|
326
|
+
if (placement.background !== undefined) {
|
|
327
|
+
for (const property of ["x", "y", "width", "height"]) {
|
|
328
|
+
next = next.editGraphics({ target: placement.background.id, property,
|
|
329
|
+
value: placement.background[property] });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (plan.background !== undefined) {
|
|
334
|
+
for (const property of ["x", "y", "width", "height"]) {
|
|
335
|
+
next = next.editGraphics({ target: plan.background.id, property,
|
|
336
|
+
value: plan.background[property] });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
|
|
326
342
|
export const rematerializeSideLegendLane = action(
|
|
327
343
|
{
|
|
328
344
|
op: "rematerializeSideLegendLane",
|
|
@@ -346,8 +362,7 @@ export const rematerializeSideLegendLane = action(
|
|
|
346
362
|
side,
|
|
347
363
|
plot,
|
|
348
364
|
canvas,
|
|
349
|
-
groups: groupBlocks(blocks, configs)
|
|
350
|
-
axisBounds: yAxisBounds(this)
|
|
365
|
+
groups: groupBlocks(blocks, configs)
|
|
351
366
|
});
|
|
352
367
|
return [{ plan, blocks }];
|
|
353
368
|
});
|
|
@@ -387,6 +402,12 @@ export const rematerializeSideLegendLane = action(
|
|
|
387
402
|
value: "left"
|
|
388
403
|
});
|
|
389
404
|
}
|
|
405
|
+
for (const block of blocks) {
|
|
406
|
+
if (block.backgroundId === undefined || plan.backgrounds.some(item => item.id === block.backgroundId)) continue;
|
|
407
|
+
const bounds = unionConcreteGraphicBounds(next.graphicSpec, block.foregroundIds);
|
|
408
|
+
const background = resolveLegendBackgroundFromBounds([bounds], block.border, canvas, "Legend");
|
|
409
|
+
next = editLegendBackground(next, block.backgroundId, background, block.border);
|
|
410
|
+
}
|
|
390
411
|
for (const background of plan.backgrounds) {
|
|
391
412
|
for (const property of ["x", "y", "width", "height"]) {
|
|
392
413
|
next = next.editGraphics({
|
|
@@ -414,55 +435,46 @@ export const rematerializeHorizontalLegendLane = action(
|
|
|
414
435
|
throw new Error("Legend lane requires resolved Canvas and plot bounds.");
|
|
415
436
|
}
|
|
416
437
|
const configs = this.guideConfigs.legend ?? {};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
blockDescriptor(this, kind, config)
|
|
438
|
+
let next = this;
|
|
439
|
+
for (const edge of ["top", "bottom"]) {
|
|
440
|
+
const entries = edgeKinds(next, edge).filter(
|
|
441
|
+
([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
|
|
422
442
|
);
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
443
|
+
if (entries.length === 0) continue;
|
|
444
|
+
if (entries.length === 1) {
|
|
445
|
+
const [kind, config] = entries[0];
|
|
446
|
+
const ids = existingIds(next, kind);
|
|
447
|
+
const bounds = unionConcreteGraphicBounds(next.graphicSpec, ids);
|
|
448
|
+
const { dx, dy } = resolveSingleHorizontalLegendPlacement({
|
|
449
|
+
plot, canvas, config, bounds
|
|
450
|
+
});
|
|
451
|
+
for (const id of ids) next = translateGraphic(next, id, dx, dy);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
let groups = groupBlocks(entries.map(([kind, config]) =>
|
|
455
|
+
blockDescriptor(next, kind, config)), configs);
|
|
456
|
+
for (const group of groups.filter(group => group.blocks.length > 1)) {
|
|
457
|
+
const config = configs[group.blocks[0].kind];
|
|
458
|
+
const children = horizontalGroups(next, group.blocks.map((block, index) => ({
|
|
459
|
+
id: block.id, blocks: [block], border: index === 0 ? false : block.border,
|
|
460
|
+
backgroundId: index === 0 ? undefined : block.backgroundId
|
|
461
|
+
}))).map(child => ({ ...child, element: child.content }));
|
|
462
|
+
const plan = resolveHorizontalLegendGroup({ edge, plot, canvas, groups: children,
|
|
463
|
+
align: config.align, offset: config.offset, border: group.border,
|
|
464
|
+
backgroundId: group.backgroundId });
|
|
465
|
+
next = applyHorizontalPlan(next, children, plan);
|
|
466
|
+
}
|
|
467
|
+
if (groups.length < 2) continue;
|
|
468
|
+
groups = groupBlocks(entries.map(([kind, config]) =>
|
|
469
|
+
blockDescriptor(next, kind, config)), configs);
|
|
470
|
+
const horizontal = horizontalGroups(next, groups);
|
|
426
471
|
const plan = resolveHorizontalLegendLane({
|
|
427
472
|
edge,
|
|
428
473
|
plot,
|
|
429
474
|
canvas,
|
|
430
|
-
groups: horizontal
|
|
431
|
-
collisionBounds: horizontalCollisionBounds(this, edge)
|
|
475
|
+
groups: horizontal
|
|
432
476
|
});
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
let next = this;
|
|
436
|
-
for (const { plan, groups } of plans) {
|
|
437
|
-
const byId = new Map(groups.map(group => [group.id, group]));
|
|
438
|
-
for (const placement of plan.placements) {
|
|
439
|
-
const group = byId.get(placement.id);
|
|
440
|
-
if (group.titleId !== undefined) {
|
|
441
|
-
next = translateGraphic(
|
|
442
|
-
next,
|
|
443
|
-
group.titleId,
|
|
444
|
-
placement.dx,
|
|
445
|
-
placement.titleDy
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
for (const id of group.contentIds) {
|
|
449
|
-
next = translateGraphic(
|
|
450
|
-
next,
|
|
451
|
-
id,
|
|
452
|
-
placement.dx,
|
|
453
|
-
placement.contentDy
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
if (placement.background !== undefined) {
|
|
457
|
-
for (const property of ["x", "y", "width", "height"]) {
|
|
458
|
-
next = next.editGraphics({
|
|
459
|
-
target: placement.background.id,
|
|
460
|
-
property,
|
|
461
|
-
value: placement.background[property]
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
477
|
+
next = applyHorizontalPlan(next, horizontal, plan);
|
|
466
478
|
}
|
|
467
479
|
return next;
|
|
468
480
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
legendGraphicIds, legendResourcePolicy, legendResourcePolicies
|
|
3
|
+
} from "../../../materialization/guides/resources.js";
|
|
4
|
+
import { findLayer } from "../../../selectors/layers.js";
|
|
5
|
+
import { resolveDefinition } from "./categorical/resolve.js";
|
|
6
|
+
import { normalizeRecipe, resolveLegendSymbol } from "./categorical/recipes.js";
|
|
7
|
+
import { resolveLayout, symbolGraphic } from "./categorical/layout.js";
|
|
8
|
+
import { resolveLegendGraphicPlacement } from "../../../materialization/graphicHierarchy.js";
|
|
9
|
+
|
|
10
|
+
export function removeLegendKinds(program, kinds) {
|
|
11
|
+
const removed = new Set(kinds);
|
|
12
|
+
const retainedSemanticKinds = new Set(
|
|
13
|
+
Object.entries(program.guideConfigs.legend ?? {})
|
|
14
|
+
.filter(([kind]) => !removed.has(kind))
|
|
15
|
+
.map(([kind]) => legendResourcePolicy(kind).semanticKind)
|
|
16
|
+
);
|
|
17
|
+
const semanticKinds = new Set(
|
|
18
|
+
kinds.map(kind => legendResourcePolicy(kind).semanticKind)
|
|
19
|
+
);
|
|
20
|
+
let next = program;
|
|
21
|
+
for (const kind of semanticKinds) {
|
|
22
|
+
if (
|
|
23
|
+
!retainedSemanticKinds.has(kind) &&
|
|
24
|
+
next.semanticSpec.guides.legend?.[kind] !== undefined
|
|
25
|
+
) {
|
|
26
|
+
next = next.editSemantic({
|
|
27
|
+
property: `guide.legend.${kind}`,
|
|
28
|
+
remove: true
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
|
|
33
|
+
if (next.graphicSpec.objects[id] !== undefined) {
|
|
34
|
+
next = next.editGraphics({ target: id, remove: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const kind of kinds) {
|
|
38
|
+
next = next._withoutMaterializationConfig(["guides", "legend", kind]);
|
|
39
|
+
}
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveCategoricalLegendRevision(program, kind, previous, channels, {
|
|
44
|
+
order = program.semanticSpec.guides.legend?.[kind]?.order, validateLayout = true
|
|
45
|
+
} = {}) {
|
|
46
|
+
const layer = findLayer(program, previous.target);
|
|
47
|
+
const definition = resolveDefinition(program, layer, channels,
|
|
48
|
+
previous.inferredTitle ? undefined : previous.title, order);
|
|
49
|
+
const config = {
|
|
50
|
+
...previous,
|
|
51
|
+
...definition,
|
|
52
|
+
symbol: normalizeRecipe(resolveLegendSymbol(program, layer, channels,
|
|
53
|
+
previous.inferredSymbol ? undefined : previous.symbol), definition.kind)
|
|
54
|
+
};
|
|
55
|
+
if (validateLayout) resolveLayout(program, config);
|
|
56
|
+
return { kind, config, order };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createCategoricalLegendFromConfig(program, config, order) {
|
|
60
|
+
const { kind } = config;
|
|
61
|
+
let next = program;
|
|
62
|
+
if (kind === "series") {
|
|
63
|
+
next = next
|
|
64
|
+
.editSemantic({
|
|
65
|
+
property: "guide.legend.series.channels",
|
|
66
|
+
value: config.channels
|
|
67
|
+
})
|
|
68
|
+
.editSemantic({
|
|
69
|
+
property: "guide.legend.series.scales",
|
|
70
|
+
value: config.scales
|
|
71
|
+
})
|
|
72
|
+
.editSemantic({
|
|
73
|
+
property: "guide.legend.series.title",
|
|
74
|
+
value: config.title
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
next = next
|
|
78
|
+
.editSemantic({
|
|
79
|
+
property: "guide.legend.color.scale",
|
|
80
|
+
value: config.scales[0]
|
|
81
|
+
})
|
|
82
|
+
.editSemantic({
|
|
83
|
+
property: "guide.legend.color.title",
|
|
84
|
+
value: config.title
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (order !== undefined && order !== "scale") {
|
|
88
|
+
next = next.editSemantic({
|
|
89
|
+
property: `guide.legend.${kind}.order`,
|
|
90
|
+
value: order
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
next = next._withLegendConfig(kind, config);
|
|
94
|
+
if (config.border !== false) next = next.createLegendBackground();
|
|
95
|
+
next = next.createLegendSymbols().createLegendLabels();
|
|
96
|
+
return config.titleVisible === false ? next : next.createLegendTitle();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Categorical content precedes its sampled companions in both creation and revision.
|
|
100
|
+
export function resolveCategoricalLegendPlacement(program) {
|
|
101
|
+
const companions = new Set(legendResourcePolicies()
|
|
102
|
+
.filter(policy => policy.family !== "categorical" && program.guideConfigs.legend?.[policy.kind] !== undefined)
|
|
103
|
+
.flatMap(policy => legendGraphicIds(policy.kind)));
|
|
104
|
+
const before = program.graphicSpec.objects.canvas?.children?.find(id => companions.has(id));
|
|
105
|
+
return resolveLegendGraphicPlacement(program, before === undefined ? {} : { before });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function categoricalSymbolIds(config) {
|
|
109
|
+
return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function reconcileCategoricalSymbols(program, previous, config) {
|
|
113
|
+
const oldSymbols = categoricalSymbolIds(previous);
|
|
114
|
+
const newSymbols = categoricalSymbolIds(config);
|
|
115
|
+
const sameOrder = oldSymbols.size === newSymbols.size &&
|
|
116
|
+
[...oldSymbols].every((id, index) => id === [...newSymbols][index]);
|
|
117
|
+
let next = program;
|
|
118
|
+
if (!sameOrder) {
|
|
119
|
+
for (const id of oldSymbols) {
|
|
120
|
+
if (next.graphicSpec.objects[id] !== undefined) {
|
|
121
|
+
next = next.editGraphics({ target: id, remove: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const layer of config.symbol.layers) {
|
|
126
|
+
if (next.graphicSpec.objects[symbolGraphic(config, layer.type)] === undefined) {
|
|
127
|
+
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[layer.type];
|
|
128
|
+
next = next[`createLegendSymbol${suffix}`]();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { action } from "../../../core/action.js";
|
|
2
2
|
import { validateKeys } from "../../../core/validation.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
removeLegendKinds,
|
|
5
|
+
resolveCategoricalLegendRevision,
|
|
6
|
+
createCategoricalLegendFromConfig
|
|
7
|
+
} from "./lifecycle.js";
|
|
8
|
+
import { resolveLegendTarget, validateLegendChannels } from "./target.js";
|
|
9
|
+
|
|
10
|
+
export { removeLegendKinds } from "./lifecycle.js";
|
|
7
11
|
|
|
8
12
|
const OPTIONS = Object.freeze(["target", "channels"]);
|
|
9
13
|
|
|
@@ -18,85 +22,33 @@ function legendKindChannels(kind, config) {
|
|
|
18
22
|
}[kind];
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
throw new TypeError("removeLegend channels must be an array.");
|
|
24
|
-
}
|
|
25
|
-
if (channels.length === 0) {
|
|
26
|
-
throw new Error("removeLegend channels must select at least one channel.");
|
|
27
|
-
}
|
|
28
|
-
const seen = new Set();
|
|
29
|
-
for (const channel of channels) {
|
|
30
|
-
if (!LEGEND_CHANNELS.includes(channel)) {
|
|
31
|
-
throw new Error(`Unsupported legend channel "${channel}".`);
|
|
32
|
-
}
|
|
33
|
-
if (seen.has(channel)) {
|
|
34
|
-
throw new Error(`removeLegend channels contains duplicate "${channel}".`);
|
|
35
|
-
}
|
|
36
|
-
seen.add(channel);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function resolveRequestedKinds(program, target, channels) {
|
|
41
|
-
validateRequestedChannels(channels);
|
|
25
|
+
function resolveRequestedRemoval(program, target, channels) {
|
|
26
|
+
validateLegendChannels(channels, "removeLegend");
|
|
42
27
|
const requested = new Set(channels);
|
|
43
28
|
const matched = new Set();
|
|
44
29
|
const kinds = [];
|
|
30
|
+
let partial;
|
|
45
31
|
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
46
32
|
if (config?.target !== target) continue;
|
|
47
33
|
const owned = legendKindChannels(kind, config);
|
|
48
34
|
const overlap = owned.filter(channel => requested.has(channel));
|
|
49
|
-
if (overlap.length
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
if (overlap.length === owned.length) {
|
|
56
|
-
kinds.push(kind);
|
|
57
|
-
for (const channel of owned) matched.add(channel);
|
|
35
|
+
if (overlap.length === 0) continue;
|
|
36
|
+
kinds.push(kind);
|
|
37
|
+
for (const channel of overlap) matched.add(channel);
|
|
38
|
+
if (overlap.length !== owned.length) {
|
|
39
|
+
partial = { kind, config, channels: owned.filter(channel => !requested.has(channel)) };
|
|
58
40
|
}
|
|
59
41
|
}
|
|
60
42
|
const missing = channels.filter(channel => !matched.has(channel));
|
|
61
43
|
if (missing.length > 0) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Legend target "${target}" has no complete block for channel "${missing[0]}".`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return kinds;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function removeLegendKinds(program, kinds) {
|
|
70
|
-
const removed = new Set(kinds);
|
|
71
|
-
const retainedSemanticKinds = new Set(
|
|
72
|
-
Object.entries(program.guideConfigs.legend ?? {})
|
|
73
|
-
.filter(([kind]) => !removed.has(kind))
|
|
74
|
-
.map(([kind]) => legendResourcePolicy(kind).semanticKind)
|
|
75
|
-
);
|
|
76
|
-
const semanticKinds = new Set(
|
|
77
|
-
kinds.map(kind => legendResourcePolicy(kind).semanticKind)
|
|
78
|
-
);
|
|
79
|
-
let next = program;
|
|
80
|
-
for (const kind of semanticKinds) {
|
|
81
|
-
if (
|
|
82
|
-
!retainedSemanticKinds.has(kind) &&
|
|
83
|
-
next.semanticSpec.guides.legend?.[kind] !== undefined
|
|
84
|
-
) {
|
|
85
|
-
next = next.editSemantic({
|
|
86
|
-
property: `guide.legend.${kind}`,
|
|
87
|
-
remove: true
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
|
|
92
|
-
if (next.graphicSpec.objects[id] !== undefined) {
|
|
93
|
-
next = next.editGraphics({ target: id, remove: true });
|
|
94
|
-
}
|
|
44
|
+
throw new Error(`Legend target "${target}" has no complete block for channel "${missing[0]}".`);
|
|
95
45
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
46
|
+
// Remove departing sibling configs from the immutable layout preflight view.
|
|
47
|
+
const view = kinds.reduce((next, kind) =>
|
|
48
|
+
next._withoutMaterializationConfig(["guides", "legend", kind]), program);
|
|
49
|
+
const revision = partial === undefined ? undefined
|
|
50
|
+
: resolveCategoricalLegendRevision(view, partial.kind, partial.config, partial.channels);
|
|
51
|
+
return { kinds, revision };
|
|
100
52
|
}
|
|
101
53
|
|
|
102
54
|
export function removeOwnedColorLegends(program, target) {
|
|
@@ -108,17 +60,20 @@ export function removeOwnedColorLegends(program, target) {
|
|
|
108
60
|
}
|
|
109
61
|
|
|
110
62
|
export const removeLegend = action(
|
|
111
|
-
{ op: "removeLegend", description: "Remove
|
|
63
|
+
{ op: "removeLegend", description: "Remove selected legend content or every block owned by one mark." },
|
|
112
64
|
function (args = {}) {
|
|
113
65
|
validateKeys(args, OPTIONS, "removeLegend");
|
|
114
66
|
const target = resolveLegendTarget(this, args.target, "removeLegend");
|
|
115
67
|
const targetKinds = Object.entries(this.guideConfigs.legend ?? {})
|
|
116
68
|
.filter(([, config]) => config?.target === target)
|
|
117
69
|
.map(([kind]) => kind);
|
|
118
|
-
const kinds = args.channels === undefined
|
|
119
|
-
? targetKinds
|
|
120
|
-
:
|
|
70
|
+
const { kinds, revision } = args.channels === undefined
|
|
71
|
+
? { kinds: targetKinds }
|
|
72
|
+
: resolveRequestedRemoval(this, target, args.channels);
|
|
121
73
|
let next = removeLegendKinds(this, kinds);
|
|
74
|
+
if (revision !== undefined) {
|
|
75
|
+
next = createCategoricalLegendFromConfig(next, revision.config, revision.order);
|
|
76
|
+
}
|
|
122
77
|
const retainedKinds = Object.keys(next.guideConfigs.legend ?? {});
|
|
123
78
|
if (args.channels === undefined) {
|
|
124
79
|
return retainedKinds.length === 0
|
|
@@ -128,7 +83,7 @@ export const removeLegend = action(
|
|
|
128
83
|
const remainingTargetKinds = targetKinds.filter(kind => !kinds.includes(kind));
|
|
129
84
|
const removedCategorical = kinds.some(kind => ["series", "color"].includes(kind));
|
|
130
85
|
if (
|
|
131
|
-
removedCategorical &&
|
|
86
|
+
removedCategorical && revision === undefined &&
|
|
132
87
|
remainingTargetKinds.includes("size") &&
|
|
133
88
|
next.guideConfigs.legend.size.inheritAppearance === true
|
|
134
89
|
) {
|