ggaction 0.0.12 → 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 (378) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +10 -3
  11. package/src/actions/basic.js +4 -2
  12. package/src/actions/boxPlots/components.js +14 -4
  13. package/src/actions/boxPlots/edit.js +31 -5
  14. package/src/actions/boxPlots/materialize.js +13 -4
  15. package/src/actions/boxPlots/options.js +48 -16
  16. package/src/actions/canvas/actions.js +3 -1
  17. package/src/actions/canvas/fitting.js +211 -0
  18. package/src/actions/canvas/index.js +2 -0
  19. package/src/actions/categoryOrder/index.js +18 -6
  20. package/src/actions/charts/area.js +51 -0
  21. package/src/actions/charts/bar.js +13 -6
  22. package/src/actions/charts/beeswarm.js +70 -0
  23. package/src/actions/charts/density.js +48 -0
  24. package/src/actions/charts/ecdf.js +186 -0
  25. package/src/actions/charts/endpoints.js +473 -0
  26. package/src/actions/charts/heatmap.js +4 -3
  27. package/src/actions/charts/histogram.js +5 -4
  28. package/src/actions/charts/horizon.js +73 -0
  29. package/src/actions/charts/index.js +37 -0
  30. package/src/actions/charts/interval-regression.js +172 -0
  31. package/src/actions/charts/line.js +8 -6
  32. package/src/actions/charts/parallel.js +8 -2
  33. package/src/actions/charts/pie.js +52 -0
  34. package/src/actions/charts/polar.js +134 -0
  35. package/src/actions/charts/radar.js +305 -0
  36. package/src/actions/charts/radial.js +50 -0
  37. package/src/actions/charts/raincloud.js +422 -0
  38. package/src/actions/charts/rug-strip.js +283 -0
  39. package/src/actions/charts/scatter.js +11 -3
  40. package/src/actions/charts/shared.js +69 -10
  41. package/src/actions/composition/actions.js +129 -3
  42. package/src/actions/coordinates/actions.js +1 -1
  43. package/src/actions/coordinates/edit.js +76 -0
  44. package/src/actions/data/bin.js +29 -0
  45. package/src/actions/data/bin2d.js +57 -185
  46. package/src/actions/data/complete.js +26 -0
  47. package/src/actions/data/computed.js +24 -0
  48. package/src/actions/data/create.js +2 -2
  49. package/src/actions/data/density.js +12 -20
  50. package/src/actions/data/derived.js +111 -15
  51. package/src/actions/data/ecdf.js +38 -0
  52. package/src/actions/data/edit.js +539 -0
  53. package/src/actions/data/filter.js +226 -30
  54. package/src/actions/data/fold.js +24 -0
  55. package/src/actions/data/impute.js +26 -0
  56. package/src/actions/data/index.js +38 -0
  57. package/src/actions/data/interval.js +2 -0
  58. package/src/actions/data/intervalEdit.js +86 -4
  59. package/src/actions/data/normalize.js +27 -0
  60. package/src/actions/data/regression.js +19 -21
  61. package/src/actions/data/revisionBindings.js +145 -0
  62. package/src/actions/data/shared.js +20 -4
  63. package/src/actions/data/stack.js +26 -0
  64. package/src/actions/data/summary.js +26 -0
  65. package/src/actions/data/timeUnit.js +4 -1
  66. package/src/actions/data/window.js +1 -1
  67. package/src/actions/encodings/appearance.js +32 -13
  68. package/src/actions/encodings/barWidth.js +8 -0
  69. package/src/actions/encodings/basic.js +2 -0
  70. package/src/actions/encodings/channels.js +415 -0
  71. package/src/actions/encodings/color/continuous.js +19 -59
  72. package/src/actions/encodings/color/index.js +30 -23
  73. package/src/actions/encodings/color/layout.js +3 -97
  74. package/src/actions/encodings/color/policy.js +5 -8
  75. package/src/actions/encodings/density/resolve.js +37 -5
  76. package/src/actions/encodings/density.js +124 -34
  77. package/src/actions/encodings/histogram.js +3 -2
  78. package/src/actions/encodings/horizon/resolve.js +8 -4
  79. package/src/actions/encodings/horizon.js +18 -8
  80. package/src/actions/encodings/index.js +6 -0
  81. package/src/actions/encodings/offset.js +30 -29
  82. package/src/actions/encodings/parallel.js +2 -8
  83. package/src/actions/encodings/position/apply.js +63 -11
  84. package/src/actions/encodings/position/index.js +52 -6
  85. package/src/actions/encodings/position/policies/arc.js +14 -0
  86. package/src/actions/encodings/position/policies/area.js +4 -42
  87. package/src/actions/encodings/position/policies/bar.js +57 -13
  88. package/src/actions/encodings/position/resolve.js +88 -26
  89. package/src/actions/encodings/ranged.js +123 -97
  90. package/src/actions/encodings/remove.js +44 -34
  91. package/src/actions/encodings/ruleAppearance.js +24 -32
  92. package/src/actions/encodings/seriesLayout.js +105 -0
  93. package/src/actions/encodings/shared.js +107 -37
  94. package/src/actions/encodings/stroke.js +226 -0
  95. package/src/actions/encodings/strokeDash.js +9 -6
  96. package/src/actions/encodings/temporal.js +7 -0
  97. package/src/actions/encodings/text.js +30 -15
  98. package/src/actions/errorBands/create.js +41 -8
  99. package/src/actions/errorBands/edit.js +360 -25
  100. package/src/actions/errorBars/create.js +56 -17
  101. package/src/actions/errorBars/edit.js +372 -21
  102. package/src/actions/facets/actions.js +619 -41
  103. package/src/actions/facets/derive.js +139 -25
  104. package/src/actions/gradientPlots/components.js +43 -13
  105. package/src/actions/gradientPlots/edit.js +14 -4
  106. package/src/actions/gradientPlots/materialize.js +8 -8
  107. package/src/actions/gradientPlots/options.js +10 -2
  108. package/src/actions/guides/applicability.js +28 -19
  109. package/src/actions/guides/axes/axes.js +4 -3
  110. package/src/actions/guides/axes/axis.js +18 -18
  111. package/src/actions/guides/axes/components.js +43 -0
  112. package/src/actions/guides/axes/edit.js +16 -51
  113. package/src/actions/guides/axes/labels.js +184 -28
  114. package/src/actions/guides/axes/lines.js +13 -7
  115. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  116. package/src/actions/guides/axes/parallel/policy.js +137 -0
  117. package/src/actions/guides/axes/parallel/resolve.js +50 -25
  118. package/src/actions/guides/axes/parallel.js +93 -217
  119. package/src/actions/guides/axes/policy.js +21 -67
  120. package/src/actions/guides/axes/tickGroups.js +15 -8
  121. package/src/actions/guides/axes/ticks.js +11 -6
  122. package/src/actions/guides/axes/titles.js +32 -11
  123. package/src/actions/guides/basic.js +2 -0
  124. package/src/actions/guides/facade.js +106 -0
  125. package/src/actions/guides/facadeAxes.js +200 -0
  126. package/src/actions/guides/facadeLegend.js +63 -0
  127. package/src/actions/guides/grids/grid.js +37 -33
  128. package/src/actions/guides/grids/resolve.js +17 -4
  129. package/src/actions/guides/guides.js +54 -31
  130. package/src/actions/guides/legends/blocks.js +426 -0
  131. package/src/actions/guides/legends/categorical/actions.js +309 -279
  132. package/src/actions/guides/legends/categorical/components.js +21 -17
  133. package/src/actions/guides/legends/categorical/index.js +37 -33
  134. package/src/actions/guides/legends/categorical/layout.js +123 -354
  135. package/src/actions/guides/legends/categorical/options.js +32 -16
  136. package/src/actions/guides/legends/categorical/recipes.js +114 -9
  137. package/src/actions/guides/legends/categorical/resolve.js +30 -15
  138. package/src/actions/guides/legends/categorical/symbols.js +87 -33
  139. package/src/actions/guides/legends/continuous/common.js +99 -26
  140. package/src/actions/guides/legends/continuous/gradient.js +120 -89
  141. package/src/actions/guides/legends/continuous/index.js +8 -2
  142. package/src/actions/guides/legends/continuous/interval.js +79 -95
  143. package/src/actions/guides/legends/continuous/opacity.js +113 -89
  144. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  145. package/src/actions/guides/legends/creation.js +40 -0
  146. package/src/actions/guides/legends/edit.js +321 -90
  147. package/src/actions/guides/legends/focused.js +2 -2
  148. package/src/actions/guides/legends/index.js +2 -0
  149. package/src/actions/guides/legends/lane.js +149 -102
  150. package/src/actions/guides/legends/lifecycle.js +143 -0
  151. package/src/actions/guides/legends/remove.js +43 -78
  152. package/src/actions/guides/legends/sampling.js +140 -0
  153. package/src/actions/guides/legends/size.js +213 -116
  154. package/src/actions/guides/legends/strokeWidth.js +121 -73
  155. package/src/actions/guides/legends/target.js +123 -0
  156. package/src/actions/guides/legends/transition.js +192 -0
  157. package/src/actions/guides/polar/axes/facade.js +81 -66
  158. package/src/actions/guides/polar/axes/labels.js +28 -8
  159. package/src/actions/guides/polar/axes/lines.js +12 -5
  160. package/src/actions/guides/polar/axes/shared.js +22 -0
  161. package/src/actions/guides/polar/axes/ticks.js +6 -5
  162. package/src/actions/guides/polar/axes/titles.js +6 -5
  163. package/src/actions/guides/polar/grids.js +8 -8
  164. package/src/actions/guides/polar/resolve.js +12 -8
  165. package/src/actions/guides/reuse.js +44 -0
  166. package/src/actions/guides/tickValues.js +19 -21
  167. package/src/actions/index.js +15 -2
  168. package/src/actions/intervals/resolve.js +35 -15
  169. package/src/actions/marks/arc/actions.js +78 -20
  170. package/src/actions/marks/area/actions.js +51 -12
  171. package/src/actions/marks/area/materialize.js +22 -3
  172. package/src/actions/marks/bar/create.js +11 -2
  173. package/src/actions/marks/bar/edit.js +20 -3
  174. package/src/actions/marks/bar/materialize.js +33 -9
  175. package/src/actions/marks/index.js +14 -0
  176. package/src/actions/marks/line/actions.js +71 -113
  177. package/src/actions/marks/line/materialize.js +57 -56
  178. package/src/actions/marks/point/create.js +8 -2
  179. package/src/actions/marks/point/edit.js +29 -21
  180. package/src/actions/marks/point/index.js +1 -0
  181. package/src/actions/marks/point/jitter.js +3 -0
  182. package/src/actions/marks/point/materialize.js +154 -16
  183. package/src/actions/marks/point/packing.js +68 -0
  184. package/src/actions/marks/rect/actions.js +33 -12
  185. package/src/actions/marks/references.js +658 -0
  186. package/src/actions/marks/remove.js +87 -4
  187. package/src/actions/marks/rule/actions.js +77 -9
  188. package/src/actions/marks/rule/appearance.js +26 -0
  189. package/src/actions/marks/shared.js +13 -13
  190. package/src/actions/marks/text/actions.js +296 -13
  191. package/src/actions/marks/text/index.js +4 -0
  192. package/src/actions/marks/text/layout.js +156 -8
  193. package/src/actions/marks/text/removal.js +173 -0
  194. package/src/actions/marks/text/selection.js +73 -0
  195. package/src/actions/marks/tick/actions.js +22 -5
  196. package/src/actions/primitives/createGraphics.js +3 -1
  197. package/src/actions/primitives/editGraphics.js +26 -0
  198. package/src/actions/primitives/semantic.js +3 -2
  199. package/src/actions/primitives/semanticAction.js +120 -10
  200. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  201. package/src/actions/primitives/semanticValidation/index.js +9 -1
  202. package/src/actions/primitives/semanticValidation/layer.js +23 -1
  203. package/src/actions/primitives/semanticValidation/scale.js +38 -8
  204. package/src/actions/regression/components.js +26 -11
  205. package/src/actions/regression/create.js +11 -3
  206. package/src/actions/regression/edit.js +40 -13
  207. package/src/actions/regression/resolve.js +1 -1
  208. package/src/actions/resources/index.js +7 -0
  209. package/src/actions/resources/remove.js +172 -0
  210. package/src/actions/scales/channels.js +212 -0
  211. package/src/actions/scales/consumers/common.js +21 -54
  212. package/src/actions/scales/consumers/families.js +28 -14
  213. package/src/actions/scales/consumers/index.js +42 -4
  214. package/src/actions/scales/consumers/seriesLayout.js +57 -12
  215. package/src/actions/scales/create.js +58 -26
  216. package/src/actions/scales/definitions.js +109 -39
  217. package/src/actions/scales/edit.js +101 -34
  218. package/src/actions/scales/editPolicy.js +95 -35
  219. package/src/actions/scales/index.js +6 -0
  220. package/src/actions/scales/materialize.js +14 -49
  221. package/src/actions/scales/offset.js +48 -0
  222. package/src/actions/scales/parallel.js +71 -0
  223. package/src/actions/scales/patch.js +1 -1
  224. package/src/actions/scales/preview.js +140 -0
  225. package/src/actions/scales/quantitativeColor.js +9 -5
  226. package/src/actions/selection/actions.js +154 -30
  227. package/src/actions/selection/index.js +3 -0
  228. package/src/actions/theme/actions.js +98 -0
  229. package/src/actions/theme/composition.js +238 -0
  230. package/src/actions/theme/index.js +16 -0
  231. package/src/actions/theme/reconcile.js +1130 -0
  232. package/src/actions/theme/state.js +160 -0
  233. package/src/actions/titles/actions.js +11 -10
  234. package/src/actions/titles/resolve.js +1 -40
  235. package/src/actions/violinPlots/create.js +90 -43
  236. package/src/actions/violinPlots/edit.js +192 -0
  237. package/src/actions/violinPlots/index.js +2 -0
  238. package/src/core/ChartProgram.js +7 -0
  239. package/src/core/action.js +32 -2
  240. package/src/core/compositionState.js +141 -8
  241. package/src/core/programState.js +20 -0
  242. package/src/core/resourceReferences.js +332 -0
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/grammar/aggregate.js +66 -23
  245. package/src/grammar/arcs.js +58 -1
  246. package/src/grammar/areaEndpoints.js +46 -0
  247. package/src/grammar/areaSeries.js +87 -41
  248. package/src/grammar/bars/aggregate.js +37 -13
  249. package/src/grammar/bars/geometry.js +44 -0
  250. package/src/grammar/bars/policy.js +9 -7
  251. package/src/grammar/bin.js +226 -0
  252. package/src/grammar/categoryOrder.js +34 -0
  253. package/src/grammar/complete.js +279 -0
  254. package/src/grammar/computed.js +351 -0
  255. package/src/grammar/coordinates.js +37 -0
  256. package/src/grammar/density.js +239 -37
  257. package/src/grammar/displayLabels.js +57 -0
  258. package/src/grammar/ecdf.js +198 -0
  259. package/src/grammar/facets/dependencies.js +133 -1
  260. package/src/grammar/facets/guides.js +3 -0
  261. package/src/grammar/facets/index.js +123 -52
  262. package/src/grammar/facets/scales.js +15 -18
  263. package/src/grammar/fold.js +123 -0
  264. package/src/grammar/horizon.js +6 -4
  265. package/src/grammar/impute.js +340 -0
  266. package/src/grammar/interval.js +37 -20
  267. package/src/grammar/lineSeries.js +20 -63
  268. package/src/grammar/markFilter.js +22 -4
  269. package/src/grammar/markLabelSelection.js +57 -0
  270. package/src/grammar/markLabels.js +110 -0
  271. package/src/grammar/normalize.js +366 -0
  272. package/src/grammar/parallelAxisTitles.js +16 -0
  273. package/src/grammar/pathSeries.js +109 -0
  274. package/src/grammar/pointPacking.js +183 -0
  275. package/src/grammar/pointShapes.js +13 -4
  276. package/src/grammar/polar.js +90 -8
  277. package/src/grammar/positionDatum.js +12 -0
  278. package/src/grammar/rects.js +9 -1
  279. package/src/grammar/regression/derive.js +4 -0
  280. package/src/grammar/regression/index.js +8 -2
  281. package/src/grammar/regression/models.js +22 -7
  282. package/src/grammar/regression/parameters.js +63 -13
  283. package/src/grammar/rotation.js +29 -0
  284. package/src/grammar/roundedRect.js +180 -0
  285. package/src/grammar/rules.js +8 -21
  286. package/src/grammar/scales/color.js +23 -1
  287. package/src/grammar/scales/colorConsumers.js +57 -0
  288. package/src/grammar/scales/continuous.js +2 -2
  289. package/src/grammar/scales/definition.js +42 -5
  290. package/src/grammar/scales/discretized.js +11 -5
  291. package/src/grammar/scales/fields.js +38 -8
  292. package/src/grammar/scales/index.js +1 -0
  293. package/src/grammar/scales/mapping.js +2 -0
  294. package/src/grammar/scales/ordinal.js +12 -3
  295. package/src/grammar/scales/policies.js +2 -2
  296. package/src/grammar/scales/radial.js +55 -0
  297. package/src/grammar/scales/size.js +456 -0
  298. package/src/grammar/scales/types.js +1 -0
  299. package/src/grammar/schemas/concreteGraphic.js +14 -2
  300. package/src/grammar/schemas/graphic.js +15 -0
  301. package/src/grammar/schemas/graphicBounds.js +179 -30
  302. package/src/grammar/schemas/semanticPath.js +23 -4
  303. package/src/grammar/stack.js +170 -0
  304. package/src/grammar/statisticalReference.js +103 -0
  305. package/src/grammar/statistics/confidenceInterval.js +101 -0
  306. package/src/grammar/strokeStyle.js +77 -0
  307. package/src/grammar/summary.js +245 -0
  308. package/src/grammar/text.js +22 -16
  309. package/src/grammar/timeUnit.js +234 -13
  310. package/src/grammar/transformTopology.js +10 -0
  311. package/src/grammar/transforms.js +418 -16
  312. package/src/grammar/valueFormat.js +112 -0
  313. package/src/grammar/weightedStatistics.js +374 -0
  314. package/src/grammar/window.js +232 -21
  315. package/src/layout/aspect.js +80 -0
  316. package/src/layout/facets.js +100 -12
  317. package/src/layout/guideCollisions.js +12 -0
  318. package/src/layout/labels.js +343 -0
  319. package/src/layout/legend.js +2 -10
  320. package/src/layout/legendItems.js +102 -0
  321. package/src/layout/legendLane.js +100 -11
  322. package/src/materialization/bars/aggregate.js +56 -147
  323. package/src/materialization/bars/histogram.js +104 -20
  324. package/src/materialization/bars/ranged.js +19 -3
  325. package/src/materialization/bars/resolve.js +7 -4
  326. package/src/materialization/categorySlotOffset.js +40 -0
  327. package/src/materialization/composition.js +3 -1
  328. package/src/materialization/compositionSnapshot.js +1 -1
  329. package/src/materialization/coordinateBounds.js +204 -0
  330. package/src/materialization/dependencies.js +93 -16
  331. package/src/materialization/encodings.js +1 -1
  332. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  333. package/src/materialization/facetGuides/placement.js +8 -2
  334. package/src/materialization/facetGuides/preparation.js +12 -7
  335. package/src/materialization/facetHeaders.js +346 -0
  336. package/src/materialization/facets.js +23 -66
  337. package/src/materialization/guides/layout.js +57 -0
  338. package/src/materialization/guides/resources.js +28 -2
  339. package/src/materialization/legends.js +11 -2
  340. package/src/materialization/marks/capabilities.js +20 -5
  341. package/src/materialization/marks/grouping.js +52 -0
  342. package/src/materialization/marks/index.js +68 -27
  343. package/src/materialization/marks/policies.js +4 -1
  344. package/src/materialization/rect.js +55 -42
  345. package/src/materialization/rectConfig.js +5 -1
  346. package/src/materialization/rowEncoding.js +13 -13
  347. package/src/materialization/scaleGuideDependencies.js +8 -5
  348. package/src/materialization/scales/map.js +6 -3
  349. package/src/materialization/scales/policies/arc.js +42 -0
  350. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  351. package/src/materialization/scales/policies/offset.js +50 -8
  352. package/src/materialization/scales/policies/series.js +5 -1
  353. package/src/materialization/scales/resolve.js +65 -17
  354. package/src/materialization/selection/compatibility.js +11 -0
  355. package/src/materialization/selection/items/arc.js +28 -4
  356. package/src/materialization/selection/items/bar.js +42 -71
  357. package/src/materialization/selection/items/common.js +27 -6
  358. package/src/materialization/selection/items/path.js +3 -13
  359. package/src/materialization/selection/items/rect.js +35 -8
  360. package/src/materialization/selection/path.js +1 -1
  361. package/src/materialization/selection/point.js +3 -1
  362. package/src/materialization/text.js +324 -14
  363. package/src/mcp/adapter.js +2 -2
  364. package/src/renderers/canvas/circle.js +2 -3
  365. package/src/renderers/canvas/line.js +2 -3
  366. package/src/renderers/canvas/native.js +4 -4
  367. package/src/renderers/canvas/path.js +2 -3
  368. package/src/renderers/canvas/rect.js +2 -3
  369. package/src/renderers/canvas/stroke.js +11 -0
  370. package/src/renderers/svg.js +19 -0
  371. package/src/selectors/datasets.js +44 -0
  372. package/src/selectors/layers.js +7 -2
  373. package/src/selectors/scales.js +6 -0
  374. package/src/theme/defaults.js +140 -0
  375. package/types/basic.d.ts +17 -2
  376. package/types/index.d.ts +211 -0
  377. package/types/program.d.ts +2182 -204
  378. package/src/materialization/bars/grouped.js +0 -144
@@ -1,12 +1,18 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { freezeOwned, isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
3
4
  import {
4
5
  validateNonEmptyString,
5
6
  validateNonNegativeFinite,
6
7
  validateOptionObject,
7
8
  validatePositiveFinite
8
9
  } from "../../core/validation.js";
9
- import { resolveFacetDefinition } from "../../grammar/facets/index.js";
10
+ import {
11
+ resolveFacetDefinition,
12
+ resolveFacetGridDefinition
13
+ } from "../../grammar/facets/index.js";
14
+ import { collectFacetScaleBindings, resolveFacetFamily } from
15
+ "../../grammar/facets/dependencies.js";
10
16
  import {
11
17
  FACET_SCALE_CHANNELS,
12
18
  normalizeFacetScalePolicies
@@ -14,27 +20,43 @@ import {
14
20
  import { resolveFacetLayout } from "../../layout/facets.js";
15
21
  import { compositionChildDescriptor } from
16
22
  "../../materialization/composition.js";
17
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
18
23
  import { deriveFacetChildren } from "./derive.js";
24
+ import { resolveFacetChildrenScales } from "./derive.js";
19
25
  import { replayDerivedData } from "./replay.js";
20
26
  import { composeFacetGuides } from "./guides.js";
21
27
  import { applyCompositionState } from "../composition/actions.js";
28
+ import { replayCompositionThemeState } from "../theme/composition.js";
29
+ import { findDataset } from "../../selectors/datasets.js";
30
+ import { findLayer } from "../../selectors/layers.js";
31
+ import { findSemanticScale } from "../../selectors/scales.js";
32
+ import { normalizeDisplayLabelMap } from "../../grammar/displayLabels.js";
33
+ import {
34
+ createDefaultFacetHeaders,
35
+ normalizeFacetHeadersConfig,
36
+ resolveFacetHeaderConfig
37
+ } from "../../materialization/facetHeaders.js";
22
38
 
23
39
  const FACET_OPTIONS = Object.freeze([
24
- "id", "field", "data", "columns", "gap", "align", "padding", "scales",
40
+ "id", "field", "data", "values", "columns", "gap", "align", "padding", "scales",
25
41
  "guides"
26
42
  ]);
43
+ const FACET_GRID_OPTIONS = Object.freeze([
44
+ "id", "data", "rows", "columns", "combinations", "gap", "align",
45
+ "padding", "scales", "guides"
46
+ ]);
47
+ const REPEAT_OPTIONS = Object.freeze([
48
+ "id", "target", "channel", "fields", "columns", "gap", "align",
49
+ "padding", "scales", "guides"
50
+ ]);
51
+ const SOURCE_EDIT_OPTIONS = Object.freeze(["program"]);
27
52
  const GUIDE_OPTIONS = Object.freeze(["axes", "legend"]);
28
53
  const HEADER_OPTIONS = Object.freeze([
29
- "fontSize", "fontFamily", "fontWeight", "color", "offset"
54
+ "fontSize", "fontFamily", "fontWeight", "color", "offset", "role",
55
+ "labelMap", "side", "align"
56
+ ]);
57
+ const HEADER_STYLE_OPTIONS = Object.freeze([
58
+ "fontSize", "fontFamily", "fontWeight", "color", "offset", "align"
30
59
  ]);
31
- const DEFAULT_HEADERS = Object.freeze({
32
- fontSize: 12,
33
- fontFamily: DEFAULT_FONT_FAMILY,
34
- fontWeight: 600,
35
- color: DEFAULT_COLORS.strongText,
36
- offset: 10
37
- });
38
60
 
39
61
  function normalizeGuides(guides) {
40
62
  if (guides === undefined) return { axes: "each", legend: false };
@@ -57,18 +79,38 @@ function requireFacetProgram(program, operation) {
57
79
  }
58
80
  }
59
81
 
82
+ function usedFacetScalePolicies(program, policies) {
83
+ const used = new Set(
84
+ collectFacetScaleBindings(program.semanticSpec).map(binding => binding.policyKey)
85
+ );
86
+ return Object.fromEntries(
87
+ FACET_SCALE_CHANNELS.flatMap(channel =>
88
+ used.has(channel) ? [[channel, policies[channel]]] : []
89
+ )
90
+ );
91
+ }
92
+
60
93
  function facetUnitTemplate(program) {
61
- const seed = program.children[program.compositionSpec.children[0]];
94
+ const seedId = program.compositionSpec.children.find(id =>
95
+ program.children[id]?.semanticSpec.layers.length > 0
96
+ ) ?? program.compositionSpec.children[0];
97
+ const seed = program.children[seedId];
62
98
  if (seed === undefined) {
63
99
  throw new Error(`Facet "${program.compositionSpec.id}" requires a retained child.`);
64
100
  }
65
- const { facets: _facets, ...unitConfigs } = program.materializationConfigs;
101
+ const { facets: _facets, guides: _guides, ...unitConfigs } = program.materializationConfigs;
66
102
  return new program.constructor({
67
103
  semanticSpec: program.semanticSpec,
68
104
  graphicSpec: seed.graphicSpec,
69
105
  resolvedScales: program.resolvedScales,
70
106
  materializationConfigs: freezeOwned({
71
107
  ...unitConfigs,
108
+ ...(seed.materializationConfigs.theme === undefined
109
+ ? {}
110
+ : { theme: seed.materializationConfigs.theme }),
111
+ ...(seed.materializationConfigs.guides === undefined
112
+ ? {}
113
+ : { guides: seed.materializationConfigs.guides }),
72
114
  canvas: seed.materializationConfigs.canvas
73
115
  }),
74
116
  children: {},
@@ -81,19 +123,44 @@ function facetUnitTemplate(program) {
81
123
 
82
124
  function rederiveFacet(program, { scales, guides }) {
83
125
  const current = program.compositionSpec;
84
- const definition = resolveFacetDefinition(program.semanticSpec, {
85
- id: current.id,
86
- data: current.facet.data,
87
- field: current.facet.field,
88
- values: current.facet.values
89
- });
90
- const request = Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
91
- program.semanticSpec.layers.some(
92
- layer => layer.encoding?.[channel]?.scale !== undefined
93
- ) ? [[channel, scales[channel]]] : []
94
- ));
126
+ if (current.facet.repeat !== undefined) {
127
+ const template = facetUnitTemplate(program);
128
+ const definition = resolveRepeatDefinition(template, {
129
+ id: current.id,
130
+ ...current.facet.repeat
131
+ });
132
+ const derived = deriveRepeatChildren(
133
+ template,
134
+ definition,
135
+ usedFacetScalePolicies(template, scales),
136
+ true
137
+ );
138
+ return applyCompositionState(program, {
139
+ children: derived.children,
140
+ compositionSpec: {
141
+ ...current,
142
+ facet: { ...current.facet, scales, guides }
143
+ }
144
+ }, current.children);
145
+ }
146
+ const definition = current.facet.grid === undefined
147
+ ? resolveFacetDefinition(program.semanticSpec, {
148
+ id: current.id,
149
+ data: current.facet.data,
150
+ field: current.facet.field,
151
+ values: current.facet.values
152
+ })
153
+ : resolveFacetGridDefinition(program.semanticSpec, {
154
+ id: current.id,
155
+ data: current.facet.data,
156
+ rows: current.facet.grid.rows,
157
+ columns: current.facet.grid.columns,
158
+ combinations: current.facet.grid.combinations
159
+ });
160
+ const request = usedFacetScalePolicies(program, scales);
95
161
  const normalized = normalizeFacetScalePolicies(program.semanticSpec, request);
96
- const derived = deriveFacetChildren(facetUnitTemplate(program), definition, {
162
+ const template = facetUnitTemplate(program);
163
+ const derived = deriveFacetChildren(template, definition, {
97
164
  closeInheritedAction: true,
98
165
  stripTitle: true,
99
166
  scales: request
@@ -106,7 +173,11 @@ function rederiveFacet(program, { scales, guides }) {
106
173
  guides
107
174
  }
108
175
  };
109
- return applyCompositionState(program, {
176
+ let parent = program._withoutMaterializationConfig(["guides", "legend"]);
177
+ for (const [kind, config] of Object.entries(template.guideConfigs.legend ?? {})) {
178
+ parent = parent._withLegendConfig(kind, config);
179
+ }
180
+ return applyCompositionState(parent, {
110
181
  children: derived.children,
111
182
  compositionSpec
112
183
  }, compositionSpec.children);
@@ -121,6 +192,9 @@ export const facet = action(
121
192
  validateOptionObject(args, FACET_OPTIONS, "facet");
122
193
  const guides = normalizeGuides(args.guides);
123
194
  const definition = resolveFacetDefinition(this.semanticSpec, args);
195
+ if (definition.family !== "cartesian" && guides.axes === "outer") {
196
+ throw new Error("Polar and Parallel facets do not support outer axes.");
197
+ }
124
198
  const scalePolicies = normalizeFacetScalePolicies(
125
199
  this.semanticSpec,
126
200
  args.scales ?? {}
@@ -159,7 +233,7 @@ export const facet = action(
159
233
  };
160
234
  return applyCompositionState(
161
235
  this._withMaterializationConfig(["facets", definition.id], {
162
- headers: DEFAULT_HEADERS
236
+ headers: createDefaultFacetHeaders()
163
237
  }),
164
238
  {
165
239
  children: derived.children,
@@ -170,6 +244,327 @@ export const facet = action(
170
244
  }
171
245
  );
172
246
 
247
+ function resolveRepeatDefinition(program, args) {
248
+ if (!isPlainObject(args)) {
249
+ throw new TypeError("repeatCharts options must be a plain object.");
250
+ }
251
+ const id = validateUserId(args.id ?? "repeat", "Repeat id");
252
+ const parallelChannel = isPlainObject(args.channel);
253
+ if (parallelChannel) {
254
+ const keys = Object.keys(args.channel);
255
+ if (keys.length !== 1 || keys[0] !== "parallelDimension" ||
256
+ typeof args.channel.parallelDimension !== "string" ||
257
+ args.channel.parallelDimension.length === 0) {
258
+ throw new Error(
259
+ "repeatCharts Parallel channel must contain exactly one non-empty parallelDimension."
260
+ );
261
+ }
262
+ } else if (!["x", "y", "theta", "r"].includes(args.channel)) {
263
+ throw new Error(
264
+ 'repeatCharts channel must be "x", "y", "theta", "r", or one parallelDimension.'
265
+ );
266
+ }
267
+ if (!Array.isArray(args.fields) || args.fields.length === 0 ||
268
+ args.fields.some(field => typeof field !== "string" || field.length === 0)) {
269
+ throw new TypeError("repeatCharts fields must be a non-empty array of field names.");
270
+ }
271
+ if (new Set(args.fields).size !== args.fields.length) {
272
+ throw new Error("repeatCharts fields must be unique.");
273
+ }
274
+ const family = resolveFacetFamily(program.semanticSpec).family;
275
+ const semanticChannel = args.channel === "r" ? "radius" : args.channel;
276
+ const eligible = program.semanticSpec.layers.filter(layer => {
277
+ if (parallelChannel) {
278
+ const matches = (layer.encoding?.parallel?.dimensions ?? []).filter(
279
+ dimension => dimension.field === args.channel.parallelDimension
280
+ );
281
+ return family === "parallel" && layer.mark?.type === "line" && matches.length === 1;
282
+ }
283
+ if (["x", "y"].includes(args.channel)) {
284
+ return family === "cartesian" &&
285
+ layer.encoding?.x?.scale !== undefined &&
286
+ layer.encoding?.y?.scale !== undefined &&
287
+ layer.encoding?.[args.channel]?.field !== undefined &&
288
+ ["point", "line", "area", "bar", "rule", "tick", "rect"].includes(layer.mark?.type);
289
+ }
290
+ const role = program.markConfigs[layer.id]?.compositionRole;
291
+ if (family !== "polar" || ["pie", "radar"].includes(role)) return false;
292
+ return ["point", "line", "arc"].includes(layer.mark?.type) &&
293
+ layer.encoding?.[semanticChannel]?.field !== undefined;
294
+ });
295
+ let target;
296
+ if (args.target !== undefined) {
297
+ target = validateUserId(args.target, "Repeat target");
298
+ if (!eligible.some(layer => layer.id === target)) {
299
+ throw new Error(
300
+ `repeatCharts target "${target}" is not eligible for the requested field role.`
301
+ );
302
+ }
303
+ } else if (eligible.length === 1) {
304
+ target = eligible[0].id;
305
+ } else {
306
+ throw new Error(
307
+ eligible.length === 0
308
+ ? "repeatCharts requires one eligible complete mark for the requested field role."
309
+ : "repeatCharts target is ambiguous; provide target."
310
+ );
311
+ }
312
+ const unrelatedLayers = program.semanticSpec.layers.filter(layer => {
313
+ if (layer.id === target) return false;
314
+ const attachedLabel = layer.mark?.type === "text" && layer.source === target;
315
+ const statisticalReference = program.markConfigs[layer.id]
316
+ ?.statisticalReference?.source === target;
317
+ return !attachedLabel && !statisticalReference;
318
+ });
319
+ if (unrelatedLayers.length > 0) {
320
+ throw new Error("repeatCharts currently supports one direct mark only.");
321
+ }
322
+ const config = program.markConfigs[target] ?? {};
323
+ const composite = [
324
+ "boxPlot", "gradientPlot", "violinPlot", "regressionPlot", "endpointPlot",
325
+ "ecdfPlot", "raincloudPlot", "intervalPlot"
326
+ ].find(key => config[key] !== undefined);
327
+ if (composite !== undefined) {
328
+ throw new Error(`repeatCharts does not replace the ${composite} composite role.`);
329
+ }
330
+ const layer = findLayer(program, target);
331
+ const dataset = findDataset(program, layer.data);
332
+ if (dataset?.transform?.length > 0) {
333
+ throw new Error("repeatCharts does not rewrite a derived dataset dependency.");
334
+ }
335
+ const encoding = parallelChannel
336
+ ? layer.encoding.parallel
337
+ : layer.encoding[semanticChannel];
338
+ const dimensionIndex = parallelChannel
339
+ ? encoding.dimensions.findIndex(
340
+ dimension => dimension.field === args.channel.parallelDimension
341
+ )
342
+ : undefined;
343
+ return {
344
+ id,
345
+ target,
346
+ channel: parallelChannel ? { ...args.channel } : args.channel,
347
+ policyKey: parallelChannel ? "parallelDimensions" : args.channel,
348
+ fields: [...args.fields],
349
+ data: layer.data,
350
+ encoding,
351
+ ...(dimensionIndex === undefined ? {} : { dimensionIndex }),
352
+ cells: args.fields.map((field, index) => ({
353
+ id: `${id}-field-${index + 1}`,
354
+ field,
355
+ value: field
356
+ }))
357
+ };
358
+ }
359
+
360
+ function repeatEncodingArgs(definition, field) {
361
+ const encoding = definition.encoding;
362
+ return {
363
+ target: definition.target,
364
+ field,
365
+ fieldType: encoding.fieldType,
366
+ ...(encoding.temporalUnit === undefined ? {} : { temporalUnit: encoding.temporalUnit }),
367
+ ...(encoding.aggregate === undefined ? {} : { aggregate: encoding.aggregate }),
368
+ ...(encoding.bin === undefined ? {} : { bin: encoding.bin }),
369
+ ...(encoding.stack === undefined ? {} : { stack: encoding.stack }),
370
+ ...(encoding.weight === undefined ? {} : { weight: encoding.weight }),
371
+ scale: { id: encoding.scale }
372
+ };
373
+ }
374
+
375
+ function parallelRepeatDimensions(program, definition, field) {
376
+ return definition.encoding.dimensions.map((dimension, index) => {
377
+ const semanticScale = findSemanticScale(program, dimension.scale);
378
+ if (semanticScale === undefined) {
379
+ throw new Error(`repeatCharts Parallel dimension scale "${dimension.scale}" is missing.`);
380
+ }
381
+ const { id: _id, ...scale } = semanticScale;
382
+ void _id;
383
+ const replacement = index === definition.dimensionIndex;
384
+ return {
385
+ field: replacement ? field : dimension.field,
386
+ fieldType: dimension.fieldType,
387
+ title: replacement && dimension.title === dimension.field
388
+ ? field
389
+ : dimension.title,
390
+ scale
391
+ };
392
+ });
393
+ }
394
+
395
+ function deriveRepeatedProgram(template, definition, field) {
396
+ if (isPlainObject(definition.channel)) {
397
+ return template.encodeParallelCoordinates({
398
+ target: definition.target,
399
+ coordinate: findLayer(template, definition.target).coordinate,
400
+ dimensions: parallelRepeatDimensions(template, definition, field),
401
+ ...(definition.encoding.key === undefined ? {} : { key: definition.encoding.key }),
402
+ missing: definition.encoding.missing
403
+ });
404
+ }
405
+ const operation = {
406
+ x: "encodeX",
407
+ y: "encodeY",
408
+ theta: "encodeTheta",
409
+ r: "encodeR"
410
+ }[definition.channel];
411
+ return template[operation](repeatEncodingArgs(definition, field));
412
+ }
413
+
414
+ function deriveRepeatChildren(base, definition, scales, closeInheritedAction) {
415
+ const template = base.semanticSpec.title.text === undefined
416
+ ? base
417
+ : base.removeTitle();
418
+ const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
419
+ cell.id,
420
+ deriveRepeatedProgram(template, definition, cell.field)
421
+ ]));
422
+ return resolveFacetChildrenScales(
423
+ template,
424
+ definition.cells.map(cell => cell.id),
425
+ independentlyResolved,
426
+ scales,
427
+ closeInheritedAction
428
+ );
429
+ }
430
+
431
+ export const repeatCharts = action(
432
+ {
433
+ op: "repeatCharts",
434
+ description: "Repeat one direct chart across an ordered field-role list."
435
+ },
436
+ function (args = {}) {
437
+ validateOptionObject(args, REPEAT_OPTIONS, "repeatCharts");
438
+ const guides = normalizeGuides(args.guides);
439
+ if (guides.axes === "outer") {
440
+ throw new Error("repeatCharts does not promote axes across different repeated fields.");
441
+ }
442
+ const definition = resolveRepeatDefinition(this, args);
443
+ const requestedScales = {
444
+ ...(args.scales ?? {}),
445
+ [definition.policyKey]: args.scales?.[definition.policyKey] ?? "independent"
446
+ };
447
+ const scalePolicies = normalizeFacetScalePolicies(
448
+ this.semanticSpec,
449
+ requestedScales
450
+ );
451
+ const derived = deriveRepeatChildren(
452
+ this,
453
+ definition,
454
+ requestedScales,
455
+ true
456
+ );
457
+ const preflight = resolveFacetLayout({
458
+ children: definition.cells.map(cell => ({
459
+ ...compositionChildDescriptor(cell.id, derived.children[cell.id]),
460
+ value: cell.value
461
+ })),
462
+ ...(Object.hasOwn(args, "columns") ? { columns: args.columns } : {}),
463
+ ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
464
+ ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
465
+ ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {}),
466
+ sharedLegend: guides.legend === "shared"
467
+ });
468
+ const compositionSpec = {
469
+ id: definition.id,
470
+ type: "facet",
471
+ children: definition.cells.map(cell => cell.id),
472
+ columns: preflight.columns,
473
+ gap: preflight.gap,
474
+ align: preflight.align,
475
+ padding: preflight.padding,
476
+ facet: {
477
+ data: definition.data,
478
+ values: definition.fields,
479
+ repeat: {
480
+ target: definition.target,
481
+ channel: definition.channel,
482
+ fields: definition.fields
483
+ },
484
+ scales: scalePolicies.channels,
485
+ guides
486
+ }
487
+ };
488
+ return applyCompositionState(
489
+ this._withMaterializationConfig(["facets", definition.id], {
490
+ headers: createDefaultFacetHeaders()
491
+ }),
492
+ { children: derived.children, compositionSpec },
493
+ compositionSpec.children
494
+ );
495
+ }
496
+ );
497
+
498
+ export const facetGrid = action(
499
+ {
500
+ op: "facetGrid",
501
+ description: "Repeat one direct-source Cartesian chart across a row and column field grid."
502
+ },
503
+ function (args = {}) {
504
+ validateOptionObject(args, FACET_GRID_OPTIONS, "facetGrid");
505
+ const guides = normalizeGuides(args.guides);
506
+ const definition = resolveFacetGridDefinition(this.semanticSpec, args);
507
+ if (definition.family !== "cartesian" && guides.axes === "outer") {
508
+ throw new Error("Polar and Parallel facets do not support outer axes.");
509
+ }
510
+ const scalePolicies = normalizeFacetScalePolicies(
511
+ this.semanticSpec,
512
+ args.scales ?? {}
513
+ );
514
+ const derived = deriveFacetChildren(this, definition, {
515
+ closeInheritedAction: true,
516
+ stripTitle: true,
517
+ scales: args.scales ?? {}
518
+ });
519
+ const preflight = resolveFacetLayout({
520
+ children: definition.cells.map(cell => ({
521
+ ...compositionChildDescriptor(cell.id, derived.children[cell.id]),
522
+ value: cell.value,
523
+ row: cell.row,
524
+ column: cell.column
525
+ })),
526
+ columns: definition.grid.columns.values.length,
527
+ ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
528
+ ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
529
+ ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {}),
530
+ sharedLegend: guides.legend === "shared"
531
+ });
532
+ const compositionSpec = {
533
+ id: definition.id,
534
+ type: "facet",
535
+ children: definition.cells.map(cell => cell.id),
536
+ columns: preflight.columns,
537
+ gap: preflight.gap,
538
+ align: preflight.align,
539
+ padding: preflight.padding,
540
+ facet: {
541
+ data: definition.data,
542
+ values: definition.cells.map(cell => cell.value),
543
+ grid: {
544
+ ...definition.grid,
545
+ cells: definition.cells.map(cell => ({
546
+ id: cell.id,
547
+ row: cell.row,
548
+ column: cell.column,
549
+ rowValue: cell.rowValue,
550
+ columnValue: cell.columnValue,
551
+ empty: cell.empty
552
+ }))
553
+ },
554
+ scales: scalePolicies.channels,
555
+ guides
556
+ }
557
+ };
558
+ return applyCompositionState(
559
+ this._withMaterializationConfig(["facets", definition.id], {
560
+ headers: createDefaultFacetHeaders()
561
+ }),
562
+ { children: derived.children, compositionSpec },
563
+ compositionSpec.children
564
+ );
565
+ }
566
+ );
567
+
173
568
  export const editFacetHeaders = action(
174
569
  {
175
570
  op: "editFacetHeaders",
@@ -187,18 +582,73 @@ export const editFacetHeaders = action(
187
582
  allowEmpty: false,
188
583
  emptyMessage: "editFacetHeaders requires at least one change."
189
584
  });
190
- const headers = { ...config.headers, ...args };
191
- validatePositiveFinite(headers.fontSize, "Facet header fontSize");
192
- validateNonEmptyString(headers.fontFamily, "Facet header fontFamily");
193
- validateNonEmptyString(headers.color, "Facet header color");
194
- validateNonNegativeFinite(headers.offset, "Facet header offset");
195
- if (!(
196
- (typeof headers.fontWeight === "string" && headers.fontWeight.length > 0) ||
197
- Number.isFinite(headers.fontWeight)
198
- )) {
199
- throw new TypeError(
200
- "Facet header fontWeight must be a non-empty string or number."
201
- );
585
+ const role = args.role ?? "all";
586
+ if (!["all", "row", "column"].includes(role)) {
587
+ throw new Error(`Unknown facet header role "${role}".`);
588
+ }
589
+ if (role === "all" && Object.hasOwn(args, "side")) {
590
+ throw new Error("editFacetHeaders side requires an explicit row or column role.");
591
+ }
592
+ if (role === "row" && this.compositionSpec.facet.grid === undefined) {
593
+ throw new Error("editFacetHeaders row role requires a row-column facet grid.");
594
+ }
595
+ if (Object.hasOwn(args, "side")) {
596
+ const sides = role === "row" ? ["left", "right"] : ["top", "bottom"];
597
+ if (!sides.includes(args.side)) {
598
+ throw new Error(`editFacetHeaders ${role} side must be ${sides.join(" or ")}.`);
599
+ }
600
+ }
601
+ if (Object.hasOwn(args, "align") &&
602
+ !["start", "center", "end"].includes(args.align)) {
603
+ throw new Error("editFacetHeaders align must be start, center, or end.");
604
+ }
605
+ if (role === "all" && Object.keys(args).every(key => key === "role")) {
606
+ throw new Error("editFacetHeaders requires at least one change.");
607
+ }
608
+ const current = normalizeFacetHeadersConfig(config.headers);
609
+ const owner = role === "all" ? "common" : role;
610
+ const patch = { ...current[owner] };
611
+ for (const key of HEADER_STYLE_OPTIONS) {
612
+ if (Object.hasOwn(args, key)) patch[key] = args[key];
613
+ }
614
+ if (Object.hasOwn(args, "side")) patch.side = args.side;
615
+ if (Object.hasOwn(args, "labelMap")) {
616
+ if (args.labelMap === "auto") delete patch.labelMap;
617
+ else {
618
+ patch.labelMap = normalizeDisplayLabelMap(
619
+ args.labelMap,
620
+ `editFacetHeaders ${role} labelMap`
621
+ );
622
+ }
623
+ }
624
+ const headers = {
625
+ ...current,
626
+ ...(role === "all" ? {} : { mode: "roles" }),
627
+ [owner]: patch
628
+ };
629
+ for (const headerRole of ["all", "row", "column"]) {
630
+ const resolved = resolveFacetHeaderConfig(headers, headerRole);
631
+ validatePositiveFinite(resolved.fontSize, `Facet ${headerRole} header fontSize`);
632
+ validateNonEmptyString(resolved.fontFamily, `Facet ${headerRole} header fontFamily`);
633
+ validateNonEmptyString(resolved.color, `Facet ${headerRole} header color`);
634
+ validateNonNegativeFinite(resolved.offset, `Facet ${headerRole} header offset`);
635
+ if (!(
636
+ (typeof resolved.fontWeight === "string" && resolved.fontWeight.length > 0) ||
637
+ Number.isFinite(resolved.fontWeight)
638
+ )) {
639
+ throw new TypeError(
640
+ `Facet ${headerRole} header fontWeight must be a non-empty string or number.`
641
+ );
642
+ }
643
+ if (!["start", "center", "end"].includes(resolved.align)) {
644
+ throw new Error(`Facet ${headerRole} header align must be start, center, or end.`);
645
+ }
646
+ if (resolved.labelMap !== undefined) {
647
+ normalizeDisplayLabelMap(
648
+ resolved.labelMap,
649
+ `Facet ${headerRole} header labelMap`
650
+ );
651
+ }
202
652
  }
203
653
  return this
204
654
  ._withMaterializationConfig(["facets", id], { ...config, headers })
@@ -219,8 +669,8 @@ export const editFacetScales = action(
219
669
  emptyMessage: "editFacetScales requires at least one channel policy change."
220
670
  });
221
671
  for (const channel of Object.keys(args)) {
222
- if (!this.semanticSpec.layers.some(
223
- layer => layer.encoding?.[channel]?.scale !== undefined
672
+ if (!collectFacetScaleBindings(this.semanticSpec).some(
673
+ binding => binding.policyKey === channel
224
674
  )) {
225
675
  throw new Error(
226
676
  `Facet scale channel "${channel}" is not used by an affected layer.`
@@ -255,6 +705,13 @@ export const editFacetGuides = action(
255
705
  });
256
706
  const current = this.compositionSpec.facet;
257
707
  const guides = normalizeGuides({ ...current.guides, ...args });
708
+ const family = resolveFacetFamily(this.semanticSpec).family;
709
+ if (family !== "cartesian" && guides.axes === "outer") {
710
+ throw new Error("Polar and Parallel facets do not support outer axes.");
711
+ }
712
+ if (current.repeat !== undefined && guides.axes === "outer") {
713
+ throw new Error("repeatCharts does not promote axes across different repeated fields.");
714
+ }
258
715
  return rederiveFacet(this, {
259
716
  scales: current.scales,
260
717
  guides
@@ -262,11 +719,132 @@ export const editFacetGuides = action(
262
719
  }
263
720
  );
264
721
 
722
+ function adoptUnitState(program, actionOwner) {
723
+ if (!(program instanceof actionOwner.constructor)) {
724
+ throw new TypeError("editFacetSource program must be a ChartProgram.");
725
+ }
726
+ if (program.compositionSpec !== undefined) {
727
+ throw new Error("editFacetSource program must be a complete unit ChartProgram.");
728
+ }
729
+ if (program.actionStack.length !== 0) {
730
+ throw new Error("editFacetSource program has an unfinished action stack.");
731
+ }
732
+ const seedId = actionOwner.compositionSpec.children.find(id =>
733
+ actionOwner.children[id]?.semanticSpec.layers.length > 0
734
+ ) ?? actionOwner.compositionSpec.children[0];
735
+ const retained = actionOwner.children[seedId]?.materializationConfigs ?? {};
736
+ const {
737
+ canvas: _retainedCanvas,
738
+ theme: _retainedTheme,
739
+ facets: _retainedFacets,
740
+ ...retainedRecipe
741
+ } = retained;
742
+ void _retainedCanvas;
743
+ void _retainedTheme;
744
+ void _retainedFacets;
745
+ return new actionOwner.constructor({
746
+ semanticSpec: program.semanticSpec,
747
+ graphicSpec: program.graphicSpec,
748
+ resolvedScales: program.resolvedScales,
749
+ materializationConfigs: freezeOwned({
750
+ ...program.materializationConfigs,
751
+ ...retainedRecipe,
752
+ ...(program.materializationConfigs.canvas === undefined
753
+ ? {}
754
+ : { canvas: program.materializationConfigs.canvas }),
755
+ ...(program.materializationConfigs.theme === undefined
756
+ ? {}
757
+ : { theme: program.materializationConfigs.theme })
758
+ }),
759
+ children: {},
760
+ context: program.context,
761
+ trace: actionOwner.trace,
762
+ actionStack: actionOwner.actionStack,
763
+ actionSequence: actionOwner._actionSequence
764
+ });
765
+ }
766
+
767
+ export const editFacetSource = action(
768
+ {
769
+ op: "editFacetSource",
770
+ description: "Reapply one facet, grid, or repeat recipe to a revised complete unit program.",
771
+ scope: "composition"
772
+ },
773
+ function (args = {}) {
774
+ validateOptionObject(args, SOURCE_EDIT_OPTIONS, "editFacetSource");
775
+ requireFacetProgram(this, "editFacetSource");
776
+ const current = this.compositionSpec;
777
+ const facetConfig = this.materializationConfigs.facets?.[current.id];
778
+ const base = adoptUnitState(args.program, this);
779
+ let revised;
780
+ const scales = usedFacetScalePolicies(base, current.facet.scales);
781
+ if (current.facet.grid !== undefined) {
782
+ revised = base.facetGrid({
783
+ id: current.id,
784
+ data: current.facet.data,
785
+ rows: current.facet.grid.rows,
786
+ columns: current.facet.grid.columns,
787
+ combinations: current.facet.grid.combinations,
788
+ gap: current.gap,
789
+ align: current.align,
790
+ padding: current.padding,
791
+ scales,
792
+ guides: current.facet.guides
793
+ });
794
+ } else if (current.facet.repeat !== undefined) {
795
+ revised = base.repeatCharts({
796
+ id: current.id,
797
+ ...current.facet.repeat,
798
+ columns: current.columns,
799
+ gap: current.gap,
800
+ align: current.align,
801
+ padding: current.padding,
802
+ scales,
803
+ guides: current.facet.guides
804
+ });
805
+ } else {
806
+ revised = base.facet({
807
+ id: current.id,
808
+ data: current.facet.data,
809
+ field: current.facet.field,
810
+ values: current.facet.values,
811
+ columns: current.columns,
812
+ gap: current.gap,
813
+ align: current.align,
814
+ padding: current.padding,
815
+ scales,
816
+ guides: current.facet.guides
817
+ });
818
+ }
819
+ if (facetConfig !== undefined) {
820
+ revised = revised
821
+ ._withMaterializationConfig(["facets", current.id], facetConfig)
822
+ .materializeComposition();
823
+ }
824
+ if (revised.semanticSpec.title.text !== undefined) {
825
+ revised = revised.removeTitle();
826
+ }
827
+ if (this.semanticSpec.title.text !== undefined) {
828
+ revised = revised.createTitle({
829
+ ...this.titleConfig,
830
+ ...this.semanticSpec.title
831
+ });
832
+ }
833
+ return replayCompositionThemeState(
834
+ revised,
835
+ this.materializationConfigs.theme
836
+ );
837
+ }
838
+ );
839
+
265
840
  export function registerFacetActions(ProgramClass) {
266
841
  ProgramClass.prototype.replayDerivedData = replayDerivedData;
267
842
  ProgramClass.prototype.composeFacetGuides = composeFacetGuides;
268
843
  ProgramClass.prototype.facet = facet;
844
+ ProgramClass.prototype.facetGrid = facetGrid;
845
+ ProgramClass.prototype.repeatCharts = repeatCharts;
269
846
  ProgramClass.prototype.editFacetHeaders = editFacetHeaders;
270
847
  ProgramClass.prototype.editFacetScales = editFacetScales;
271
848
  ProgramClass.prototype.editFacetGuides = editFacetGuides;
849
+ ProgramClass.prototype.editFacetSource = editFacetSource;
272
850
  }