ggaction 0.0.23 → 0.0.26
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 +65 -0
- package/README.md +2 -2
- 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 +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +6 -4
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +4 -2
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -1
- package/src/actions/guides/axes/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +2 -537
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- package/src/actions/guides/axes/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- package/src/actions/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +23 -10
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +20 -3
- package/src/core/facetGap.js +44 -0
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +41 -4
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +32 -10
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/facets.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/layout/title.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/planner.js +19 -4
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +116 -25
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +4 -2
- package/types/index.d.ts +14 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +572 -372
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { closedAction } from "../../core/action.js";
|
|
2
|
+
import { normalizeOverlayBinding, overlayGroupFields, overlayPositionEncoding } from "../../grammar/overlay.js";
|
|
3
|
+
import { requireLayer, resolveEligibleLayer } from "../../selectors/layers.js";
|
|
4
|
+
import { resolveOverlaySeries, overlaySegments } from "../../materialization/overlayItems.js";
|
|
5
|
+
import { resolveFacadeId } from "../charts/shared.js";
|
|
6
|
+
import { buildCurvePathCommands } from "../../grammar/curveCommands.js";
|
|
7
|
+
import { mapScaleConsumerValues } from "../../materialization/scales/map.js";
|
|
8
|
+
import { createPointShapeGraphic } from "../../grammar/pointShapes.js";
|
|
9
|
+
import { editMarkGraphic } from "../marks/shared.js";
|
|
10
|
+
import { rematerializeHighlightBaseline } from "../marks/lifecycle.js";
|
|
11
|
+
import { detachOverlay } from "./snapshot.js";
|
|
12
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
13
|
+
import { requestedStrokeDetails, STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
|
|
14
|
+
import { resolveDirectionValues } from "../../grammar/direction.js";
|
|
15
|
+
|
|
16
|
+
function positionBindings(program, id, source) {
|
|
17
|
+
let next = program.editSemantic({ property: `layer[${id}].data`, value: source.data });
|
|
18
|
+
next = next.editSemantic({ property: `layer[${id}].coordinate`, value: source.coordinate });
|
|
19
|
+
for (const channel of ["x", "y"]) {
|
|
20
|
+
const encoding = overlayPositionEncoding(source, channel);
|
|
21
|
+
if (requireLayer(next, id).encoding?.[channel]) {
|
|
22
|
+
next = next.editSemantic({ property: `layer[${id}].encoding.${channel}`, remove: true });
|
|
23
|
+
}
|
|
24
|
+
for (const property of ["field", "fieldType", "scale", "temporalUnit"]) {
|
|
25
|
+
if (encoding?.[property] !== undefined) next = next.editSemantic({
|
|
26
|
+
property: `layer[${id}].encoding.${channel}.${property}`, value: encoding[property]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mappedAppearance(program, layer, channel, members, fallback) {
|
|
34
|
+
const encoding = layer.encoding?.[channel];
|
|
35
|
+
if (!encoding?.scale) return fallback;
|
|
36
|
+
const values = [...new Set(members.map(row => row[encoding.field]).filter(value => value != null))];
|
|
37
|
+
if (!values.length) return fallback;
|
|
38
|
+
if (values.length > 1) {
|
|
39
|
+
throw new Error(`Overlay ${channel} requires one value per visible item or series; choose a grouping field.`);
|
|
40
|
+
}
|
|
41
|
+
return mapScaleConsumerValues([values[0]], program.resolvedScales[encoding.scale], channel)[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function synchronizeSourceAppearance(program, id, source, binding) {
|
|
45
|
+
let next = program;
|
|
46
|
+
const fields = binding.series === "source" ? overlayGroupFields(source) : [];
|
|
47
|
+
if (requireLayer(next, id).encoding?.group) next = next.editSemantic({ property: `layer[${id}].encoding.group`, remove: true });
|
|
48
|
+
if (fields.length) next = next.editSemantic({ property: `layer[${id}].encoding.group.fields`, value: fields })
|
|
49
|
+
.editSemantic({ property: `layer[${id}].encoding.group.fieldType`, value: "nominal" });
|
|
50
|
+
if (!binding.inheritColor) return next;
|
|
51
|
+
if (requireLayer(next, id).encoding?.color) next = next.editSemantic({ property: `layer[${id}].encoding.color`, remove: true });
|
|
52
|
+
const color = source.encoding?.color;
|
|
53
|
+
if (binding.series === "source" && color?.scale &&
|
|
54
|
+
(requireLayer(next, id).mark.type === "point" || fields.includes(color.field))) {
|
|
55
|
+
for (const key of ["field", "fieldType", "scale", "temporalUnit"]) {
|
|
56
|
+
if (color[key] !== undefined) next = next.editSemantic({ property: `layer[${id}].encoding.color.${key}`, value: color[key] });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const rematerializeOverlay = /* @__PURE__ */ closedAction({
|
|
63
|
+
op: "rematerializeOverlay", description: "Rebuild an overlay from its source's final visible items."
|
|
64
|
+
}, ["id"], function ({ id } = {}) {
|
|
65
|
+
const highlighted = rematerializeHighlightBaseline(this, { target: id,
|
|
66
|
+
operation: "rematerializeOverlay", resetProperty: "items", resetValue: [] });
|
|
67
|
+
if (highlighted !== undefined) return highlighted;
|
|
68
|
+
const previous = requireLayer(this, id);
|
|
69
|
+
const binding = normalizeOverlayBinding(previous.derivedBindings?.overlay);
|
|
70
|
+
const source = requireLayer(this, binding.source);
|
|
71
|
+
const series = resolveOverlaySeries(this, binding);
|
|
72
|
+
let next = synchronizeSourceAppearance(positionBindings(this, id, source), id, source, binding);
|
|
73
|
+
for (const [channel, encoding] of Object.entries(requireLayer(next, id).encoding)) {
|
|
74
|
+
if (["x", "y"].includes(channel) || channel === "color" && binding.inheritColor || !encoding.scale) continue;
|
|
75
|
+
next = next.rematerializeScale({ id: encoding.scale, marks: false, guides: false });
|
|
76
|
+
}
|
|
77
|
+
const layer = requireLayer(next, id), config = next.markConfigs[id];
|
|
78
|
+
const color = (members, fallback) => mappedAppearance(next, layer, "color", members, fallback);
|
|
79
|
+
if (layer.mark.type === "line") {
|
|
80
|
+
const entries = series.flatMap(group => {
|
|
81
|
+
return overlaySegments(group, binding).map(points => {
|
|
82
|
+
const positions = points.map(({ x, y }) => ({ x, y }));
|
|
83
|
+
return { type: "path", properties: {
|
|
84
|
+
commands: [...(positions.length === 1 ? [{ op: "M", ...positions[0] }]
|
|
85
|
+
: buildCurvePathCommands(positions, config.curve ?? "linear", config.tension)),
|
|
86
|
+
...(config.closed ? [{ op: "Z" }] : [])],
|
|
87
|
+
stroke: mappedAppearance(next, layer, "stroke", group.members,
|
|
88
|
+
config.stroke ?? color(group.members, "#4c78a8")),
|
|
89
|
+
strokeWidth: mappedAppearance(next, layer, "strokeWidth", group.members, config.strokeWidth ?? 2),
|
|
90
|
+
strokeDash: mappedAppearance(next, layer, "strokeDash", group.members, config.strokeDash ?? []),
|
|
91
|
+
...requestedStrokeDetails(config, "Overlay line"),
|
|
92
|
+
...(config.opacity === undefined && !layer.encoding.opacity ? {} : {
|
|
93
|
+
opacity: mappedAppearance(next, layer, "opacity", group.members, config.opacity ?? 1) })
|
|
94
|
+
} };
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
next = next.editGraphics({ target: id, property: "items", value: entries });
|
|
98
|
+
} else {
|
|
99
|
+
const items = series.flatMap(group => group.points.map(point => ({
|
|
100
|
+
...createPointShapeGraphic({
|
|
101
|
+
shape: mappedAppearance(next, layer, "shape", [point.fields], config.shape ?? "circle"),
|
|
102
|
+
x: point.x, y: point.y,
|
|
103
|
+
angle: resolveDirectionValues([point.fields], layer.encoding.angle)?.[0],
|
|
104
|
+
area: mappedAppearance(next, layer, "size", [point.fields], Math.PI * (config.radius ?? 3) ** 2),
|
|
105
|
+
fill: color([point.fields], config.fill ?? "#4c78a8"),
|
|
106
|
+
stroke: mappedAppearance(next, layer, "stroke", [point.fields], config.stroke === false ? "transparent" : config.stroke),
|
|
107
|
+
strokeWidth: mappedAppearance(next, layer, "strokeWidth", [point.fields], config.stroke === false ? 0 : config.strokeWidth),
|
|
108
|
+
opacity: mappedAppearance(next, layer, "opacity", [point.fields], config.opacity),
|
|
109
|
+
...requestedStrokeDetails(config, "Overlay point")
|
|
110
|
+
})
|
|
111
|
+
})));
|
|
112
|
+
next = next.editGraphics({ target: id, property: "items", value: items });
|
|
113
|
+
}
|
|
114
|
+
return editMarkGraphic(next, id, {});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const createOverlay = /* @__PURE__ */ closedAction({
|
|
118
|
+
op: "createOverlay", description: "Add a line or points that follow another mark's visible results."
|
|
119
|
+
}, ["id", "source", "type", "basis", "series", "missing", "orderBy", "line", "point"], function (args = {}) {
|
|
120
|
+
if (!["line", "point"].includes(args.type ?? "line")) throw new Error("Overlay type must be line or point.");
|
|
121
|
+
const type = args.type ?? "line";
|
|
122
|
+
if (args[type] !== undefined) validateOptionObject(args[type], ["clip", "opacity", "stroke", "strokeWidth", ...STROKE_STYLE_PROPERTIES,
|
|
123
|
+
...(type === "line" ? ["curve", "tension", "closed"] : ["shape", "fill"])], `createOverlay ${type}`);
|
|
124
|
+
if (type === "line" && args.point !== undefined || type === "point" && args.line !== undefined) {
|
|
125
|
+
throw new Error("Overlay appearance must match its type.");
|
|
126
|
+
}
|
|
127
|
+
const source = resolveEligibleLayer(this, { target: args.source,
|
|
128
|
+
predicate: layer => ["bar", "line", "area", "point"].includes(layer.mark?.type) && !layer.derivedBindings?.overlay,
|
|
129
|
+
label: "overlay source", targetOption: "source" });
|
|
130
|
+
const binding = normalizeOverlayBinding({ source: source.id, basis: args.basis ?? "visibleItems", mode: "follow",
|
|
131
|
+
series: args.series ?? "source", missing: args.missing ?? "break",
|
|
132
|
+
inheritColor: args[type]?.[type === "line" ? "stroke" : "fill"] === undefined,
|
|
133
|
+
...(args.orderBy === undefined ? type === "point" && source.mark.type === "point" ? { orderBy: "x" } : {} : { orderBy: args.orderBy }) });
|
|
134
|
+
resolveOverlaySeries(this, binding);
|
|
135
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "overlay", operation: "createOverlay" });
|
|
136
|
+
let next = this[type === "line" ? "createLineMark" : "createPointMark"]({
|
|
137
|
+
...(args[type] ?? {}), id, data: source.data
|
|
138
|
+
});
|
|
139
|
+
next = positionBindings(next, id, source).editSemantic({
|
|
140
|
+
property: `layer[${id}].derivedBindings.overlay`, value: binding
|
|
141
|
+
});
|
|
142
|
+
return next.rematerializeOverlay({ id });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const editOverlay = /* @__PURE__ */ closedAction({
|
|
146
|
+
op: "editOverlay", description: "Edit the source, series, ordering, or gaps of a linked overlay."
|
|
147
|
+
}, ["target", "source", "series", "missing", "orderBy", "follow"], function (args = {}) {
|
|
148
|
+
const layer = resolveEligibleLayer(this, { target: args.target,
|
|
149
|
+
predicate: layer => Boolean(layer.derivedBindings?.overlay), label: "overlay" });
|
|
150
|
+
if (!Object.keys(args).some(key => key !== "target")) throw new Error("editOverlay requires an edit.");
|
|
151
|
+
if (args.follow !== undefined && typeof args.follow !== "boolean") throw new TypeError("Overlay follow must be boolean.");
|
|
152
|
+
if (args.follow === false) {
|
|
153
|
+
if (Object.keys(args).some(key => !["target", "follow"].includes(key))) throw new Error("Detach cannot be combined with source edits.");
|
|
154
|
+
return detachOverlay(this, layer);
|
|
155
|
+
}
|
|
156
|
+
const { target, follow, ...patch } = args;
|
|
157
|
+
const binding = normalizeOverlayBinding({ ...layer.derivedBindings.overlay, ...patch });
|
|
158
|
+
if (binding.source === layer.id) throw new Error("An overlay cannot follow itself.");
|
|
159
|
+
resolveOverlaySeries(this, binding);
|
|
160
|
+
return this.editSemantic({ property: `layer[${layer.id}].derivedBindings.overlay`, value: binding })
|
|
161
|
+
.rematerializeOverlay({ id: layer.id });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export function registerOverlayActions(ProgramClass) {
|
|
165
|
+
ProgramClass.prototype.createOverlay = createOverlay;
|
|
166
|
+
ProgramClass.prototype.editOverlay = editOverlay;
|
|
167
|
+
ProgramClass.prototype.rematerializeOverlay = rematerializeOverlay;
|
|
168
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { requireSemanticScale } from "../../selectors/scales.js";
|
|
2
|
+
import { resolveOverlaySeries, overlaySegments } from "../../materialization/overlayItems.js";
|
|
3
|
+
import { resolveCoordinateBounds } from "../../materialization/coordinateBounds.js";
|
|
4
|
+
import { requireLayer } from "../../selectors/layers.js";
|
|
5
|
+
|
|
6
|
+
function availableName(base, taken) {
|
|
7
|
+
let name = base, index = 2;
|
|
8
|
+
while (taken.has(name)) name = `${base}-${index++}`;
|
|
9
|
+
taken.add(name);
|
|
10
|
+
return name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Detaching explicitly freezes the current visible results. Normalized plot
|
|
14
|
+
// coordinates retain offsets/dodging exactly and still respond to Canvas resize.
|
|
15
|
+
export function detachOverlay(program, layer) {
|
|
16
|
+
const binding = layer.derivedBindings.overlay;
|
|
17
|
+
const groups = resolveOverlaySeries(program, binding);
|
|
18
|
+
const frame = resolveCoordinateBounds(program, layer.coordinate);
|
|
19
|
+
const fields = new Set(groups.flatMap(group => group.points.flatMap(point => Object.keys(point.fields))));
|
|
20
|
+
const x = availableName("__overlay_x", fields), y = availableName("__overlay_y", fields);
|
|
21
|
+
const series = availableName("__overlay_series", fields);
|
|
22
|
+
const values = groups.flatMap(group => overlaySegments(group, binding).flatMap((points, segment) =>
|
|
23
|
+
points.map(point => ({ ...point.fields, [x]: (point.x - frame.x) / frame.width,
|
|
24
|
+
[y]: 1 - (point.y - frame.y) / frame.height, [series]: JSON.stringify([group.key, segment]) }))));
|
|
25
|
+
const data = availableName(`${layer.id}-snapshot`, new Set(program.semanticSpec.datasets.map(dataset => dataset.id)));
|
|
26
|
+
const scales = new Set(program.semanticSpec.scales.map(scale => scale.id));
|
|
27
|
+
let next = program.createData({ id: data, values })
|
|
28
|
+
.editSemantic({ property: `layer[${layer.id}].derivedBindings.overlay`, remove: true })
|
|
29
|
+
.editSemantic({ property: `layer[${layer.id}].data`, value: data });
|
|
30
|
+
for (const channel of ["x", "y", "group"]) {
|
|
31
|
+
if (requireLayer(next, layer.id).encoding[channel]) next = next.editSemantic({ property: `layer[${layer.id}].encoding.${channel}`, remove: true });
|
|
32
|
+
}
|
|
33
|
+
// Appearance shares no mutable scale identity with the former source.
|
|
34
|
+
for (const [channel, encoding] of Object.entries(layer.encoding)) {
|
|
35
|
+
if (["x", "y"].includes(channel) || !encoding.scale) continue;
|
|
36
|
+
const definition = requireSemanticScale(program, encoding.scale);
|
|
37
|
+
const id = availableName(`${layer.id}-${channel}-snapshot`, scales);
|
|
38
|
+
next = next.createScale({ ...definition, id })
|
|
39
|
+
.editSemantic({ property: `layer[${layer.id}].encoding.${channel}.scale`, value: id });
|
|
40
|
+
}
|
|
41
|
+
return next.encodeChannels({ target: layer.id, channels: {
|
|
42
|
+
x: { field: x, fieldType: "quantitative", scale: { id: availableName(`${layer.id}-snapshot-x`, scales), type: "linear", domain: [0, 1], zero: false, nice: false } },
|
|
43
|
+
y: { field: y, fieldType: "quantitative", scale: { id: availableName(`${layer.id}-snapshot-y`, scales), type: "linear", domain: [0, 1], zero: false, nice: false } },
|
|
44
|
+
...(layer.mark.type === "line" ? { group: { fields: [series] } } : {}) } });
|
|
45
|
+
}
|
|
@@ -13,7 +13,6 @@ import { validateConcreteGraphicValue } from
|
|
|
13
13
|
"../../grammar/schemas/concreteGraphic.js";
|
|
14
14
|
import {
|
|
15
15
|
collectGraphicSubtreeIds,
|
|
16
|
-
findGraphicParent,
|
|
17
16
|
requireGraphic,
|
|
18
17
|
resolveGraphicChildOrder
|
|
19
18
|
} from "../../grammar/schemas/graphicTree.js";
|
|
@@ -145,28 +144,25 @@ function editItems(owner, property, value, id, program) {
|
|
|
145
144
|
return changed ? freezeOwned({ ...owner, items: freezeOwned(items) }) : owner;
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
function removeGraphicTrees(graphicSpec, targets) {
|
|
148
|
+
const removedIds = new Set();
|
|
149
|
+
for (const id of targets) {
|
|
150
|
+
const found = requireGraphic(graphicSpec, id);
|
|
151
|
+
if (found.kind === "item") throw new Error("editGraphics cannot remove a generated item independently.");
|
|
152
|
+
if (found.object.type === "canvas" && graphicSpec.order.includes(id)) throw new Error("The canvas root cannot be removed.");
|
|
153
|
+
for (const child of collectGraphicSubtreeIds(graphicSpec, id)) removedIds.add(child);
|
|
155
154
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
...
|
|
164
|
-
|
|
165
|
-
});
|
|
166
|
-
} else {
|
|
167
|
-
order = freezeOwned(order.filter(objectId => objectId !== id));
|
|
155
|
+
// One immutable replacement for the complete removal set. Repeated per-root
|
|
156
|
+
// copying made facet guide cleanup quadratic in the number of panels.
|
|
157
|
+
const objects = {};
|
|
158
|
+
for (const id in graphicSpec.objects) {
|
|
159
|
+
if (removedIds.has(id)) continue;
|
|
160
|
+
const object = graphicSpec.objects[id];
|
|
161
|
+
const retained = object.children?.some(child => removedIds.has(child))
|
|
162
|
+
? freezeOwned({ ...object, children: freezeOwned(object.children.filter(child => !removedIds.has(child))) }) : object;
|
|
163
|
+
Object.defineProperty(objects, id, { value: retained, enumerable: true, writable: true, configurable: true });
|
|
168
164
|
}
|
|
169
|
-
return freezeOwned({ objects: freezeOwned(objects), order });
|
|
165
|
+
return freezeOwned({ objects: freezeOwned(objects), order: freezeOwned(graphicSpec.order.filter(id => !removedIds.has(id))) });
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
const editGraphics = /* @__PURE__ */ closedAction(
|
|
@@ -177,7 +173,8 @@ const editGraphics = /* @__PURE__ */ closedAction(
|
|
|
177
173
|
}, ["target", "property", "value", "remove"],
|
|
178
174
|
function (args = {}) {
|
|
179
175
|
const { target, property, value, remove = false } = args;
|
|
180
|
-
|
|
176
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
177
|
+
if (!targets.length || targets.some(id => typeof id !== "string" || id.length === 0) || Array.isArray(target) && !remove) {
|
|
181
178
|
throw new TypeError("editGraphics requires a non-empty target string.");
|
|
182
179
|
}
|
|
183
180
|
if (typeof remove !== "boolean") throw new TypeError("editGraphics remove must be a boolean.");
|
|
@@ -186,13 +183,10 @@ const editGraphics = /* @__PURE__ */ closedAction(
|
|
|
186
183
|
}
|
|
187
184
|
if (!remove && value === undefined) throw new TypeError("editGraphics requires a value.");
|
|
188
185
|
|
|
189
|
-
const found = requireGraphic(this.graphicSpec, target);
|
|
190
186
|
if (remove) {
|
|
191
|
-
|
|
192
|
-
throw new Error("editGraphics cannot remove a generated item independently.");
|
|
193
|
-
}
|
|
194
|
-
return this._clone({ graphicSpec: removeGraphicTree(this.graphicSpec, found.id) });
|
|
187
|
+
return this._clone({ graphicSpec: removeGraphicTrees(this.graphicSpec, targets) });
|
|
195
188
|
}
|
|
189
|
+
const found = requireGraphic(this.graphicSpec, target);
|
|
196
190
|
|
|
197
191
|
if (property === "children") {
|
|
198
192
|
const children = freezeOwned(resolveGraphicChildOrder(this.graphicSpec, target, value));
|
|
@@ -232,7 +232,7 @@ export function withoutPreviewLayerEncodings(program, { id, channels }) {
|
|
|
232
232
|
return program._clone({ semanticSpec });
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// Composition materialization may retain this native
|
|
235
|
+
// Composition materialization may retain this native combined guide meaning.
|
|
236
236
|
// Use the existing semantic primitives on a private unit adapter; this does not
|
|
237
237
|
// relax the public composition guard or accept arbitrary semantic paths.
|
|
238
238
|
export function synchronizeFacetColorSizeMeaning(program, meaning) {
|
|
@@ -240,7 +240,7 @@ export function synchronizeFacetColorSizeMeaning(program, meaning) {
|
|
|
240
240
|
throw new Error("Color-size parent meaning requires a facet composition.");
|
|
241
241
|
}
|
|
242
242
|
if (meaning !== undefined && (!isPlainObject(meaning) ||
|
|
243
|
-
Object.keys(meaning).some(key => !["colorScale", "sizeScale", "title"].includes(key)))) {
|
|
243
|
+
Object.keys(meaning).some(key => !["colorScale", "sizeScale", "shapeScale", "colorChannel", "title"].includes(key)))) {
|
|
244
244
|
throw new Error("Color-size parent meaning requires its native scale bindings and title.");
|
|
245
245
|
}
|
|
246
246
|
let unit = new program.constructor({ semanticSpec: program.semanticSpec, graphicSpec: program.graphicSpec,
|
|
@@ -281,11 +281,10 @@ export function createSemanticPrimitiveAction(validateSemanticValue, planNamedRe
|
|
|
281
281
|
const removesNamedResource = remove && parsed.path.length === 0 &&
|
|
282
282
|
["dataset", "scale", "coordinate"].includes(parsed.kind);
|
|
283
283
|
if (this.compositionSpec !== undefined && !(
|
|
284
|
-
this.compositionSpec.type === "facet" &&
|
|
285
|
-
(parsed.kind === "title" || removesNamedResource)
|
|
284
|
+
parsed.kind === "title" || (this.compositionSpec.type === "facet" && removesNamedResource)
|
|
286
285
|
)) {
|
|
287
286
|
throw new Error(
|
|
288
|
-
"editSemantic on a composition parent supports only
|
|
287
|
+
"editSemantic on a composition parent supports only title state and unreferenced named resource removal."
|
|
289
288
|
);
|
|
290
289
|
}
|
|
291
290
|
if (remove) {
|
|
@@ -29,6 +29,7 @@ export function validateGuideSemanticValue(_program, parsed, value) {
|
|
|
29
29
|
const property = parsed.path.at(-1);
|
|
30
30
|
if (parsed.id === "legend.colorSize") {
|
|
31
31
|
if(property === "title") validateNonEmptySemanticString(value,"Legend title");
|
|
32
|
+
else if(property === "colorChannel") { if(!["color","stroke"].includes(value))throw new Error("Combined legend colorChannel requires color or stroke."); }
|
|
32
33
|
else validateUserId(value,"Legend scale id");
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
@@ -23,6 +23,7 @@ import { ITEM_MISSING_MARK_TYPES, validateItemMissing } from
|
|
|
23
23
|
"../../../grammar/itemMissing.js";
|
|
24
24
|
import { isIndependentText, validateTextOffsetEncoding } from "../../../grammar/text.js";
|
|
25
25
|
import { validateLineMissing, validateLineMissingScope } from "../../../grammar/lineMissing.js";
|
|
26
|
+
import { normalizeOverlayBinding } from "../../../grammar/overlay.js";
|
|
26
27
|
|
|
27
28
|
function validateLayerSource(program, parsed, value, sourceMarkTypes) {
|
|
28
29
|
validateUserId(value, "Layer source id");
|
|
@@ -53,6 +54,12 @@ export function validateLayerSemanticValue(
|
|
|
53
54
|
} = {}
|
|
54
55
|
) {
|
|
55
56
|
const property = parsed.path.join(".");
|
|
57
|
+
if (property === "derivedBindings.overlay") {
|
|
58
|
+
const binding = normalizeOverlayBinding(value);
|
|
59
|
+
validateLayerSource(program, parsed, binding.source, ["bar", "line", "area", "point"]);
|
|
60
|
+
if (findLayer(program, binding.source)?.derivedBindings?.overlay) throw new Error("Overlay source must be independent.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
56
63
|
if (/^encoding\.(dx|dy)(\.|$)/.test(property)) {
|
|
57
64
|
if (!isIndependentText(findLayer(program, parsed.id))) {
|
|
58
65
|
throw new Error("Numeric text offsets require independent Text marks.");
|
|
@@ -9,6 +9,8 @@ import { findDataset } from "../../../selectors/datasets.js";
|
|
|
9
9
|
import { requireSemanticScale } from "../../../selectors/scales.js";
|
|
10
10
|
import { SCALED_ENCODING_CHANNELS } from "../../../core/vocabulary.js";
|
|
11
11
|
import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
|
|
12
|
+
import { resolveOverlaySeries } from "../../../materialization/overlayItems.js";
|
|
13
|
+
import { uniqueFields } from "../../../materialization/selection/items/common.js";
|
|
12
14
|
|
|
13
15
|
export function findScale(program, id) {
|
|
14
16
|
return requireSemanticScale(program, id);
|
|
@@ -19,6 +21,8 @@ export function findScaleConsumers(program, id) {
|
|
|
19
21
|
for (const layer of program.semanticSpec.layers) {
|
|
20
22
|
if (isSourceOwnedText(layer)) continue;
|
|
21
23
|
for (const channel of SCALED_ENCODING_CHANNELS) {
|
|
24
|
+
if (layer.derivedBindings?.overlay && ["x", "y", "x2", "y2", "xOffset", "yOffset"].includes(channel)) continue;
|
|
25
|
+
if (layer.derivedBindings?.overlay?.inheritColor && channel === "color") continue;
|
|
22
26
|
const encoding = layer.encoding?.[channel];
|
|
23
27
|
if (encoding?.scale === id) consumers.push({ layer, channel, encoding });
|
|
24
28
|
}
|
|
@@ -59,6 +63,12 @@ export function readConsumerFieldValues(
|
|
|
59
63
|
scale = findScale(program, consumer.encoding.scale)
|
|
60
64
|
) {
|
|
61
65
|
const { field, fieldType, temporalUnit } = consumer.encoding;
|
|
66
|
+
if (consumer.layer.derivedBindings?.overlay) {
|
|
67
|
+
const groups = resolveOverlaySeries(program, consumer.layer.derivedBindings.overlay, { geometry: false });
|
|
68
|
+
const rows = consumer.layer.mark.type === "point" ? groups.flatMap(group => group.points.map(point => point.fields))
|
|
69
|
+
: groups.map(group => uniqueFields(group.points.map(point => point.fields)));
|
|
70
|
+
return readScaleField(rows, field, fieldType, { allowUnknown: Object.hasOwn(scale, "unknown"), temporalUnit });
|
|
71
|
+
}
|
|
62
72
|
if (consumer.layer.mark.type === "line" && consumer.layer.mark.missing === "break" &&
|
|
63
73
|
["x", "y"].includes(consumer.channel)) {
|
|
64
74
|
return readLinePosition(dataset.values, consumer.encoding).filter(value => value !== null);
|
|
@@ -60,7 +60,7 @@ export function resolveMarkFamilyConsumerValues(program, consumer, dataset) {
|
|
|
60
60
|
if (
|
|
61
61
|
layer.mark?.type === "line" &&
|
|
62
62
|
((["x", "y"].includes(channel) && encoding.x !== undefined &&
|
|
63
|
-
isAggregate(encoding.y?.aggregate) &&
|
|
63
|
+
(isAggregate(encoding.y?.aggregate) || isAggregate(encoding.x?.aggregate)) &&
|
|
64
64
|
!(channel === "x" &&
|
|
65
65
|
encoding.x?.bin !== undefined)) ||
|
|
66
66
|
appearance) &&
|
|
@@ -63,7 +63,7 @@ function resolveScalePreviewAtBounds(program, id, bounds) {
|
|
|
63
63
|
const polarFrame = channel === "radius"
|
|
64
64
|
? resolveSharedRadiusFrame(program, consumers, bounds)
|
|
65
65
|
: undefined;
|
|
66
|
-
const resolvedScale = resolveScaleMaterialization({ id, scale, channel, consumers, valuesByConsumer,
|
|
66
|
+
const resolvedScale = resolveScaleMaterialization({ program, id, scale, channel, consumers, valuesByConsumer,
|
|
67
67
|
bounds: ["color", "stroke", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"].includes(channel)
|
|
68
68
|
? undefined : bounds,
|
|
69
69
|
resolvedScales: program.resolvedScales, markConfigs: program.markConfigs, polarFrame,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { highlightTargets } from "../../selectors/selectionTargets.js";
|
|
1
2
|
import { action, closedAction } from "../../core/action.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import { isPlainObject } from "../../core/immutable.js";
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
} from "../theme/state.js";
|
|
31
32
|
|
|
32
33
|
const SELECTOR_KEYS = Object.freeze([
|
|
33
|
-
"grain", "field", "channel", "property", "op", "value", "values", "min", "max",
|
|
34
|
+
"rows", "grain", "field", "channel", "property", "op", "value", "values", "min", "max",
|
|
34
35
|
"inclusive", "count", "groupBy", "ties"
|
|
35
36
|
]);
|
|
36
37
|
const SELECT_OPTIONS = Object.freeze(["id", "target", ...SELECTOR_KEYS]);
|
|
@@ -39,7 +40,7 @@ const REMOVE_SELECTION_OPTIONS = Object.freeze(["selection"]);
|
|
|
39
40
|
const HIGHLIGHT_OPTIONS = Object.freeze([
|
|
40
41
|
"id", "target", "select", "selection", "color", "opacity", "fill",
|
|
41
42
|
"stroke", "strokeWidth", "strokeDash", "shape", "size", "offset",
|
|
42
|
-
"dimOthers", "bringToFront"
|
|
43
|
+
"dimOthers", "bringToFront", "fontSize"
|
|
43
44
|
]);
|
|
44
45
|
const INTERNAL_SELECTION_OPTIONS = Object.freeze(["selection", "style", "keys"]);
|
|
45
46
|
const INTERNAL_DIM_OPTIONS = Object.freeze(["selection", "opacity", "keys"]);
|
|
@@ -59,7 +60,7 @@ function resolveTarget(program, target, label = "mark selection") {
|
|
|
59
60
|
return resolveEligibleLayer(program, {
|
|
60
61
|
target: requested,
|
|
61
62
|
label,
|
|
62
|
-
predicate: layer => findSelectionPolicy(layer.mark?.type) !== undefined
|
|
63
|
+
predicate: layer => findSelectionPolicy(layer.mark?.type) !== undefined && (requested !== undefined || !isSourceOwnedText(layer))
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -321,7 +322,7 @@ export const removeMarkHighlight = /* @__PURE__ */ closedAction(
|
|
|
321
322
|
const current = resolveStoredSelection(this, args.selection);
|
|
322
323
|
const dependent = Object.entries(
|
|
323
324
|
this.materializationConfigs.highlights ?? {}
|
|
324
|
-
).filter(([, config]) => config.selection === current.id);
|
|
325
|
+
).filter(([, config]) => config.selection === current.id || config.groupSelection === current.id);
|
|
325
326
|
if (dependent.length === 0) {
|
|
326
327
|
throw new Error(
|
|
327
328
|
`Selection "${current.id}" has no highlight assignment.`
|
|
@@ -331,7 +332,8 @@ export const removeMarkHighlight = /* @__PURE__ */ closedAction(
|
|
|
331
332
|
for (const [id] of dependent) {
|
|
332
333
|
next = next._withoutMaterializationConfig(["highlights", id]);
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
+
for (const target of new Set(dependent.map(([, config]) => config.target))) next = rebuildTargetHighlights(next, target);
|
|
336
|
+
return next;
|
|
335
337
|
}
|
|
336
338
|
);
|
|
337
339
|
|
|
@@ -342,7 +344,9 @@ export const removeMarkSelection = /* @__PURE__ */ closedAction(
|
|
|
342
344
|
}, REMOVE_SELECTION_OPTIONS,
|
|
343
345
|
function (args = {}) {
|
|
344
346
|
const current = resolveStoredSelection(this, args.selection);
|
|
345
|
-
const
|
|
347
|
+
const componentDependents = Object.entries(this.materializationConfigs.selections ?? {}).filter(([, definition]) => definition.sourceSelection === current.id);
|
|
348
|
+
const labelDependents = [current.id, ...componentDependents.map(([id]) => id)]
|
|
349
|
+
.flatMap(id => namedLabelDependents(this, id));
|
|
346
350
|
if (labelDependents.length > 0) {
|
|
347
351
|
throw new Error(
|
|
348
352
|
`Selection "${current.id}" is referenced by attached labels: ${labelDependents.join(", ")}.`
|
|
@@ -354,6 +358,7 @@ export const removeMarkSelection = /* @__PURE__ */ closedAction(
|
|
|
354
358
|
let next = hasDependentHighlight
|
|
355
359
|
? this.removeMarkHighlight({ selection: current.id })
|
|
356
360
|
: this;
|
|
361
|
+
for (const [id] of componentDependents) next = next._withoutMaterializationConfig(["selections", id]);
|
|
357
362
|
next = next._withoutMaterializationConfig(["selections", current.id]);
|
|
358
363
|
return this.context.currentSelection === current.id
|
|
359
364
|
? next._withContext({ currentSelection: undefined })
|
|
@@ -540,9 +545,7 @@ export const rematerializeMarkHighlights = /* @__PURE__ */ closedAction(
|
|
|
540
545
|
}
|
|
541
546
|
);
|
|
542
547
|
|
|
543
|
-
|
|
544
|
-
{ op: "highlightMarks", description: "Select and emphasize final visual mark items." }, HIGHLIGHT_OPTIONS,
|
|
545
|
-
function (args = {}) {
|
|
548
|
+
function highlightSingle(args = {}) {
|
|
546
549
|
if (args.select !== undefined && args.selection !== undefined) {
|
|
547
550
|
throw new Error("highlightMarks accepts select or selection, not both.");
|
|
548
551
|
}
|
|
@@ -564,15 +567,20 @@ export const highlightMarks = /* @__PURE__ */ closedAction(
|
|
|
564
567
|
if (args.select !== undefined) {
|
|
565
568
|
layer = resolveTarget(this, args.target, "highlight mark");
|
|
566
569
|
resolveMarkSelection(this, layer.id, args.select);
|
|
570
|
+
if (args.fontSize !== undefined && layer.mark.type !== "text") throw new Error("fontSize highlight requires a text mark.");
|
|
567
571
|
const style = requireSelectionPolicy(
|
|
568
572
|
layer.mark.type
|
|
569
573
|
).normalizeHighlightStyle(args);
|
|
570
|
-
|
|
574
|
+
if (args.id !== undefined && next.materializationConfigs.selections?.[args.id]) {
|
|
575
|
+
if (next.materializationConfigs.selections[args.id].target !== layer.id) throw new Error("Highlight ID belongs to a different target.");
|
|
576
|
+
if (next.materializationConfigs.highlights?.[args.id]) next = next.removeMarkHighlight({ selection: args.id });
|
|
577
|
+
next = next.editMarkSelection({ selection: args.id, ...args.select });
|
|
578
|
+
} else next = next.selectMarks({
|
|
571
579
|
...(args.id === undefined ? {} : { id: args.id }),
|
|
572
580
|
target: layer.id,
|
|
573
581
|
...args.select
|
|
574
582
|
});
|
|
575
|
-
resolved = resolveStoredSelection(next);
|
|
583
|
+
resolved = resolveStoredSelection(next, args.id);
|
|
576
584
|
resolved = { ...resolved, style };
|
|
577
585
|
} else {
|
|
578
586
|
resolved = resolveStoredSelection(next, args.selection);
|
|
@@ -580,6 +588,7 @@ export const highlightMarks = /* @__PURE__ */ closedAction(
|
|
|
580
588
|
throw new Error("highlightMarks target must match its selection target.");
|
|
581
589
|
}
|
|
582
590
|
layer = resolveTarget(next, resolved.definition.target, "highlight mark");
|
|
591
|
+
if (args.fontSize !== undefined && layer.mark.type !== "text") throw new Error("fontSize highlight requires a text mark.");
|
|
583
592
|
resolved = {
|
|
584
593
|
...resolved,
|
|
585
594
|
style: requireSelectionPolicy(
|
|
@@ -611,4 +620,47 @@ export const highlightMarks = /* @__PURE__ */ closedAction(
|
|
|
611
620
|
? next.rematerializeLegendHighlights()
|
|
612
621
|
: next;
|
|
613
622
|
}
|
|
623
|
+
|
|
624
|
+
export const highlightMarks = /* @__PURE__ */ closedAction(
|
|
625
|
+
{ op: "highlightMarks", description: "Select and emphasize final visual mark items." }, HIGHLIGHT_OPTIONS,
|
|
626
|
+
function (args = {}) {
|
|
627
|
+
if (args.select === undefined && args.id === undefined) {
|
|
628
|
+
const named = resolveStoredSelection(this, args.selection);
|
|
629
|
+
if (highlightTargets(this, named.definition.target).length > 1) {
|
|
630
|
+
if (args.target !== undefined && args.target !== named.definition.target) throw new Error("highlightMarks target must match its selection target.");
|
|
631
|
+
args = { ...args, id: named.id, target: named.definition.target, select: named.selector };
|
|
632
|
+
delete args.selection;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const targets = args.target === undefined ? [] : highlightTargets(this, args.target);
|
|
636
|
+
if (targets.length <= 1 || args.select === undefined) return highlightSingle.call(this, { ...args, ...(targets[0] ? { target: targets[0] } : {}) });
|
|
637
|
+
const primary = resolveMarkSelection(this, targets[0], args.select);
|
|
638
|
+
const selected = new Set(primary.keys);
|
|
639
|
+
const rows = primary.items.filter(item => selected.has(item.key)).flatMap(item => item.rows);
|
|
640
|
+
// The oldest explicitly recorded dataset is common to all dependent marks.
|
|
641
|
+
const datasets = this.semanticSpec.datasets;
|
|
642
|
+
const source = datasets.find(dataset => primary.items.some(item => item.rows.some(ref => ref.data === dataset.id)));
|
|
643
|
+
const select = source ? { rows: { data: source.id, indices: [...new Set(rows.filter(ref => ref.data === source.id).map(ref => ref.index))] } } : args.select;
|
|
644
|
+
const baseId = args.id ?? resolveSelectionCreationId(this, undefined, targets[0]);
|
|
645
|
+
let next = this;
|
|
646
|
+
for (const target of targets) {
|
|
647
|
+
const local = { ...args, id: target === targets[0] ? baseId : `${baseId}-${target}`, target,
|
|
648
|
+
select: target === targets[0] ? args.select : select };
|
|
649
|
+
if (target !== targets[0] && args.select.field && !['min', 'max'].includes(args.select.op)) {
|
|
650
|
+
try { resolveMarkSelection(this, target, args.select); local.select = args.select; } catch { /* Keep the owner's source-row selection for mixed-grain components. */ }
|
|
651
|
+
}
|
|
652
|
+
// Common color/opacity propagate; shape and area are component-specific.
|
|
653
|
+
const type = findLayer(this, target).mark.type;
|
|
654
|
+
if (type !== 'point') { delete local.size; delete local.shape; }
|
|
655
|
+
if (!['line', 'rule', 'tick'].includes(type)) delete local.strokeDash;
|
|
656
|
+
if (type === 'bar') delete local.offset;
|
|
657
|
+
if (target !== targets[0] && ['line', 'rule', 'tick'].includes(type) && local.fill) { local.color = local.fill; delete local.fill; }
|
|
658
|
+
next = highlightSingle.call(next, local);
|
|
659
|
+
if (target !== targets[0] && source) {
|
|
660
|
+
next = next._withSelectionConfig(local.id, { ...next.materializationConfigs.selections[local.id], sourceSelection: baseId });
|
|
661
|
+
}
|
|
662
|
+
next = next._withHighlightConfig(local.id, { ...next.materializationConfigs.highlights[local.id], groupSelection: baseId });
|
|
663
|
+
}
|
|
664
|
+
return next._withContext({ currentSelection: baseId });
|
|
665
|
+
}
|
|
614
666
|
);
|
|
@@ -13,14 +13,6 @@ import {
|
|
|
13
13
|
resolveCanvasGraphicPlacement
|
|
14
14
|
} from "../../materialization/graphicHierarchy.js";
|
|
15
15
|
|
|
16
|
-
function assertTitleScope(program, operation) {
|
|
17
|
-
if (
|
|
18
|
-
program.compositionSpec !== undefined &&
|
|
19
|
-
program.compositionSpec.type !== "facet"
|
|
20
|
-
) {
|
|
21
|
-
throw new Error(`${operation} is not available on this composition ChartProgram.`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
16
|
|
|
25
17
|
function hasRotation(graphic) {
|
|
26
18
|
return (graphic.items ?? [graphic]).some(
|
|
@@ -136,7 +128,6 @@ export const createTitle = /* @__PURE__ */ action(
|
|
|
136
128
|
scope: "any"
|
|
137
129
|
},
|
|
138
130
|
withGuideLayoutValidation(function (args = {}) {
|
|
139
|
-
assertTitleScope(this, "createTitle");
|
|
140
131
|
const options = normalizeTitleOptions(args);
|
|
141
132
|
if (Object.keys(this.semanticSpec.title).length > 0) {
|
|
142
133
|
throw new Error("createTitle requires missing semantic title state.");
|
|
@@ -153,7 +144,7 @@ export const createTitle = /* @__PURE__ */ action(
|
|
|
153
144
|
next = next.editSemantic({ property: "title.subtitle", value: subtitle });
|
|
154
145
|
}
|
|
155
146
|
next = next._withTitleConfig(config);
|
|
156
|
-
if (next.compositionSpec
|
|
147
|
+
if (next.compositionSpec !== undefined) {
|
|
157
148
|
return next.materializeComposition();
|
|
158
149
|
}
|
|
159
150
|
resolveTitleLayout(next, config);
|
|
@@ -170,7 +161,6 @@ export const editTitle = /* @__PURE__ */ action(
|
|
|
170
161
|
scope: "any"
|
|
171
162
|
},
|
|
172
163
|
withGuideLayoutValidation(function (args = {}) {
|
|
173
|
-
assertTitleScope(this, "editTitle");
|
|
174
164
|
if (this.semanticSpec.title.text === undefined) {
|
|
175
165
|
throw new Error("editTitle requires an existing chart title.");
|
|
176
166
|
}
|
|
@@ -193,7 +183,7 @@ export const editTitle = /* @__PURE__ */ action(
|
|
|
193
183
|
});
|
|
194
184
|
}
|
|
195
185
|
next = next._withTitleConfig(normalized.config);
|
|
196
|
-
if (next.compositionSpec
|
|
186
|
+
if (next.compositionSpec !== undefined) {
|
|
197
187
|
return next.materializeComposition();
|
|
198
188
|
}
|
|
199
189
|
resolveTitleLayout(next, normalized.config);
|
|
@@ -208,7 +198,6 @@ export const removeTitle = /* @__PURE__ */ action(
|
|
|
208
198
|
scope: "any"
|
|
209
199
|
},
|
|
210
200
|
withGuideLayoutValidation(function (args = {}) {
|
|
211
|
-
assertTitleScope(this, "removeTitle");
|
|
212
201
|
noOptions(args, "removeTitle");
|
|
213
202
|
if (
|
|
214
203
|
this.semanticSpec.title.text === undefined &&
|
|
@@ -231,7 +220,7 @@ export const removeTitle = /* @__PURE__ */ action(
|
|
|
231
220
|
}
|
|
232
221
|
}
|
|
233
222
|
next = next._withoutMaterializationConfig(["title"]);
|
|
234
|
-
return next.compositionSpec
|
|
223
|
+
return next.compositionSpec !== undefined
|
|
235
224
|
? next.materializeComposition()
|
|
236
225
|
: next;
|
|
237
226
|
})
|