ggaction 0.0.24 → 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.
Files changed (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -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 removeGraphicTree(graphicSpec, id) {
149
- const parent = findGraphicParent(graphicSpec, id);
150
- if (
151
- graphicSpec.objects[id].type === "canvas" &&
152
- parent?.kind !== "parent"
153
- ) {
154
- throw new Error("The canvas root cannot be removed.");
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
- const removedIds = new Set(collectGraphicSubtreeIds(graphicSpec, id));
157
- const objects = Object.fromEntries(
158
- Object.entries(graphicSpec.objects).filter(([objectId]) => !removedIds.has(objectId))
159
- );
160
- let order = graphicSpec.order;
161
- if (parent?.kind === "parent") {
162
- objects[parent.id] = freezeOwned({
163
- ...objects[parent.id],
164
- children: freezeOwned(parent.object.children.filter(childId => childId !== id))
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
- if (typeof target !== "string" || target.length === 0) {
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
- if (found.kind === "item") {
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 two-scale guide meaning.
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 facet title state and unreferenced named resource removal."
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
- return rebuildTargetHighlights(next, current.definition.target);
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 labelDependents = namedLabelDependents(this, current.id);
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
- export const highlightMarks = /* @__PURE__ */ closedAction(
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
- next = next.selectMarks({
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?.type === "facet") {
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?.type === "facet") {
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?.type === "facet"
223
+ return next.compositionSpec !== undefined
235
224
  ? next.materializeComposition()
236
225
  : next;
237
226
  })