ggaction 0.0.13 → 0.0.16

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 (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  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-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -1,7 +1,27 @@
1
+ import { rematerializeTypography } from "../../materialization/typography.js";
1
2
  import { themeTokens } from "../../theme/defaults.js";
3
+ import {
4
+ normalizeThemeState,
5
+ resolveEffectiveThemeTokens,
6
+ setThemeStateOverrides
7
+ } from "./state.js";
2
8
  import { findLayer } from "../../selectors/layers.js";
3
9
 
4
- const COLOR_PROPERTIES = Object.freeze(["background", "fill", "stroke"]);
10
+ let rematerializeThemeHighlights = program => program;
11
+
12
+ export function registerThemeHighlightReconciler(reconciler) {
13
+ if (typeof reconciler !== "function") {
14
+ throw new TypeError("Theme highlight reconciler must be a function.");
15
+ }
16
+ rematerializeThemeHighlights = reconciler;
17
+ }
18
+
19
+ const THEME_STYLE_PROPERTIES = Object.freeze([
20
+ "background",
21
+ "fill",
22
+ "stroke",
23
+ "fontFamily"
24
+ ]);
5
25
  const COLOR_TOKENS = Object.freeze([
6
26
  "mark",
7
27
  "text",
@@ -20,51 +40,57 @@ const COLOR_TOKENS = Object.freeze([
20
40
  "gradientCenter"
21
41
  ]);
22
42
 
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
- }
29
- }
30
- return undefined;
43
+ function tokenFor(value, sourceTokens, targetTokens) {
44
+ const matches = COLOR_TOKENS.filter(token =>
45
+ value === sourceTokens[token] || value === themeTokens("light")[token]
46
+ );
47
+ if (matches.length === 0) return undefined;
48
+ const targets = new Set(matches.map(token => targetTokens[token]));
49
+ if (targets.size !== 1) return undefined;
50
+ return matches[0];
31
51
  }
32
52
 
33
- function roleValues(role, names, legacy = []) {
53
+ function roleValues(role, sourceTokens, legacy = []) {
34
54
  return new Set([
35
- ...names.map(name => themeTokens(name)[role]),
55
+ sourceTokens[role],
36
56
  themeTokens("light")[role],
37
57
  ...legacy.map(token => themeTokens("light")[token] ?? token)
38
58
  ]);
39
59
  }
40
60
 
41
- function mapRoleValue(value, role, names, targetTokens, legacy) {
61
+ function mapRoleValue(value, role, sourceTokens, targetTokens, legacy) {
42
62
  if (Array.isArray(value)) {
43
63
  let changed = false;
44
64
  const next = value.map(item => {
45
- const mapped = mapRoleValue(item, role, names, targetTokens, legacy);
65
+ const mapped = mapRoleValue(
66
+ item,
67
+ role,
68
+ sourceTokens,
69
+ targetTokens,
70
+ legacy
71
+ );
46
72
  changed ||= mapped.changed;
47
73
  return mapped.value;
48
74
  });
49
75
  return { changed, value: changed ? next : value };
50
76
  }
51
- if (!roleValues(role, names, legacy).has(value)) {
77
+ if (!roleValues(role, sourceTokens, legacy).has(value)) {
52
78
  return { changed: false, value };
53
79
  }
54
80
  return { changed: value !== targetTokens[role], value: targetTokens[role] };
55
81
  }
56
82
 
57
- function mapColorValue(value, names, targetTokens) {
83
+ function mapColorValue(value, sourceTokens, targetTokens) {
58
84
  if (Array.isArray(value)) {
59
85
  let changed = false;
60
86
  const next = value.map(item => {
61
- const mapped = mapColorValue(item, names, targetTokens);
87
+ const mapped = mapColorValue(item, sourceTokens, targetTokens);
62
88
  changed ||= mapped.changed;
63
89
  return mapped.value;
64
90
  });
65
91
  return { changed, value: changed ? next : value };
66
92
  }
67
- const token = tokenFor(value, names);
93
+ const token = tokenFor(value, sourceTokens, targetTokens);
68
94
  if (token === undefined) return { changed: false, value };
69
95
  const next = targetTokens[token];
70
96
  return { changed: next !== value, value: next };
@@ -73,7 +99,10 @@ function mapColorValue(value, names, targetTokens) {
73
99
  function isFieldDrivenMark(program, id, property) {
74
100
  if (!(["fill", "stroke"].includes(property))) return false;
75
101
  const layer = findLayer(program, id);
76
- return layer?.encoding?.color?.field !== undefined;
102
+ return property === "stroke"
103
+ ? layer?.encoding?.stroke?.field !== undefined ||
104
+ (layer?.mark?.type === "line" && layer.encoding?.color?.field !== undefined)
105
+ : layer?.encoding?.color?.field !== undefined;
77
106
  }
78
107
 
79
108
  function isRegressionBand(program, id) {
@@ -128,12 +157,16 @@ function componentRole(program, id, property) {
128
157
  }
129
158
 
130
159
  function graphicRole(program, id, property) {
160
+ if (property === "fontFamily") return "fontFamily";
131
161
  if (id === "canvas" && property === "background") return "background";
162
+ if (id === `${program.compositionSpec?.id}-headers` && property === "fill") {
163
+ return "strongText";
164
+ }
132
165
  const component = componentRole(program, id, property);
133
166
  if (component !== undefined) return component;
134
167
  const layer = findLayer(program, id);
135
168
  if (layer !== undefined && ["fill", "stroke"].includes(property)) {
136
- if (layer.encoding?.color?.field !== undefined) return undefined;
169
+ if (isFieldDrivenMark(program, id, property)) return undefined;
137
170
  if (property === "fill" && isRegressionBand(program, id)) {
138
171
  return "regressionBand";
139
172
  }
@@ -176,7 +209,7 @@ function isOverridden(overrides, key) {
176
209
  }
177
210
 
178
211
  function isFieldDrivenLegendSymbol(id) {
179
- return /(?:color|series)Legend(?:Symbol|Symbols|Gradient|Swatches|Lines|Points)/iu
212
+ return /(?:color|series|stroke)(?:Legend|Gradient|Interval)(?:Symbol|Symbols|Strips|Swatches|Lines|Points)/iu
180
213
  .test(id);
181
214
  }
182
215
 
@@ -185,14 +218,14 @@ function mapGraphicProperties(
185
218
  id,
186
219
  type,
187
220
  properties,
188
- names,
221
+ sourceTokens,
189
222
  targetTokens,
190
223
  overrides,
191
224
  overrideId = id
192
225
  ) {
193
226
  let changed = false;
194
227
  const next = { ...properties };
195
- for (const property of COLOR_PROPERTIES) {
228
+ for (const property of THEME_STYLE_PROPERTIES) {
196
229
  if (!Object.hasOwn(properties, property)) continue;
197
230
  if (isOverridden(overrides, `g:${overrideId}.${property}`) ||
198
231
  isOverridden(overrides, `g:${id}.${property}`)) continue;
@@ -202,7 +235,7 @@ function mapGraphicProperties(
202
235
  const mapped = mapRoleValue(
203
236
  properties[property],
204
237
  role,
205
- names,
238
+ sourceTokens,
206
239
  targetTokens,
207
240
  roleLegacy(role)
208
241
  );
@@ -214,21 +247,28 @@ function mapGraphicProperties(
214
247
  return { changed, properties: changed ? next : properties, type };
215
248
  }
216
249
 
217
- function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
250
+ function recolorGraphic(
251
+ program,
252
+ id,
253
+ graphic,
254
+ sourceTokens,
255
+ targetTokens,
256
+ overrides
257
+ ) {
218
258
  if (graphic.items === undefined) {
219
259
  const mapped = mapGraphicProperties(
220
260
  program,
221
261
  id,
222
262
  graphic.type,
223
263
  graphic.properties,
224
- names,
264
+ sourceTokens,
225
265
  targetTokens,
226
266
  overrides,
227
267
  id
228
268
  );
229
269
  if (!mapped.changed) return program;
230
270
  let next = program;
231
- for (const property of COLOR_PROPERTIES) {
271
+ for (const property of THEME_STYLE_PROPERTIES) {
232
272
  if (mapped.properties[property] !== graphic.properties[property]) {
233
273
  next = next.editGraphics({
234
274
  target: id,
@@ -248,7 +288,7 @@ function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
248
288
  id,
249
289
  type,
250
290
  item.properties,
251
- names,
291
+ sourceTokens,
252
292
  targetTokens,
253
293
  overrides,
254
294
  item.id
@@ -263,6 +303,13 @@ function recolorGraphic(program, id, graphic, names, targetTokens, overrides) {
263
303
 
264
304
  function configRole(program, path) {
265
305
  const property = path.at(-1);
306
+ if (property === "fontFamily") return "fontFamily";
307
+ if (path[0] === "facets" && path.includes("headers") &&
308
+ property === "color") {
309
+ return path[path.indexOf("headers") + 1] === "row"
310
+ ? "mutedText"
311
+ : "strongText";
312
+ }
266
313
  if (path[0] === "marks" && ["fill", "stroke"].includes(property)) {
267
314
  if (path.includes("boxPlot") && path.includes("median") &&
268
315
  property === "stroke") return "boxMedian";
@@ -271,7 +318,7 @@ function configRole(program, path) {
271
318
  const component = componentRole(program, path[1], property);
272
319
  if (component !== undefined) return component;
273
320
  const layer = findLayer(program, path[1]);
274
- if (layer?.encoding?.color?.field !== undefined) return undefined;
321
+ if (isFieldDrivenMark(program, path[1], property)) return undefined;
275
322
  if (property === "fill" && isRegressionBand(program, path[1])) {
276
323
  return "regressionBand";
277
324
  }
@@ -298,7 +345,7 @@ function configRole(program, path) {
298
345
  function recolorConfig(
299
346
  program,
300
347
  value,
301
- names,
348
+ sourceTokens,
302
349
  targetTokens,
303
350
  overrides,
304
351
  path = []
@@ -309,7 +356,7 @@ function recolorConfig(
309
356
  const mapped = recolorConfig(
310
357
  program,
311
358
  item,
312
- names,
359
+ sourceTokens,
313
360
  targetTokens,
314
361
  overrides,
315
362
  [...path, index]
@@ -321,7 +368,8 @@ function recolorConfig(
321
368
  }
322
369
  if (value === null || typeof value !== "object") {
323
370
  const property = path.at(-1);
324
- if (!["color", "fill", "stroke", "background"].includes(property)) {
371
+ if (!["color", "fill", "stroke", "background", "fontFamily"]
372
+ .includes(property)) {
325
373
  return { changed: false, value };
326
374
  }
327
375
  if (isOverridden(overrides, `c:${path.join(".")}`)) {
@@ -332,8 +380,14 @@ function recolorConfig(
332
380
  }
333
381
  const role = configRole(program, path);
334
382
  return role === undefined
335
- ? mapColorValue(value, names, targetTokens)
336
- : mapRoleValue(value, role, names, targetTokens, roleLegacy(role));
383
+ ? mapColorValue(value, sourceTokens, targetTokens)
384
+ : mapRoleValue(
385
+ value,
386
+ role,
387
+ sourceTokens,
388
+ targetTokens,
389
+ roleLegacy(role)
390
+ );
337
391
  }
338
392
  if (["theme", "highlights", "selections"].includes(path[0])) {
339
393
  return { changed: false, value };
@@ -344,7 +398,7 @@ function recolorConfig(
344
398
  const mapped = recolorConfig(
345
399
  program,
346
400
  item,
347
- names,
401
+ sourceTokens,
348
402
  targetTokens,
349
403
  overrides,
350
404
  [...path, key]
@@ -355,6 +409,11 @@ function recolorConfig(
355
409
  return { changed, value: changed ? next : value };
356
410
  }
357
411
 
412
+ function addMarkPropertyOverrides(overrides, id, property) {
413
+ overrides.add(`c:marks.${id}.${property}`);
414
+ overrides.add(`g:${id}.${property}`);
415
+ }
416
+
358
417
  function addMarkOverrides(overrides, program, op, args) {
359
418
  const match = /^(?:create|edit)(Point|Line|Area|Arc|Bar|Rect|Rule|Tick|Text)Mark$/u
360
419
  .exec(op);
@@ -367,44 +426,45 @@ function addMarkOverrides(overrides, program, op, args) {
367
426
  (op.startsWith("create") ? type : undefined) ??
368
427
  (candidates.length === 1 ? candidates[0].id : program.context.currentMark);
369
428
  if (typeof id !== "string") return;
370
- for (const property of ["fill", "stroke"]) {
429
+ for (const property of ["fill", "stroke", "fontFamily"]) {
371
430
  if (!Object.hasOwn(args, property)) continue;
372
- overrides.add(`c:marks.${id}.${property}`);
373
- overrides.add(`g:${id}.${property}`);
431
+ addMarkPropertyOverrides(overrides, id, property);
374
432
  }
375
433
  }
376
434
 
377
435
  function addFacadeMarkOverrides(overrides, op, args) {
378
436
  const facades = {
379
- createScatterPlot: ["scatterPlot", "point", ["fill", "stroke"]],
380
- createLinePlot: ["linePlot", "line", ["stroke"]],
381
- createPolarScatterPlot: ["polarScatterPlot", "point", ["fill", "stroke"]],
382
- createPolarLinePlot: ["polarLinePlot", "line", ["stroke"]],
383
- createRadarPlot: ["radarPlot", "line", ["stroke"]],
384
- createRugPlot: ["rugPlot", "tick", ["stroke"]],
385
- createStripPlot: ["stripPlot", "point", ["fill", "stroke"]],
386
- createBeeswarmPlot: ["beeswarmPlot", "point", ["fill", "stroke"]],
387
- createBarPlot: ["barPlot", "bar", ["fill", "stroke"]],
388
- createAreaPlot: ["areaPlot", "area", ["fill", "stroke"]],
389
- createPiePlot: ["piePlot", "arc", ["fill", "stroke"]],
390
- createRosePlot: ["rosePlot", "arc", ["fill", "stroke"]],
391
- createRadialBarPlot: ["radialBarPlot", "arc", ["fill", "stroke"]],
392
- createDensityPlot: ["densityPlot", "area", ["fill", "stroke"]],
393
- createParallelCoordinates: ["parallelCoordinates", "line", ["stroke"]],
394
- createHistogram: ["histogram", "bar", ["fill", "stroke"]],
395
- createHeatmap: ["heatmap", "rect", ["stroke"]],
396
- createHorizonPlot: ["horizonPlot", "area", ["stroke"]],
397
- editHorizon: ["horizonPlot", "area", ["stroke"]],
398
- createViolinPlot: ["violinPlot", "area", ["fill", "stroke"]]
437
+ createScatterPlot: ["point", true],
438
+ createLinePlot: ["line", false],
439
+ createPolarScatterPlot: ["point", true],
440
+ createPolarLinePlot: ["line", false],
441
+ createRadarPlot: ["line", false],
442
+ createRugPlot: ["tick", false],
443
+ createStripPlot: ["point", true],
444
+ createBeeswarmPlot: ["point", true],
445
+ createBarPlot: ["bar", true],
446
+ createAreaPlot: ["area", true],
447
+ createPiePlot: ["arc", true],
448
+ createRosePlot: ["arc", true],
449
+ createRadialBarPlot: ["arc", true],
450
+ createDensityPlot: ["area", true],
451
+ createParallelCoordinates: ["line", false],
452
+ createHistogram: ["bar", true],
453
+ createHeatmap: ["rect", false],
454
+ createHorizonPlot: ["area", false],
455
+ editHorizon: ["area", false],
456
+ createViolinPlot: ["area", true]
399
457
  };
400
458
  const facade = facades[op];
401
459
  if (facade === undefined) return;
402
- const [defaultId, option, properties] = facade;
460
+ const [option, includesFill] = facade;
461
+ const subject = op === "editHorizon" ? "HorizonPlot" : op.slice(6);
462
+ const defaultId = subject[0].toLowerCase() + subject.slice(1);
463
+ const properties = includesFill ? ["fill", "stroke"] : ["stroke"];
403
464
  const id = args.id ?? defaultId;
404
465
  for (const property of properties) {
405
466
  if (!Object.hasOwn(args[option] ?? {}, property)) continue;
406
- overrides.add(`c:marks.${id}.${property}`);
407
- overrides.add(`g:${id}.${property}`);
467
+ addMarkPropertyOverrides(overrides, id, property);
408
468
  }
409
469
  }
410
470
 
@@ -431,12 +491,13 @@ function resolveConfiguredOwner(program, key, requested, defaultId) {
431
491
  }
432
492
 
433
493
  function addTextFacadeOverrides(overrides, node) {
434
- if (!["createAnnotation", "createMarkLabels"].includes(node.op) ||
435
- !Object.hasOwn(node.args, "fill")) return;
494
+ if (!["createAnnotation", "createMarkLabels"].includes(node.op)) return;
436
495
  const id = findDescendant(node, "createTextMark")?.args.id ?? node.args.id;
437
496
  if (typeof id !== "string") return;
438
- overrides.add(`c:marks.${id}.fill`);
439
- overrides.add(`g:${id}.fill`);
497
+ for (const property of ["fill", "fontFamily"]) {
498
+ if (!Object.hasOwn(node.args, property)) continue;
499
+ addMarkPropertyOverrides(overrides, id, property);
500
+ }
440
501
  }
441
502
 
442
503
  function addErrorBarOverrides(overrides, program, node) {
@@ -451,8 +512,7 @@ function addErrorBarOverrides(overrides, program, node) {
451
512
  if (id === undefined) return;
452
513
  const config = program.markConfigs[id].errorBar;
453
514
  for (const target of [id, config.lowerCapId, config.upperCapId]) {
454
- overrides.add(`c:marks.${target}.stroke`);
455
- overrides.add(`g:${target}.stroke`);
515
+ addMarkPropertyOverrides(overrides, target, "stroke");
456
516
  }
457
517
  }
458
518
 
@@ -478,8 +538,7 @@ function addErrorBandOverrides(overrides, program, node) {
478
538
  const targets = errorBandTargets(program, node);
479
539
  if (targets === undefined) return;
480
540
  if (Object.hasOwn(node.args, "fill") && node.args.fill !== false) {
481
- overrides.add(`c:marks.${targets.id}.fill`);
482
- overrides.add(`g:${targets.id}.fill`);
541
+ addMarkPropertyOverrides(overrides, targets.id, "fill");
483
542
  }
484
543
  const boundary = node.op === "editErrorBandBoundary"
485
544
  ? node.args
@@ -490,8 +549,7 @@ function addErrorBandOverrides(overrides, program, node) {
490
549
  ? [targets.lower, targets.upper]
491
550
  : [(node.args.boundary === "lower" ? targets.lower : targets.upper)];
492
551
  for (const id of ids) {
493
- overrides.add(`c:marks.${id}.stroke`);
494
- overrides.add(`g:${id}.stroke`);
552
+ addMarkPropertyOverrides(overrides, id, "stroke");
495
553
  }
496
554
  }
497
555
 
@@ -531,8 +589,7 @@ function addReferenceOverrides(overrides, op, args) {
531
589
  const id = args.id ?? defaultId;
532
590
  for (const property of properties) {
533
591
  if (!Object.hasOwn(args, property)) continue;
534
- overrides.add(`c:marks.${id}.${property}`);
535
- overrides.add(`g:${id}.${property}`);
592
+ addMarkPropertyOverrides(overrides, id, property);
536
593
  }
537
594
  }
538
595
 
@@ -553,12 +610,10 @@ function addRegressionOverrides(overrides, node) {
553
610
  : child?.args.id ?? child?.args.target;
554
611
  if (typeof id !== "string") return;
555
612
  if (Object.hasOwn(style ?? {}, "color")) {
556
- overrides.add(`c:marks.${id}.fill`);
557
- overrides.add(`g:${id}.fill`);
613
+ addMarkPropertyOverrides(overrides, id, "fill");
558
614
  }
559
615
  if (Object.hasOwn(style ?? {}, "stroke")) {
560
- overrides.add(`c:marks.${id}.stroke`);
561
- overrides.add(`g:${id}.stroke`);
616
+ addMarkPropertyOverrides(overrides, id, "stroke");
562
617
  }
563
618
  }
564
619
 
@@ -651,7 +706,7 @@ function clearRecreatedOverrides(overrides, op, args) {
651
706
  function addAxisOverrides(overrides, op, args) {
652
707
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis(Line|Ticks|Labels|Title)$/u
653
708
  .exec(op);
654
- if (match === null || !Object.hasOwn(args, "color")) return;
709
+ if (match === null) return;
655
710
  const channel = {
656
711
  X: "x",
657
712
  Y: "y",
@@ -660,10 +715,17 @@ function addAxisOverrides(overrides, op, args) {
660
715
  }[match[1]];
661
716
  const component = match[2].toLowerCase();
662
717
  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
- );
718
+ if (Object.hasOwn(args, "color")) {
719
+ overrides.add(`c:guides.axis.${channel}.${component}.color`);
720
+ overrides.add(
721
+ `g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
722
+ );
723
+ }
724
+ if (["labels", "title"].includes(component) &&
725
+ Object.hasOwn(args, "fontFamily")) {
726
+ overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
727
+ overrides.add(`g:${idPrefix}Axis${match[2]}.fontFamily`);
728
+ }
667
729
  }
668
730
 
669
731
  function addCompleteAxisOverride(overrides, name, args) {
@@ -680,12 +742,18 @@ function addCompleteAxisOverride(overrides, name, args) {
680
742
  components.labels = args.ticksAndLabels.labels;
681
743
  }
682
744
  for (const [component, options] of Object.entries(components)) {
683
- if (!Object.hasOwn(options ?? {}, "color")) continue;
684
745
  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
- );
746
+ if (Object.hasOwn(options ?? {}, "color")) {
747
+ overrides.add(`c:guides.axis.${channel}.${component}.color`);
748
+ overrides.add(
749
+ `g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
750
+ );
751
+ }
752
+ if (["labels", "title"].includes(component) &&
753
+ Object.hasOwn(options ?? {}, "fontFamily")) {
754
+ overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
755
+ overrides.add(`g:${idPrefix}Axis${suffix}.fontFamily`);
756
+ }
689
757
  }
690
758
  }
691
759
 
@@ -722,9 +790,14 @@ function addTitleOverrides(overrides, args) {
722
790
  ["titleStyle", "chartTitle"],
723
791
  ["subtitleStyle", "chartSubtitle"]
724
792
  ]) {
725
- if (!Object.hasOwn(args[option] ?? {}, "color")) continue;
726
- overrides.add(`c:title.${option}.color`);
727
- overrides.add(`g:${id}.fill`);
793
+ if (Object.hasOwn(args[option] ?? {}, "color")) {
794
+ overrides.add(`c:title.${option}.color`);
795
+ overrides.add(`g:${id}.fill`);
796
+ }
797
+ if (Object.hasOwn(args[option] ?? {}, "fontFamily")) {
798
+ overrides.add(`c:title.${option}.fontFamily`);
799
+ overrides.add(`g:${id}.fontFamily`);
800
+ }
728
801
  }
729
802
  }
730
803
 
@@ -744,46 +817,110 @@ function addParallelOverrides(overrides, program, op, args) {
744
817
  components.labels = args.ticksAndLabels.labels;
745
818
  }
746
819
  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
- );
820
+ if (Object.hasOwn(options ?? {}, "color")) {
821
+ overrides.add(
822
+ `c:guides.axis.parallel.axes.dimensions.${index}.${component}.color`
823
+ );
824
+ }
825
+ if (["labels", "title"].includes(component) &&
826
+ Object.hasOwn(options ?? {}, "fontFamily")) {
827
+ overrides.add(
828
+ `c:guides.axis.parallel.axes.dimensions.${index}.${component}.fontFamily`
829
+ );
830
+ }
751
831
  }
752
832
  }
753
833
 
754
834
  function addLegendOverrides(overrides, program, args) {
755
- const legend = program.guideConfigs.legend ?? {};
756
- const prefixes = {
835
+ for (const kind of Object.keys(program.guideConfigs.legend ?? {})) {
836
+ const prefix = legendPrefix(kind);
837
+ for (const [option, suffix, property, graphicProperty] of [
838
+ ["labels", "Labels", "color", "fill"],
839
+ ["labels", "Labels", "fontFamily", "fontFamily"],
840
+ ["titleStyle", "Title", "color", "fill"],
841
+ ["titleStyle", "Title", "fontFamily", "fontFamily"],
842
+ ["border", "Background", "color", "stroke"]
843
+ ]) {
844
+ if (!Object.hasOwn(args[option] ?? {}, property)) continue;
845
+ overrides.add(`c:guides.legend.${kind}.${option}.${property}`);
846
+ if (prefix) overrides.add(`g:${prefix}${suffix}.${graphicProperty}`);
847
+ }
848
+ }
849
+ }
850
+
851
+ function legendPrefix(kind) {
852
+ return {
757
853
  series: "seriesLegend",
758
854
  color: "colorLegend",
855
+ stroke: "strokeLegend",
759
856
  interval: "colorLegend",
760
857
  gradient: "colorGradient",
858
+ strokeInterval: "strokeInterval",
859
+ strokeGradient: "strokeGradient",
761
860
  size: "sizeLegend",
762
861
  strokeWidth: "strokeWidthLegend",
763
862
  opacity: "opacityLegend"
764
- };
765
- for (const kind of Object.keys(legend)) {
766
- if (Object.hasOwn(args.labels ?? {}, "color")) {
767
- overrides.add(`c:guides.legend.${kind}.labels.color`);
768
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Labels.fill`);
769
- }
770
- if (Object.hasOwn(args.titleStyle ?? {}, "color")) {
771
- overrides.add(`c:guides.legend.${kind}.titleStyle.color`);
772
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Title.fill`);
863
+ }[kind];
864
+ }
865
+
866
+ function addLegendBlockOverrides(overrides, program, args) {
867
+ if (typeof args.target !== "string" || typeof args.channel !== "string") return;
868
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
869
+ if (config.target !== args.target) continue;
870
+ const key = Object.keys(config.blockOverrides ?? {}).find(value => {
871
+ try {
872
+ return JSON.parse(value).includes(args.channel);
873
+ } catch {
874
+ return false;
875
+ }
876
+ });
877
+ if (key === undefined) continue;
878
+ const prefix = `c:guides.legend.${kind}.blockOverrides.${key}`;
879
+ const graphic = legendPrefix(kind);
880
+ for (const [property, graphicProperty] of [
881
+ ["color", "fill"],
882
+ ["fontFamily", "fontFamily"]
883
+ ]) {
884
+ if (!Object.hasOwn(args.text ?? {}, property)) continue;
885
+ overrides.add(`${prefix}.text.${property}`);
886
+ if (graphic !== undefined) {
887
+ overrides.add(`g:${graphic}Labels.${graphicProperty}`);
888
+ }
773
889
  }
774
- if (Object.hasOwn(args.border ?? {}, "color")) {
775
- overrides.add(`c:guides.legend.${kind}.border.color`);
776
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Background.stroke`);
890
+ for (const property of ["fill", "stroke"]) {
891
+ if (!Object.hasOwn(args.symbol ?? {}, property)) continue;
892
+ overrides.add(`${prefix}.symbol.${property}`);
893
+ if (graphic !== undefined) {
894
+ overrides.add(`g:${graphic}Symbols.${property}`);
895
+ }
777
896
  }
778
897
  }
779
898
  }
780
899
 
900
+ function addFacetOverrides(overrides, program, args) {
901
+ const id = program.compositionSpec?.type === "facet"
902
+ ? program.compositionSpec.id
903
+ : undefined;
904
+ if (id === undefined) return;
905
+ const owner = args.role === undefined || args.role === "all"
906
+ ? "common"
907
+ : args.role;
908
+ for (const [property, graphicProperty] of [
909
+ ["color", "fill"],
910
+ ["fontFamily", "fontFamily"]
911
+ ]) {
912
+ if (!Object.hasOwn(args, property)) continue;
913
+ overrides.add(`c:facets.${id}.headers.${owner}.${property}`);
914
+ overrides.add(`g:${id}-headers.${graphicProperty}`);
915
+ }
916
+ }
917
+
781
918
  function explicitOverrides(program, node) {
782
919
  const { op, args } = node;
783
920
  const overrides = new Set();
784
921
  if (op === "editGraphics" &&
785
922
  typeof args.target === "string" &&
786
- ["background", "fill", "stroke"].includes(args.property)) {
923
+ ["background", "fill", "stroke", "fontFamily"].includes(args.property)) {
787
924
  overrides.add(`g:${args.target}.${args.property}`);
788
925
  }
789
926
  if (["createCanvas", "editCanvas"].includes(op) &&
@@ -818,6 +955,12 @@ function explicitOverrides(program, node) {
818
955
  if (op === "editLegendBorder") {
819
956
  addLegendOverrides(overrides, program, args);
820
957
  }
958
+ if (op === "editLegendBlock") {
959
+ addLegendBlockOverrides(overrides, program, args);
960
+ }
961
+ if (op === "editFacetHeaders") {
962
+ addFacetOverrides(overrides, program, args);
963
+ }
821
964
  return overrides;
822
965
  }
823
966
 
@@ -836,22 +979,41 @@ function collectOverrides(program) {
836
979
  }
837
980
 
838
981
  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"];
982
+ const storedState = program.materializationConfigs.theme;
983
+ if (storedState === undefined) return program;
984
+ // Data/context/trace-only transitions cannot change authored appearance.
985
+ // Preserve all visual identities instead of rescanning every existing mark.
986
+ if (program.graphicSpec === source.graphicSpec &&
987
+ program.materializationConfigs === source.materializationConfigs &&
988
+ program.children === source.children &&
989
+ program.compositionSpec === source.compositionSpec &&
990
+ ["layers", "scales", "coordinates", "guides", "title"].every(
991
+ key => program.semanticSpec[key] === source.semanticSpec[key]
992
+ )) return program;
993
+ const composition = program.compositionSpec !== undefined;
994
+ const state = normalizeThemeState(storedState, { composition });
995
+ const sourceState = normalizeThemeState(
996
+ source.materializationConfigs.theme,
997
+ { composition }
998
+ );
999
+ const sourceRootTokens = resolveEffectiveThemeTokens(sourceState);
1000
+ const targetRootTokens = resolveEffectiveThemeTokens(state);
1001
+ const contentState = current => composition
1002
+ ? { frames: current.frames.filter(frame => frame.scope === "descendants") }
1003
+ : current;
1004
+ const sourceTokens = resolveEffectiveThemeTokens(contentState(sourceState));
1005
+ const targetTokens = resolveEffectiveThemeTokens(contentState(state));
844
1006
  const overrides = collectOverrides(program);
845
1007
 
846
- let themed = program._withMaterializationConfig(["theme"], {
847
- ...state,
848
- overrides: [...overrides].sort()
849
- });
1008
+ let themed = program._withMaterializationConfig(
1009
+ ["theme"],
1010
+ setThemeStateOverrides(state, [...overrides])
1011
+ );
850
1012
 
851
1013
  const configs = recolorConfig(
852
1014
  themed,
853
1015
  themed.materializationConfigs,
854
- names,
1016
+ sourceTokens,
855
1017
  targetTokens,
856
1018
  overrides
857
1019
  );
@@ -863,19 +1025,49 @@ export function reconcileProgramTheme(program, { source, metadata }) {
863
1025
  if (configs.changed && configs.value.title !== undefined) {
864
1026
  next = next._withMaterializationConfig(["title"], configs.value.title);
865
1027
  }
1028
+ if (configs.changed && configs.value.facets !== undefined) {
1029
+ next = next._withMaterializationConfig(["facets"], configs.value.facets);
1030
+ }
1031
+
1032
+ const facetsChanged = configs.value.facets !==
1033
+ themed.materializationConfigs.facets;
1034
+ const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily;
1035
+ if (facetsChanged && next.compositionSpec?.type === "facet") {
1036
+ next = next.materializeComposition();
1037
+ } else if (typographyChanged) {
1038
+ next = rematerializeTypography(next);
1039
+ }
866
1040
 
867
1041
  const parallelChanged = configs.value.guides.axis?.parallel !==
868
1042
  themed.guideConfigs.axis?.parallel;
869
- if (parallelChanged && typeof next.rematerializeParallelAxes === "function") {
1043
+ if (parallelChanged && !typographyChanged && typeof next.rematerializeParallelAxes === "function") {
870
1044
  next = next.rematerializeParallelAxes();
871
1045
  }
872
1046
 
873
1047
  for (const [id, graphic] of Object.entries(next.graphicSpec.objects)) {
874
1048
  if (parallelChanged && id.startsWith("parallelAxis")) continue;
875
- next = recolorGraphic(next, id, graphic, names, targetTokens, overrides);
1049
+ const graphicSourceTokens = id === "canvas"
1050
+ ? sourceRootTokens
1051
+ : sourceTokens;
1052
+ const graphicTargetTokens = id === "canvas"
1053
+ ? targetRootTokens
1054
+ : targetTokens;
1055
+ next = recolorGraphic(
1056
+ next,
1057
+ id,
1058
+ graphic,
1059
+ graphicSourceTokens,
1060
+ graphicTargetTokens,
1061
+ overrides
1062
+ );
1063
+ }
1064
+
1065
+ if (sourceTokens.highlight !== targetTokens.highlight) {
1066
+ next = rematerializeThemeHighlights(next);
876
1067
  }
877
1068
 
878
- if (state.removing === true && metadata.op === "removeTheme") {
1069
+ if (metadata.op === "removeTheme" && state.frames.length === 0 &&
1070
+ (state.descendantFrames?.length ?? 0) === 0) {
879
1071
  next = next._withoutMaterializationConfig(["theme"]);
880
1072
  }
881
1073
  return next;