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
@@ -1,7 +1,26 @@
1
1
  import { themeTokens } from "../../theme/defaults.js";
2
+ import {
3
+ normalizeThemeState,
4
+ resolveEffectiveThemeTokens,
5
+ setThemeStateOverrides
6
+ } from "./state.js";
2
7
  import { findLayer } from "../../selectors/layers.js";
3
8
 
4
- const COLOR_PROPERTIES = Object.freeze(["background", "fill", "stroke"]);
9
+ let rematerializeThemeHighlights = program => program;
10
+
11
+ export function registerThemeHighlightReconciler(reconciler) {
12
+ if (typeof reconciler !== "function") {
13
+ throw new TypeError("Theme highlight reconciler must be a function.");
14
+ }
15
+ rematerializeThemeHighlights = reconciler;
16
+ }
17
+
18
+ const THEME_STYLE_PROPERTIES = Object.freeze([
19
+ "background",
20
+ "fill",
21
+ "stroke",
22
+ "fontFamily"
23
+ ]);
5
24
  const COLOR_TOKENS = Object.freeze([
6
25
  "mark",
7
26
  "text",
@@ -20,51 +39,60 @@ const COLOR_TOKENS = Object.freeze([
20
39
  "gradientCenter"
21
40
  ]);
22
41
 
23
- function tokenFor(value, names) {
24
- for (const name of names) {
25
- const tokens = themeTokens(name);
26
- for (const token of COLOR_TOKENS) {
27
- if (value === tokens[token]) return token;
28
- }
42
+ function tokenFor(value, sourceTokens, targetTokens) {
43
+ const matches = COLOR_TOKENS.filter(token =>
44
+ value === sourceTokens[token] || value === themeTokens("light")[token]
45
+ );
46
+ if (matches.length === 0) return undefined;
47
+ const targets = new Set(matches.map(token => targetTokens[token]));
48
+ if (targets.size !== 1) return undefined;
49
+ for (const token of matches) {
50
+ if (targetTokens[token] === targets.values().next().value) return token;
29
51
  }
30
52
  return undefined;
31
53
  }
32
54
 
33
- function roleValues(role, names, legacy = []) {
55
+ function roleValues(role, sourceTokens, legacy = []) {
34
56
  return new Set([
35
- ...names.map(name => themeTokens(name)[role]),
57
+ sourceTokens[role],
36
58
  themeTokens("light")[role],
37
59
  ...legacy.map(token => themeTokens("light")[token] ?? token)
38
60
  ]);
39
61
  }
40
62
 
41
- function mapRoleValue(value, role, names, targetTokens, legacy) {
63
+ function mapRoleValue(value, role, sourceTokens, targetTokens, legacy) {
42
64
  if (Array.isArray(value)) {
43
65
  let changed = false;
44
66
  const next = value.map(item => {
45
- const mapped = mapRoleValue(item, role, names, targetTokens, legacy);
67
+ const mapped = mapRoleValue(
68
+ item,
69
+ role,
70
+ sourceTokens,
71
+ targetTokens,
72
+ legacy
73
+ );
46
74
  changed ||= mapped.changed;
47
75
  return mapped.value;
48
76
  });
49
77
  return { changed, value: changed ? next : value };
50
78
  }
51
- if (!roleValues(role, names, legacy).has(value)) {
79
+ if (!roleValues(role, sourceTokens, legacy).has(value)) {
52
80
  return { changed: false, value };
53
81
  }
54
82
  return { changed: value !== targetTokens[role], value: targetTokens[role] };
55
83
  }
56
84
 
57
- function mapColorValue(value, names, targetTokens) {
85
+ function mapColorValue(value, sourceTokens, targetTokens) {
58
86
  if (Array.isArray(value)) {
59
87
  let changed = false;
60
88
  const next = value.map(item => {
61
- const mapped = mapColorValue(item, names, targetTokens);
89
+ const mapped = mapColorValue(item, sourceTokens, targetTokens);
62
90
  changed ||= mapped.changed;
63
91
  return mapped.value;
64
92
  });
65
93
  return { changed, value: changed ? next : value };
66
94
  }
67
- const token = tokenFor(value, names);
95
+ const token = tokenFor(value, sourceTokens, targetTokens);
68
96
  if (token === undefined) return { changed: false, value };
69
97
  const next = targetTokens[token];
70
98
  return { changed: next !== value, value: next };
@@ -73,7 +101,10 @@ function mapColorValue(value, names, targetTokens) {
73
101
  function isFieldDrivenMark(program, id, property) {
74
102
  if (!(["fill", "stroke"].includes(property))) return false;
75
103
  const layer = findLayer(program, id);
76
- return layer?.encoding?.color?.field !== undefined;
104
+ return property === "stroke"
105
+ ? layer?.encoding?.stroke?.field !== undefined ||
106
+ (layer?.mark?.type === "line" && layer.encoding?.color?.field !== undefined)
107
+ : layer?.encoding?.color?.field !== undefined;
77
108
  }
78
109
 
79
110
  function isRegressionBand(program, id) {
@@ -128,12 +159,16 @@ function componentRole(program, id, property) {
128
159
  }
129
160
 
130
161
  function graphicRole(program, id, property) {
162
+ if (property === "fontFamily") return "fontFamily";
131
163
  if (id === "canvas" && property === "background") return "background";
164
+ if (id === `${program.compositionSpec?.id}-headers` && property === "fill") {
165
+ return "strongText";
166
+ }
132
167
  const component = componentRole(program, id, property);
133
168
  if (component !== undefined) return component;
134
169
  const layer = findLayer(program, id);
135
170
  if (layer !== undefined && ["fill", "stroke"].includes(property)) {
136
- if (layer.encoding?.color?.field !== undefined) return undefined;
171
+ if (isFieldDrivenMark(program, id, property)) return undefined;
137
172
  if (property === "fill" && isRegressionBand(program, id)) {
138
173
  return "regressionBand";
139
174
  }
@@ -176,7 +211,7 @@ function isOverridden(overrides, key) {
176
211
  }
177
212
 
178
213
  function isFieldDrivenLegendSymbol(id) {
179
- return /(?:color|series)Legend(?:Symbol|Symbols|Gradient|Swatches|Lines|Points)/iu
214
+ return /(?:color|series|stroke)(?:Legend|Gradient|Interval)(?:Symbol|Symbols|Strips|Swatches|Lines|Points)/iu
180
215
  .test(id);
181
216
  }
182
217
 
@@ -185,14 +220,14 @@ function mapGraphicProperties(
185
220
  id,
186
221
  type,
187
222
  properties,
188
- names,
223
+ sourceTokens,
189
224
  targetTokens,
190
225
  overrides,
191
226
  overrideId = id
192
227
  ) {
193
228
  let changed = false;
194
229
  const next = { ...properties };
195
- for (const property of COLOR_PROPERTIES) {
230
+ for (const property of THEME_STYLE_PROPERTIES) {
196
231
  if (!Object.hasOwn(properties, property)) continue;
197
232
  if (isOverridden(overrides, `g:${overrideId}.${property}`) ||
198
233
  isOverridden(overrides, `g:${id}.${property}`)) continue;
@@ -202,7 +237,7 @@ function mapGraphicProperties(
202
237
  const mapped = mapRoleValue(
203
238
  properties[property],
204
239
  role,
205
- names,
240
+ sourceTokens,
206
241
  targetTokens,
207
242
  roleLegacy(role)
208
243
  );
@@ -214,21 +249,28 @@ function mapGraphicProperties(
214
249
  return { changed, properties: changed ? next : properties, type };
215
250
  }
216
251
 
217
- function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
252
+ function recolorGraphic(
253
+ program,
254
+ id,
255
+ graphic,
256
+ sourceTokens,
257
+ targetTokens,
258
+ overrides
259
+ ) {
218
260
  if (graphic.items === undefined) {
219
261
  const mapped = mapGraphicProperties(
220
262
  program,
221
263
  id,
222
264
  graphic.type,
223
265
  graphic.properties,
224
- names,
266
+ sourceTokens,
225
267
  targetTokens,
226
268
  overrides,
227
269
  id
228
270
  );
229
271
  if (!mapped.changed) return program;
230
272
  let next = program;
231
- for (const property of COLOR_PROPERTIES) {
273
+ for (const property of THEME_STYLE_PROPERTIES) {
232
274
  if (mapped.properties[property] !== graphic.properties[property]) {
233
275
  next = next.editGraphics({
234
276
  target: id,
@@ -248,7 +290,7 @@ function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
248
290
  id,
249
291
  type,
250
292
  item.properties,
251
- names,
293
+ sourceTokens,
252
294
  targetTokens,
253
295
  overrides,
254
296
  item.id
@@ -263,6 +305,13 @@ function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
263
305
 
264
306
  function configRole(program, path) {
265
307
  const property = path.at(-1);
308
+ if (property === "fontFamily") return "fontFamily";
309
+ if (path[0] === "facets" && path.includes("headers") &&
310
+ property === "color") {
311
+ return path[path.indexOf("headers") + 1] === "row"
312
+ ? "mutedText"
313
+ : "strongText";
314
+ }
266
315
  if (path[0] === "marks" && ["fill", "stroke"].includes(property)) {
267
316
  if (path.includes("boxPlot") && path.includes("median") &&
268
317
  property === "stroke") return "boxMedian";
@@ -271,7 +320,7 @@ function configRole(program, path) {
271
320
  const component = componentRole(program, path[1], property);
272
321
  if (component !== undefined) return component;
273
322
  const layer = findLayer(program, path[1]);
274
- if (layer?.encoding?.color?.field !== undefined) return undefined;
323
+ if (isFieldDrivenMark(program, path[1], property)) return undefined;
275
324
  if (property === "fill" && isRegressionBand(program, path[1])) {
276
325
  return "regressionBand";
277
326
  }
@@ -298,7 +347,7 @@ function configRole(program, path) {
298
347
  function recolorConfig(
299
348
  program,
300
349
  value,
301
- names,
350
+ sourceTokens,
302
351
  targetTokens,
303
352
  overrides,
304
353
  path = []
@@ -309,7 +358,7 @@ function recolorConfig(
309
358
  const mapped = recolorConfig(
310
359
  program,
311
360
  item,
312
- names,
361
+ sourceTokens,
313
362
  targetTokens,
314
363
  overrides,
315
364
  [...path, index]
@@ -321,7 +370,8 @@ function recolorConfig(
321
370
  }
322
371
  if (value === null || typeof value !== "object") {
323
372
  const property = path.at(-1);
324
- if (!["color", "fill", "stroke", "background"].includes(property)) {
373
+ if (!["color", "fill", "stroke", "background", "fontFamily"]
374
+ .includes(property)) {
325
375
  return { changed: false, value };
326
376
  }
327
377
  if (isOverridden(overrides, `c:${path.join(".")}`)) {
@@ -332,8 +382,14 @@ function recolorConfig(
332
382
  }
333
383
  const role = configRole(program, path);
334
384
  return role === undefined
335
- ? mapColorValue(value, names, targetTokens)
336
- : mapRoleValue(value, role, names, targetTokens, roleLegacy(role));
385
+ ? mapColorValue(value, sourceTokens, targetTokens)
386
+ : mapRoleValue(
387
+ value,
388
+ role,
389
+ sourceTokens,
390
+ targetTokens,
391
+ roleLegacy(role)
392
+ );
337
393
  }
338
394
  if (["theme", "highlights", "selections"].includes(path[0])) {
339
395
  return { changed: false, value };
@@ -344,7 +400,7 @@ function recolorConfig(
344
400
  const mapped = recolorConfig(
345
401
  program,
346
402
  item,
347
- names,
403
+ sourceTokens,
348
404
  targetTokens,
349
405
  overrides,
350
406
  [...path, key]
@@ -367,7 +423,7 @@ function addMarkOverrides(overrides, program, op, args) {
367
423
  (op.startsWith("create") ? type : undefined) ??
368
424
  (candidates.length === 1 ? candidates[0].id : program.context.currentMark);
369
425
  if (typeof id !== "string") return;
370
- for (const property of ["fill", "stroke"]) {
426
+ for (const property of ["fill", "stroke", "fontFamily"]) {
371
427
  if (!Object.hasOwn(args, property)) continue;
372
428
  overrides.add(`c:marks.${id}.${property}`);
373
429
  overrides.add(`g:${id}.${property}`);
@@ -431,12 +487,14 @@ function resolveConfiguredOwner(program, key, requested, defaultId) {
431
487
  }
432
488
 
433
489
  function addTextFacadeOverrides(overrides, node) {
434
- if (!["createAnnotation", "createMarkLabels"].includes(node.op) ||
435
- !Object.hasOwn(node.args, "fill")) return;
490
+ if (!["createAnnotation", "createMarkLabels"].includes(node.op)) return;
436
491
  const id = findDescendant(node, "createTextMark")?.args.id ?? node.args.id;
437
492
  if (typeof id !== "string") return;
438
- overrides.add(`c:marks.${id}.fill`);
439
- overrides.add(`g:${id}.fill`);
493
+ for (const property of ["fill", "fontFamily"]) {
494
+ if (!Object.hasOwn(node.args, property)) continue;
495
+ overrides.add(`c:marks.${id}.${property}`);
496
+ overrides.add(`g:${id}.${property}`);
497
+ }
440
498
  }
441
499
 
442
500
  function addErrorBarOverrides(overrides, program, node) {
@@ -651,7 +709,7 @@ function clearRecreatedOverrides(overrides, op, args) {
651
709
  function addAxisOverrides(overrides, op, args) {
652
710
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis(Line|Ticks|Labels|Title)$/u
653
711
  .exec(op);
654
- if (match === null || !Object.hasOwn(args, "color")) return;
712
+ if (match === null) return;
655
713
  const channel = {
656
714
  X: "x",
657
715
  Y: "y",
@@ -660,10 +718,17 @@ function addAxisOverrides(overrides, op, args) {
660
718
  }[match[1]];
661
719
  const component = match[2].toLowerCase();
662
720
  const idPrefix = match[1] === "Radial" ? "radial" : channel;
663
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
664
- overrides.add(
665
- `g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
666
- );
721
+ if (Object.hasOwn(args, "color")) {
722
+ overrides.add(`c:guides.axis.${channel}.${component}.color`);
723
+ overrides.add(
724
+ `g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
725
+ );
726
+ }
727
+ if (["labels", "title"].includes(component) &&
728
+ Object.hasOwn(args, "fontFamily")) {
729
+ overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
730
+ overrides.add(`g:${idPrefix}Axis${match[2]}.fontFamily`);
731
+ }
667
732
  }
668
733
 
669
734
  function addCompleteAxisOverride(overrides, name, args) {
@@ -680,12 +745,18 @@ function addCompleteAxisOverride(overrides, name, args) {
680
745
  components.labels = args.ticksAndLabels.labels;
681
746
  }
682
747
  for (const [component, options] of Object.entries(components)) {
683
- if (!Object.hasOwn(options ?? {}, "color")) continue;
684
748
  const suffix = `${component[0].toUpperCase()}${component.slice(1)}`;
685
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
686
- overrides.add(
687
- `g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
688
- );
749
+ if (Object.hasOwn(options ?? {}, "color")) {
750
+ overrides.add(`c:guides.axis.${channel}.${component}.color`);
751
+ overrides.add(
752
+ `g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
753
+ );
754
+ }
755
+ if (["labels", "title"].includes(component) &&
756
+ Object.hasOwn(options ?? {}, "fontFamily")) {
757
+ overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
758
+ overrides.add(`g:${idPrefix}Axis${suffix}.fontFamily`);
759
+ }
689
760
  }
690
761
  }
691
762
 
@@ -722,9 +793,14 @@ function addTitleOverrides(overrides, args) {
722
793
  ["titleStyle", "chartTitle"],
723
794
  ["subtitleStyle", "chartSubtitle"]
724
795
  ]) {
725
- if (!Object.hasOwn(args[option] ?? {}, "color")) continue;
726
- overrides.add(`c:title.${option}.color`);
727
- overrides.add(`g:${id}.fill`);
796
+ if (Object.hasOwn(args[option] ?? {}, "color")) {
797
+ overrides.add(`c:title.${option}.color`);
798
+ overrides.add(`g:${id}.fill`);
799
+ }
800
+ if (Object.hasOwn(args[option] ?? {}, "fontFamily")) {
801
+ overrides.add(`c:title.${option}.fontFamily`);
802
+ overrides.add(`g:${id}.fontFamily`);
803
+ }
728
804
  }
729
805
  }
730
806
 
@@ -744,10 +820,17 @@ function addParallelOverrides(overrides, program, op, args) {
744
820
  components.labels = args.ticksAndLabels.labels;
745
821
  }
746
822
  for (const [component, options] of Object.entries(components)) {
747
- if (!Object.hasOwn(options ?? {}, "color")) continue;
748
- overrides.add(
749
- `c:guides.axis.parallel.axes.dimensions.${index}.${component}.color`
750
- );
823
+ if (Object.hasOwn(options ?? {}, "color")) {
824
+ overrides.add(
825
+ `c:guides.axis.parallel.axes.dimensions.${index}.${component}.color`
826
+ );
827
+ }
828
+ if (["labels", "title"].includes(component) &&
829
+ Object.hasOwn(options ?? {}, "fontFamily")) {
830
+ overrides.add(
831
+ `c:guides.axis.parallel.axes.dimensions.${index}.${component}.fontFamily`
832
+ );
833
+ }
751
834
  }
752
835
  }
753
836
 
@@ -756,8 +839,11 @@ function addLegendOverrides(overrides, program, args) {
756
839
  const prefixes = {
757
840
  series: "seriesLegend",
758
841
  color: "colorLegend",
842
+ stroke: "strokeLegend",
759
843
  interval: "colorLegend",
760
844
  gradient: "colorGradient",
845
+ strokeInterval: "strokeInterval",
846
+ strokeGradient: "strokeGradient",
761
847
  size: "sizeLegend",
762
848
  strokeWidth: "strokeWidthLegend",
763
849
  opacity: "opacityLegend"
@@ -767,10 +853,18 @@ function addLegendOverrides(overrides, program, args) {
767
853
  overrides.add(`c:guides.legend.${kind}.labels.color`);
768
854
  if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Labels.fill`);
769
855
  }
856
+ if (Object.hasOwn(args.labels ?? {}, "fontFamily")) {
857
+ overrides.add(`c:guides.legend.${kind}.labels.fontFamily`);
858
+ if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Labels.fontFamily`);
859
+ }
770
860
  if (Object.hasOwn(args.titleStyle ?? {}, "color")) {
771
861
  overrides.add(`c:guides.legend.${kind}.titleStyle.color`);
772
862
  if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Title.fill`);
773
863
  }
864
+ if (Object.hasOwn(args.titleStyle ?? {}, "fontFamily")) {
865
+ overrides.add(`c:guides.legend.${kind}.titleStyle.fontFamily`);
866
+ if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Title.fontFamily`);
867
+ }
774
868
  if (Object.hasOwn(args.border ?? {}, "color")) {
775
869
  overrides.add(`c:guides.legend.${kind}.border.color`);
776
870
  if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Background.stroke`);
@@ -778,12 +872,79 @@ function addLegendOverrides(overrides, program, args) {
778
872
  }
779
873
  }
780
874
 
875
+ function legendPrefix(kind) {
876
+ return {
877
+ series: "seriesLegend",
878
+ color: "colorLegend",
879
+ stroke: "strokeLegend",
880
+ interval: "colorLegend",
881
+ gradient: "colorGradient",
882
+ strokeInterval: "strokeInterval",
883
+ strokeGradient: "strokeGradient",
884
+ size: "sizeLegend",
885
+ strokeWidth: "strokeWidthLegend",
886
+ opacity: "opacityLegend"
887
+ }[kind];
888
+ }
889
+
890
+ function addLegendBlockOverrides(overrides, program, args) {
891
+ if (typeof args.target !== "string" || typeof args.channel !== "string") return;
892
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
893
+ if (config.target !== args.target) continue;
894
+ const key = Object.keys(config.blockOverrides ?? {}).find(value => {
895
+ try {
896
+ return JSON.parse(value).includes(args.channel);
897
+ } catch {
898
+ return false;
899
+ }
900
+ });
901
+ if (key === undefined) continue;
902
+ const prefix = `c:guides.legend.${kind}.blockOverrides.${key}`;
903
+ const graphic = legendPrefix(kind);
904
+ for (const [property, graphicProperty] of [
905
+ ["color", "fill"],
906
+ ["fontFamily", "fontFamily"]
907
+ ]) {
908
+ if (!Object.hasOwn(args.text ?? {}, property)) continue;
909
+ overrides.add(`${prefix}.text.${property}`);
910
+ if (graphic !== undefined) {
911
+ overrides.add(`g:${graphic}Labels.${graphicProperty}`);
912
+ }
913
+ }
914
+ for (const property of ["fill", "stroke"]) {
915
+ if (!Object.hasOwn(args.symbol ?? {}, property)) continue;
916
+ overrides.add(`${prefix}.symbol.${property}`);
917
+ if (graphic !== undefined) {
918
+ overrides.add(`g:${graphic}Symbols.${property}`);
919
+ }
920
+ }
921
+ }
922
+ }
923
+
924
+ function addFacetOverrides(overrides, program, args) {
925
+ const id = program.compositionSpec?.type === "facet"
926
+ ? program.compositionSpec.id
927
+ : undefined;
928
+ if (id === undefined) return;
929
+ const owner = args.role === undefined || args.role === "all"
930
+ ? "common"
931
+ : args.role;
932
+ for (const [property, graphicProperty] of [
933
+ ["color", "fill"],
934
+ ["fontFamily", "fontFamily"]
935
+ ]) {
936
+ if (!Object.hasOwn(args, property)) continue;
937
+ overrides.add(`c:facets.${id}.headers.${owner}.${property}`);
938
+ overrides.add(`g:${id}-headers.${graphicProperty}`);
939
+ }
940
+ }
941
+
781
942
  function explicitOverrides(program, node) {
782
943
  const { op, args } = node;
783
944
  const overrides = new Set();
784
945
  if (op === "editGraphics" &&
785
946
  typeof args.target === "string" &&
786
- ["background", "fill", "stroke"].includes(args.property)) {
947
+ ["background", "fill", "stroke", "fontFamily"].includes(args.property)) {
787
948
  overrides.add(`g:${args.target}.${args.property}`);
788
949
  }
789
950
  if (["createCanvas", "editCanvas"].includes(op) &&
@@ -818,6 +979,12 @@ function explicitOverrides(program, node) {
818
979
  if (op === "editLegendBorder") {
819
980
  addLegendOverrides(overrides, program, args);
820
981
  }
982
+ if (op === "editLegendBlock") {
983
+ addLegendBlockOverrides(overrides, program, args);
984
+ }
985
+ if (op === "editFacetHeaders") {
986
+ addFacetOverrides(overrides, program, args);
987
+ }
821
988
  return overrides;
822
989
  }
823
990
 
@@ -835,23 +1002,75 @@ function collectOverrides(program) {
835
1002
  return overrides;
836
1003
  }
837
1004
 
1005
+ function callExisting(program, graphicId, operation) {
1006
+ return program.graphicSpec.objects[graphicId] !== undefined &&
1007
+ typeof program[operation] === "function"
1008
+ ? program[operation]()
1009
+ : program;
1010
+ }
1011
+
1012
+ function rematerializeThemeTypography(program) {
1013
+ if (program.compositionSpec !== undefined) {
1014
+ return program.compositionSpec.type === "facet"
1015
+ ? program.materializeComposition()
1016
+ : program;
1017
+ }
1018
+
1019
+ let next = program;
1020
+ for (const layer of next.semanticSpec.layers) {
1021
+ if (layer.mark?.type === "text" &&
1022
+ next.graphicSpec.objects[layer.id] !== undefined &&
1023
+ typeof next.rematerializeTextMark === "function") {
1024
+ next = next.rematerializeTextMark({ id: layer.id });
1025
+ }
1026
+ }
1027
+ for (const [id, operation] of [
1028
+ ["xAxisLabels", "editXAxisLabels"],
1029
+ ["yAxisLabels", "editYAxisLabels"],
1030
+ ["xAxisTitle", "editXAxisTitle"],
1031
+ ["yAxisTitle", "editYAxisTitle"],
1032
+ ["thetaAxisLabels", "editThetaAxisLabels"],
1033
+ ["radialAxisLabels", "editRadialAxisLabels"],
1034
+ ["thetaAxisTitle", "editThetaAxisTitle"],
1035
+ ["radialAxisTitle", "editRadialAxisTitle"]
1036
+ ]) {
1037
+ next = callExisting(next, id, operation);
1038
+ }
1039
+ if (Object.keys(next.guideConfigs.legend ?? {}).length > 0 &&
1040
+ typeof next.rematerializeLegend === "function") {
1041
+ next = next.rematerializeLegend();
1042
+ }
1043
+ next = callExisting(next, "chartTitle", "rematerializeTitle");
1044
+ return next;
1045
+ }
1046
+
838
1047
  export function reconcileProgramTheme(program, { source, metadata }) {
839
- const state = program.materializationConfigs.theme;
840
- if (state === undefined) return program;
841
- const sourceName = source.materializationConfigs.theme?.name ?? "light";
842
- const targetTokens = themeTokens(state.name);
843
- const names = sourceName === "light" ? ["light"] : [sourceName, "light"];
1048
+ const storedState = program.materializationConfigs.theme;
1049
+ if (storedState === undefined) return program;
1050
+ const composition = program.compositionSpec !== undefined;
1051
+ const state = normalizeThemeState(storedState, { composition });
1052
+ const sourceState = normalizeThemeState(
1053
+ source.materializationConfigs.theme,
1054
+ { composition }
1055
+ );
1056
+ const sourceRootTokens = resolveEffectiveThemeTokens(sourceState);
1057
+ const targetRootTokens = resolveEffectiveThemeTokens(state);
1058
+ const contentState = current => composition
1059
+ ? { frames: current.frames.filter(frame => frame.scope === "descendants") }
1060
+ : current;
1061
+ const sourceTokens = resolveEffectiveThemeTokens(contentState(sourceState));
1062
+ const targetTokens = resolveEffectiveThemeTokens(contentState(state));
844
1063
  const overrides = collectOverrides(program);
845
1064
 
846
- let themed = program._withMaterializationConfig(["theme"], {
847
- ...state,
848
- overrides: [...overrides].sort()
849
- });
1065
+ let themed = program._withMaterializationConfig(
1066
+ ["theme"],
1067
+ setThemeStateOverrides(state, [...overrides])
1068
+ );
850
1069
 
851
1070
  const configs = recolorConfig(
852
1071
  themed,
853
1072
  themed.materializationConfigs,
854
- names,
1073
+ sourceTokens,
855
1074
  targetTokens,
856
1075
  overrides
857
1076
  );
@@ -863,6 +1082,17 @@ export function reconcileProgramTheme(program, { source, metadata }) {
863
1082
  if (configs.changed && configs.value.title !== undefined) {
864
1083
  next = next._withMaterializationConfig(["title"], configs.value.title);
865
1084
  }
1085
+ if (configs.changed && configs.value.facets !== undefined) {
1086
+ next = next._withMaterializationConfig(["facets"], configs.value.facets);
1087
+ }
1088
+
1089
+ const facetsChanged = configs.value.facets !==
1090
+ themed.materializationConfigs.facets;
1091
+ if (facetsChanged && next.compositionSpec?.type === "facet") {
1092
+ next = next.materializeComposition();
1093
+ } else if (sourceTokens.fontFamily !== targetTokens.fontFamily) {
1094
+ next = rematerializeThemeTypography(next);
1095
+ }
866
1096
 
867
1097
  const parallelChanged = configs.value.guides.axis?.parallel !==
868
1098
  themed.guideConfigs.axis?.parallel;
@@ -872,10 +1102,28 @@ export function reconcileProgramTheme(program, { source, metadata }) {
872
1102
 
873
1103
  for (const [id, graphic] of Object.entries(next.graphicSpec.objects)) {
874
1104
  if (parallelChanged && id.startsWith("parallelAxis")) continue;
875
- next = recolorGraphic(next, id, graphic, names, targetTokens, overrides);
1105
+ const graphicSourceTokens = id === "canvas"
1106
+ ? sourceRootTokens
1107
+ : sourceTokens;
1108
+ const graphicTargetTokens = id === "canvas"
1109
+ ? targetRootTokens
1110
+ : targetTokens;
1111
+ next = recolorGraphic(
1112
+ next,
1113
+ id,
1114
+ graphic,
1115
+ graphicSourceTokens,
1116
+ graphicTargetTokens,
1117
+ overrides
1118
+ );
1119
+ }
1120
+
1121
+ if (sourceTokens.highlight !== targetTokens.highlight) {
1122
+ next = rematerializeThemeHighlights(next);
876
1123
  }
877
1124
 
878
- if (state.removing === true && metadata.op === "removeTheme") {
1125
+ if (metadata.op === "removeTheme" && state.frames.length === 0 &&
1126
+ (state.descendantFrames?.length ?? 0) === 0) {
879
1127
  next = next._withoutMaterializationConfig(["theme"]);
880
1128
  }
881
1129
  return next;