ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  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/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -2,7 +2,9 @@ import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { isPlainObject } from "../../core/immutable.js";
4
4
  import { validateKeys } from "../../core/validation.js";
5
- import { resolveEligibleLayer } from "../../selectors/layers.js";
5
+ import { collectResourceReferences } from "../../core/resourceReferences.js";
6
+ import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
7
+ import { isSourceOwnedText } from "../../grammar/text.js";
6
8
  import { transformPointHighlightChild } from "../../materialization/selection/point.js";
7
9
  import {
8
10
  transformPathHighlightProperties,
@@ -22,6 +24,10 @@ import {
22
24
  normalizeDimOthers,
23
25
  validateUnitInterval
24
26
  } from "../../materialization/selection/styles.js";
27
+ import {
28
+ normalizeThemeState,
29
+ resolveEffectiveThemeTokens
30
+ } from "../theme/state.js";
25
31
 
26
32
  const SELECTOR_KEYS = Object.freeze([
27
33
  "grain", "field", "channel", "property", "op", "value", "values", "min", "max",
@@ -58,32 +64,38 @@ function resolveTarget(program, target, label = "mark selection") {
58
64
  }
59
65
 
60
66
  function hasLegendSelection(program, target, selection) {
61
- const legend = program.guideConfigs.legend?.series ??
62
- program.guideConfigs.legend?.color;
63
67
  const selector = program.materializationConfigs.selections?.[selection]?.selector;
64
- return legend?.target === target &&
65
- selector?.field !== undefined &&
66
- selector.field === legend.field;
68
+ const layer = findLayer(program, target);
69
+ const field = selector?.field ?? layer?.encoding?.[selector?.channel]?.field;
70
+ if (field === undefined) return false;
71
+ return ["series", "color", "stroke"].some(kind => {
72
+ const legend = program.guideConfigs.legend?.[kind];
73
+ return legend?.target === target && legend.field === field &&
74
+ (selector?.channel === undefined || legend.channels.includes(selector.channel));
75
+ });
67
76
  }
68
77
 
69
- function categoricalLegendKind(program, target) {
70
- return ["series", "color"].find(
78
+ function categoricalLegendKinds(program, target) {
79
+ return ["series", "color", "stroke"].filter(
71
80
  kind => program.guideConfigs.legend?.[kind]?.target === target
72
81
  );
73
82
  }
74
83
 
75
84
  function resetCategoricalLegendSymbols(program, target) {
76
- const kind = categoricalLegendKind(program, target);
77
- if (kind === undefined) return program;
85
+ const kinds = categoricalLegendKinds(program, target);
86
+ if (kinds.length === 0) return program;
78
87
  let next = program;
79
- for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
80
- const graphic = next.graphicSpec.objects[id];
81
- if (graphic === undefined) continue;
82
- next = graphic.type === "collection"
83
- ? next.editGraphics({ target: id, property: "items", value: [] })
84
- : next.editGraphics({ target: id, property: "length", value: 0 });
88
+ for (const kind of kinds) {
89
+ for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
90
+ const graphic = next.graphicSpec.objects[id];
91
+ if (graphic === undefined) continue;
92
+ next = graphic.type === "collection"
93
+ ? next.editGraphics({ target: id, property: "items", value: [] })
94
+ : next.editGraphics({ target: id, property: "length", value: 0 });
95
+ }
96
+ next = next.rematerializeLegendSymbols({ kind });
85
97
  }
86
- return next.rematerializeLegendSymbols();
98
+ return next;
87
99
  }
88
100
 
89
101
  function targetHighlightEntries(program, target) {
@@ -91,7 +103,32 @@ function targetHighlightEntries(program, target) {
91
103
  .filter(([, config]) => config.target === target);
92
104
  }
93
105
 
94
- function rebuildTargetHighlights(program, target) {
106
+ function namedLabelDependents(program, selection) {
107
+ const referenced = new Set(collectResourceReferences(program, {
108
+ kind: "selection",
109
+ id: selection
110
+ }).filter(reference =>
111
+ reference.ownerKind === "markConfig" &&
112
+ reference.path.join(".") === "labelAuthoring.selection.id"
113
+ ).map(reference => reference.ownerId));
114
+ return Object.entries(program.markConfigs)
115
+ .filter(([id, config]) => {
116
+ const layer = findLayer(program, id);
117
+ return isSourceOwnedText(layer) &&
118
+ config?.labelAuthoring?.selection?.kind === "named" &&
119
+ referenced.has(id);
120
+ })
121
+ .map(([id]) => id)
122
+ .sort();
123
+ }
124
+
125
+ function rematerializeLabels(program, ids) {
126
+ let next = program;
127
+ for (const id of ids) next = next.rematerializeTextMark({ id });
128
+ return next;
129
+ }
130
+
131
+ function rebuildTargetHighlights(program, target, { labels = [] } = {}) {
95
132
  const layer = resolveTarget(program, target, "highlight mark");
96
133
  const highlights = targetHighlightEntries(program, target);
97
134
  let baseline = program;
@@ -105,12 +142,26 @@ function rebuildTargetHighlights(program, target) {
105
142
  baseline = baseline[requireSelectionPolicy(
106
143
  layer.mark.type
107
144
  ).rematerializeOp]({ id: target });
145
+ baseline = rematerializeLabels(baseline, labels);
108
146
  baseline = resetCategoricalLegendSymbols(baseline, target);
109
147
  return highlights.length === 0
110
148
  ? baseline
111
149
  : baseline.rematerializeMarkHighlights({ target, highlights });
112
150
  }
113
151
 
152
+ export function rematerializeLabelsBeforeHighlights(
153
+ program,
154
+ target,
155
+ labels,
156
+ { skipWithoutHighlights = false } = {}
157
+ ) {
158
+ return targetHighlightEntries(program, target).length === 0
159
+ ? skipWithoutHighlights
160
+ ? program
161
+ : rematerializeLabels(program, labels)
162
+ : rebuildTargetHighlights(program, target, { labels });
163
+ }
164
+
114
165
  function selectedKeys(args, resolved) {
115
166
  if (args.keys === undefined) return resolved.keys;
116
167
  if (!Array.isArray(args.keys) || !args.keys.every(key =>
@@ -155,9 +206,10 @@ function editSelectedItems(
155
206
  }
156
207
 
157
208
  function applyHighlight(program, config, keys) {
209
+ const style = resolveHighlightStyle(program, config);
158
210
  let next = program[requireSelectionPolicy(config.markType).applyHighlightOp]({
159
211
  selection: config.selection,
160
- style: config.style,
212
+ style,
161
213
  keys
162
214
  });
163
215
  if (config.dimOthers !== false) {
@@ -172,6 +224,60 @@ function applyHighlight(program, config, keys) {
172
224
  : next;
173
225
  }
174
226
 
227
+ function highlightPaintProperty(markType) {
228
+ return ["line", "rule", "tick"].includes(markType) ? "stroke" : "fill";
229
+ }
230
+
231
+ function requestedPaintSource(args, markType, style) {
232
+ const property = highlightPaintProperty(markType);
233
+ const explicit = args.color !== undefined || args[property] !== undefined;
234
+ if (explicit) return "explicit";
235
+ return Object.hasOwn(style, property) ? "theme" : "preserve";
236
+ }
237
+
238
+ function tracePaintSource(program, id, config) {
239
+ let source;
240
+ for (const node of program.trace.children) {
241
+ if (node.op !== "highlightMarks") continue;
242
+ const requestedId = node.args.selection ?? node.args.id ??
243
+ `${node.args.target ?? config.target}Selection`;
244
+ if (requestedId !== id) continue;
245
+ const property = highlightPaintProperty(config.markType);
246
+ const explicit = node.args.color !== undefined ||
247
+ node.args[property] !== undefined;
248
+ source = explicit
249
+ ? "explicit"
250
+ : Object.hasOwn(config.style, property) ? "theme" : "preserve";
251
+ }
252
+ return source ?? "explicit";
253
+ }
254
+
255
+ function normalizeStoredHighlightConfig(program, id, config) {
256
+ return config.paintSource === undefined
257
+ ? { ...config, paintSource: tracePaintSource(program, id, config) }
258
+ : config;
259
+ }
260
+
261
+ function resolveHighlightStyle(program, config) {
262
+ if (config.paintSource !== "theme") return config.style;
263
+ const property = highlightPaintProperty(config.markType);
264
+ return {
265
+ ...config.style,
266
+ [property]: resolveEffectiveThemeTokens(normalizeThemeState(
267
+ program.materializationConfigs.theme
268
+ )).highlight
269
+ };
270
+ }
271
+
272
+ export function rematerializeThemeHighlights(program) {
273
+ const targets = [...new Set(Object.values(
274
+ program.materializationConfigs.highlights ?? {}
275
+ ).map(config => config.target))];
276
+ let next = program;
277
+ for (const target of targets) next = rebuildTargetHighlights(next, target);
278
+ return next;
279
+ }
280
+
175
281
  export const selectMarks = action(
176
282
  { op: "selectMarks", description: "Create one reusable selection over final mark items." },
177
283
  function (args = {}) {
@@ -198,7 +304,7 @@ export const editMarkSelection = action(
198
304
  current.definition.target,
199
305
  selectorFrom(args)
200
306
  );
201
- const next = this
307
+ let next = this
202
308
  ._withSelectionConfig(current.id, {
203
309
  target: current.definition.target,
204
310
  selector: replacement.selector
@@ -207,9 +313,15 @@ export const editMarkSelection = action(
207
313
  const hasDependentHighlight = Object.values(
208
314
  next.materializationConfigs.highlights ?? {}
209
315
  ).some(config => config.selection === current.id);
210
- return hasDependentHighlight
211
- ? rebuildTargetHighlights(next, current.definition.target)
212
- : next;
316
+ const labels = namedLabelDependents(this, current.id);
317
+ if (hasDependentHighlight) {
318
+ return rematerializeLabelsBeforeHighlights(
319
+ next,
320
+ current.definition.target,
321
+ labels
322
+ );
323
+ }
324
+ return rematerializeLabels(next, labels);
213
325
  }
214
326
  );
215
327
 
@@ -245,6 +357,12 @@ export const removeMarkSelection = action(
245
357
  function (args = {}) {
246
358
  validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkSelection");
247
359
  const current = resolveStoredSelection(this, args.selection);
360
+ const labelDependents = namedLabelDependents(this, current.id);
361
+ if (labelDependents.length > 0) {
362
+ throw new Error(
363
+ `Selection "${current.id}" is referenced by attached labels: ${labelDependents.join(", ")}.`
364
+ );
365
+ }
248
366
  const hasDependentHighlight = Object.values(
249
367
  this.materializationConfigs.highlights ?? {}
250
368
  ).some(config => config.selection === current.id);
@@ -287,6 +405,15 @@ function transformRectangularProperties(properties, style, offset = false) {
287
405
  };
288
406
  }
289
407
 
408
+ function transformRectangularHighlightChild(item, style, offset) {
409
+ return {
410
+ ...item,
411
+ properties: item.type === "path"
412
+ ? transformPathHighlightProperties(item.properties, style)
413
+ : transformRectangularProperties(item.properties, style, offset)
414
+ };
415
+ }
416
+
290
417
  function applyRectangularHighlight(
291
418
  program,
292
419
  args,
@@ -301,12 +428,8 @@ function applyRectangularHighlight(
301
428
  throw new Error(`${operation} requires a ${markType} selection.`);
302
429
  }
303
430
  if (keys.length === 0) return program;
304
- return editSelectedItems(program, resolved, keys, item => ({
305
- ...item,
306
- properties: transformRectangularProperties(
307
- item.properties, args.style, offset
308
- )
309
- }));
431
+ return editSelectedItems(program, resolved, keys, item =>
432
+ transformRectangularHighlightChild(item, args.style, offset));
310
433
  }
311
434
 
312
435
  export const applyBarHighlight = action(
@@ -415,7 +538,7 @@ export const rematerializeMarkHighlights = action(
415
538
  }
416
539
  const prepared = args.highlights.map(([id, config]) => ({
417
540
  id,
418
- config,
541
+ config: normalizeStoredHighlightConfig(this, id, config),
419
542
  keys: resolveStoredSelection(this, config.selection).keys
420
543
  }));
421
544
  let next = this;
@@ -492,6 +615,7 @@ export const highlightMarks = action(
492
615
  selection,
493
616
  markType: layer.mark.type,
494
617
  style,
618
+ paintSource: requestedPaintSource(args, layer.mark.type, style),
495
619
  dimOthers,
496
620
  bringToFront
497
621
  };
@@ -9,12 +9,15 @@ import {
9
9
  highlightMarks,
10
10
  placeSelectedMarkItemsLast,
11
11
  rematerializeMarkHighlights,
12
+ rematerializeThemeHighlights,
12
13
  removeMarkHighlight,
13
14
  removeMarkSelection,
14
15
  selectMarks
15
16
  } from "./actions.js";
17
+ import { registerThemeHighlightReconciler } from "../theme/reconcile.js";
16
18
 
17
19
  export function registerSelectionActions(ProgramClass) {
20
+ registerThemeHighlightReconciler(rematerializeThemeHighlights);
18
21
  ProgramClass.prototype.selectMarks = selectMarks;
19
22
  ProgramClass.prototype.editMarkSelection = editMarkSelection;
20
23
  ProgramClass.prototype.removeMarkHighlight = removeMarkHighlight;
@@ -1,25 +1,71 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { noOptions, validateOptionObject } from "../../core/validation.js";
3
- import { THEME_NAMES } from "../../theme/defaults.js";
3
+ import { normalizeThemeDefinition } from "../../theme/defaults.js";
4
+ import {
5
+ LOCAL_THEME_OWNER,
6
+ createThemeFrame,
7
+ moveLocalThemeScope,
8
+ normalizeThemeState,
9
+ removeThemeFrameOwner,
10
+ upsertThemeFrame
11
+ } from "./state.js";
12
+
13
+ let compositionThemeHandlers;
14
+
15
+ export function registerCompositionThemeHandlers(handlers) {
16
+ if (handlers === undefined ||
17
+ typeof handlers.apply !== "function" ||
18
+ typeof handlers.remove !== "function") {
19
+ throw new TypeError("Composition theme handlers must provide apply and remove functions.");
20
+ }
21
+ compositionThemeHandlers = handlers;
22
+ }
23
+
24
+ function requireCompositionThemeHandlers() {
25
+ if (compositionThemeHandlers === undefined) {
26
+ throw new Error("Composition theme actions are unavailable from this entry point.");
27
+ }
28
+ return compositionThemeHandlers;
29
+ }
4
30
 
5
31
  export const applyTheme = action(
6
32
  {
7
33
  op: "applyTheme",
8
- description: "Apply persistent visual defaults to the chart program."
34
+ description: "Apply persistent visual defaults to the chart program.",
35
+ scope: "any"
9
36
  },
10
37
  function (args = {}) {
11
- validateOptionObject(args, ["theme"], "applyTheme", {
38
+ validateOptionObject(args, ["theme", "scope"], "applyTheme", {
12
39
  allowEmpty: false,
13
40
  emptyMessage: "applyTheme requires theme.",
14
41
  emptyError: Error
15
42
  });
16
- if (!THEME_NAMES.includes(args.theme)) {
17
- throw new Error(`Unsupported theme "${args.theme}".`);
43
+ if (!Object.hasOwn(args, "theme")) {
44
+ throw new Error("applyTheme requires theme.");
18
45
  }
19
- const previous = this.materializationConfigs.theme;
46
+ const definition = normalizeThemeDefinition(args.theme);
47
+ if (args.scope !== undefined &&
48
+ !["self", "descendants"].includes(args.scope)) {
49
+ throw new Error(`Unsupported theme scope "${args.scope}".`);
50
+ }
51
+ if (this.compositionSpec !== undefined) {
52
+ return requireCompositionThemeHandlers().apply(
53
+ this,
54
+ { base: definition.name, tokens: definition.tokens },
55
+ args.scope ?? "descendants"
56
+ );
57
+ }
58
+ const scope = "self";
59
+ const previous = normalizeThemeState(this.materializationConfigs.theme);
60
+ const frame = createThemeFrame({
61
+ owner: LOCAL_THEME_OWNER,
62
+ scope,
63
+ definition: { base: definition.name, tokens: definition.tokens }
64
+ });
20
65
  return this._withMaterializationConfig(["theme"], {
21
- name: args.theme,
22
- overrides: previous?.overrides ?? []
66
+ ...previous,
67
+ frames: upsertThemeFrame(previous.frames, frame),
68
+ localOrder: moveLocalThemeScope(previous.localOrder, scope)
23
69
  });
24
70
  }
25
71
  );
@@ -27,17 +73,26 @@ export const applyTheme = action(
27
73
  export const removeTheme = action(
28
74
  {
29
75
  op: "removeTheme",
30
- description: "Remove program theme defaults while preserving local styles."
76
+ description: "Remove program theme defaults while preserving local styles.",
77
+ scope: "any"
31
78
  },
32
79
  function (args = {}) {
33
80
  noOptions(args, "removeTheme");
34
- if (this.materializationConfigs.theme === undefined) {
81
+ if (this.compositionSpec !== undefined) {
82
+ return requireCompositionThemeHandlers().remove(this);
83
+ }
84
+ const previous = normalizeThemeState(this.materializationConfigs.theme);
85
+ const removed = removeThemeFrameOwner(
86
+ previous.frames,
87
+ LOCAL_THEME_OWNER
88
+ );
89
+ if (!removed.removed) {
35
90
  throw new Error("removeTheme requires an active program theme.");
36
91
  }
37
92
  return this._withMaterializationConfig(["theme"], {
38
- ...this.materializationConfigs.theme,
39
- name: "light",
40
- removing: true
93
+ ...previous,
94
+ frames: removed.frames,
95
+ localOrder: previous.localOrder.filter(scope => scope !== "self")
41
96
  });
42
97
  }
43
98
  );
@@ -0,0 +1,238 @@
1
+ import { freezeOwned } from "../../core/immutable.js";
2
+ import { materializeCompositionGraphics } from
3
+ "../../materialization/composition.js";
4
+ import { materializeFacetGraphics } from "../../materialization/facets.js";
5
+ import {
6
+ compositionThemeOwner,
7
+ createThemeFrame,
8
+ moveLocalThemeScope,
9
+ normalizeThemeState,
10
+ removeThemeFrameOwner,
11
+ upsertThemeFrame
12
+ } from "./state.js";
13
+ import { reconcileProgramTheme } from "./reconcile.js";
14
+
15
+ const DESCRIPTION = "Apply inherited visual defaults to a composition child.";
16
+
17
+ function materializeComposition(program) {
18
+ return program.compositionSpec.type === "facet"
19
+ ? materializeFacetGraphics(program)
20
+ : materializeCompositionGraphics(program);
21
+ }
22
+
23
+ function withThemeState(program, state) {
24
+ return program._withMaterializationConfig(["theme"], state);
25
+ }
26
+
27
+ function addFrame(program, frame, { descendants, local = false }) {
28
+ const composition = program.compositionSpec !== undefined;
29
+ const previous = normalizeThemeState(
30
+ program.materializationConfigs.theme,
31
+ { composition }
32
+ );
33
+ return withThemeState(program, {
34
+ ...previous,
35
+ frames: upsertThemeFrame(previous.frames, frame),
36
+ ...(composition
37
+ ? {
38
+ descendantFrames: descendants
39
+ ? upsertThemeFrame(previous.descendantFrames, frame)
40
+ : previous.descendantFrames
41
+ }
42
+ : {}),
43
+ localOrder: local
44
+ ? moveLocalThemeScope(previous.localOrder, frame.scope)
45
+ : previous.localOrder
46
+ });
47
+ }
48
+
49
+ function removeFrame(program, owner, { descendants, localScope }) {
50
+ const composition = program.compositionSpec !== undefined;
51
+ const previous = normalizeThemeState(
52
+ program.materializationConfigs.theme,
53
+ { composition }
54
+ );
55
+ const root = removeThemeFrameOwner(previous.frames, owner);
56
+ const policies = composition && descendants
57
+ ? removeThemeFrameOwner(previous.descendantFrames, owner)
58
+ : { frames: previous.descendantFrames, removed: false };
59
+ return {
60
+ program: withThemeState(program, {
61
+ ...previous,
62
+ frames: root.frames,
63
+ ...(composition ? { descendantFrames: policies.frames } : {}),
64
+ localOrder: localScope === undefined
65
+ ? previous.localOrder
66
+ : freezeOwned(previous.localOrder.filter(scope => scope !== localScope))
67
+ }),
68
+ removed: root.removed || policies.removed
69
+ };
70
+ }
71
+
72
+ function inheritedMetadata(op) {
73
+ return freezeOwned({ op, description: DESCRIPTION, scope: "any" });
74
+ }
75
+
76
+ function enterInheritedAction(program, op, frame) {
77
+ return program._enterAction({
78
+ op,
79
+ description: DESCRIPTION,
80
+ args: op === "applyTheme"
81
+ ? freezeOwned({
82
+ theme: freezeOwned({
83
+ base: frame.name,
84
+ tokens: frame.tokens
85
+ }),
86
+ scope: "descendants",
87
+ inheritedFrom: frame.owner
88
+ })
89
+ : freezeOwned({ inheritedFrom: frame.owner })
90
+ });
91
+ }
92
+
93
+ function applyInheritedFrame(program, frame) {
94
+ const source = program;
95
+ let next = enterInheritedAction(program, "applyTheme", frame);
96
+ next = addFrame(next, frame, {
97
+ descendants: next.compositionSpec !== undefined
98
+ });
99
+
100
+ if (next.compositionSpec !== undefined) {
101
+ const children = Object.fromEntries(
102
+ next.compositionSpec.children.map(id => [
103
+ id,
104
+ applyInheritedFrame(next.children[id], frame)
105
+ ])
106
+ );
107
+ next = next._withCompositionState({
108
+ children,
109
+ compositionSpec: next.compositionSpec
110
+ });
111
+ next = materializeComposition(next);
112
+ }
113
+
114
+ next = reconcileProgramTheme(next, {
115
+ source,
116
+ metadata: inheritedMetadata("applyTheme")
117
+ });
118
+ return next._exitAction();
119
+ }
120
+
121
+ function removeInheritedFrame(program, frame) {
122
+ const source = program;
123
+ let next = enterInheritedAction(program, "removeTheme", frame);
124
+ const removed = removeFrame(next, frame.owner, {
125
+ descendants: next.compositionSpec !== undefined
126
+ });
127
+ next = removed.program;
128
+
129
+ if (next.compositionSpec !== undefined) {
130
+ const children = Object.fromEntries(
131
+ next.compositionSpec.children.map(id => [
132
+ id,
133
+ removeInheritedFrame(next.children[id], frame)
134
+ ])
135
+ );
136
+ next = next._withCompositionState({
137
+ children,
138
+ compositionSpec: next.compositionSpec
139
+ });
140
+ next = materializeComposition(next);
141
+ }
142
+
143
+ next = reconcileProgramTheme(next, {
144
+ source,
145
+ metadata: inheritedMetadata("removeTheme")
146
+ });
147
+ return next._exitAction();
148
+ }
149
+
150
+ export function applyCompositionTheme(program, definition, scope) {
151
+ const id = program.compositionSpec.id;
152
+ const frame = createThemeFrame({
153
+ owner: compositionThemeOwner(id, scope),
154
+ scope,
155
+ definition
156
+ });
157
+ let next = addFrame(program, frame, {
158
+ descendants: scope === "descendants",
159
+ local: true
160
+ });
161
+ if (scope !== "descendants") return next;
162
+
163
+ const children = Object.fromEntries(
164
+ next.compositionSpec.children.map(childId => [
165
+ childId,
166
+ applyInheritedFrame(next.children[childId], frame)
167
+ ])
168
+ );
169
+ next = next._withCompositionState({
170
+ children,
171
+ compositionSpec: next.compositionSpec
172
+ });
173
+ return materializeComposition(next);
174
+ }
175
+
176
+ export function removeCompositionTheme(program) {
177
+ const state = normalizeThemeState(
178
+ program.materializationConfigs.theme,
179
+ { composition: true }
180
+ );
181
+ const scope = state.localOrder.at(-1);
182
+ if (scope === undefined) {
183
+ throw new Error("removeTheme requires an active program theme.");
184
+ }
185
+ const owner = compositionThemeOwner(program.compositionSpec.id, scope);
186
+ const frame = state.frames.find(current => current.owner === owner) ??
187
+ state.descendantFrames.find(current => current.owner === owner);
188
+ if (frame === undefined) {
189
+ throw new Error("removeTheme requires an active program theme.");
190
+ }
191
+
192
+ let next = removeFrame(program, owner, {
193
+ descendants: scope === "descendants",
194
+ localScope: scope
195
+ }).program;
196
+ if (scope !== "descendants") return next;
197
+
198
+ const children = Object.fromEntries(
199
+ next.compositionSpec.children.map(id => [
200
+ id,
201
+ removeInheritedFrame(next.children[id], frame)
202
+ ])
203
+ );
204
+ next = next._withCompositionState({
205
+ children,
206
+ compositionSpec: next.compositionSpec
207
+ });
208
+ return materializeComposition(next);
209
+ }
210
+
211
+ export function replayCompositionThemeState(program, storedState) {
212
+ if (storedState === undefined) return program;
213
+ const state = normalizeThemeState(storedState, { composition: true });
214
+ let next = withThemeState(program, state);
215
+ for (const frame of state.descendantFrames) {
216
+ const children = Object.fromEntries(
217
+ next.compositionSpec.children.map(id => [
218
+ id,
219
+ applyInheritedFrame(next.children[id], frame)
220
+ ])
221
+ );
222
+ next = next._withCompositionState({
223
+ children,
224
+ compositionSpec: next.compositionSpec
225
+ });
226
+ }
227
+ return materializeComposition(next);
228
+ }
229
+
230
+ export function replayInheritedThemeFrames(program, storedState) {
231
+ if (storedState === undefined) return program;
232
+ const state = normalizeThemeState(storedState, { composition: true });
233
+ let next = program;
234
+ for (const frame of state.descendantFrames) {
235
+ next = applyInheritedFrame(next, frame);
236
+ }
237
+ return next;
238
+ }
@@ -1,8 +1,15 @@
1
1
  import { setActionCompletionHook } from "../../core/action.js";
2
- import { applyTheme, removeTheme } from "./actions.js";
2
+ import {
3
+ applyTheme,
4
+ registerCompositionThemeHandlers,
5
+ removeTheme
6
+ } from "./actions.js";
3
7
  import { reconcileProgramTheme } from "./reconcile.js";
4
8
 
5
- export function registerThemeActions(ProgramClass) {
9
+ export function registerThemeActions(ProgramClass, compositionHandlers) {
10
+ if (compositionHandlers !== undefined) {
11
+ registerCompositionThemeHandlers(compositionHandlers);
12
+ }
6
13
  ProgramClass.prototype.applyTheme = applyTheme;
7
14
  ProgramClass.prototype.removeTheme = removeTheme;
8
15
  setActionCompletionHook(reconcileProgramTheme);