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
@@ -15,13 +15,22 @@ import {
15
15
  getSourceDependentMarkSteps
16
16
  } from "../../materialization/marks/index.js";
17
17
  import { findLayer } from "../../selectors/layers.js";
18
- import { removeLegendKinds } from "../guides/legends/remove.js";
18
+ import {
19
+ removeLegendKinds,
20
+ resolveCategoricalLegendRevision,
21
+ createCategoricalLegendFromConfig
22
+ } from "../guides/legends/lifecycle.js";
19
23
  import { clearMarkGraphic } from "./shared.js";
24
+ import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
25
+ import { findDataset } from "../../selectors/datasets.js";
26
+ import { assertPathGroupCompatible, validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
27
+ import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
28
+ import { planLegendBlockTransitions } from "../guides/legends/transition.js";
20
29
 
21
30
  const OPTIONS = Object.freeze(["target", "channel"]);
22
31
  const REMOVABLE_CHANNELS = Object.freeze([
23
32
  "x", "y", "x2", "y2", "xOffset", "yOffset", "theta", "radius",
24
- "color", "strokeDash", "strokeWidth", "size", "shape", "group",
33
+ "color", "stroke", "strokeDash", "strokeWidth", "size", "shape", "group",
25
34
  "angle", "opacity", "text"
26
35
  ]);
27
36
  const REMOVE_AXIS = Object.freeze({
@@ -32,14 +41,11 @@ const REMOVE_AXIS = Object.freeze({
32
41
  });
33
42
  const SPECIALIZED_LEGEND_KIND = Object.freeze({
34
43
  color: Object.freeze(["gradient", "interval"]),
44
+ stroke: Object.freeze(["strokeGradient", "strokeInterval"]),
35
45
  size: Object.freeze(["size"]),
36
46
  opacity: Object.freeze(["opacity"]),
37
47
  strokeWidth: Object.freeze(["strokeWidth"])
38
48
  });
39
- const LEGEND_OPTION_KEYS = Object.freeze([
40
- "position", "align", "direction", "columns", "offset", "titlePosition",
41
- "title", "symbol", "labels", "titleStyle", "itemGap", "border"
42
- ]);
43
49
 
44
50
  function resolveTarget(program, requested, channel) {
45
51
  const candidates = program.semanticSpec.layers.filter(
@@ -77,7 +83,7 @@ function activeCascade(layer, channel) {
77
83
  channels.add("y2");
78
84
  channels.add("yOffset");
79
85
  }
80
- if (channel === "color" && layer.mark?.type === "bar") {
86
+ if (channel === "color" && layer.mark?.type === "bar" && layer.layout === undefined) {
81
87
  if (resolveBarColorLayout(layer) === "group") {
82
88
  channels.add(resolveBarOffsetChannel(layer));
83
89
  }
@@ -85,6 +91,7 @@ function activeCascade(layer, channel) {
85
91
  if (
86
92
  channel === "group" &&
87
93
  layer.mark?.type === "area" &&
94
+ layer.layout === undefined &&
88
95
  layer.encoding?.color?.field !== undefined &&
89
96
  layer.encoding.color.field === layer.encoding.group?.field
90
97
  ) {
@@ -93,21 +100,6 @@ function activeCascade(layer, channel) {
93
100
  return [...channels].filter(active => layer.encoding?.[active] !== undefined);
94
101
  }
95
102
 
96
- function assertSelectionCompatibility(program, target, channels) {
97
- for (const [id, selection] of Object.entries(
98
- program.materializationConfigs.selections ?? {}
99
- )) {
100
- if (
101
- selection.target === target &&
102
- channels.includes(selection.selector?.channel)
103
- ) {
104
- throw new Error(
105
- `Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
106
- );
107
- }
108
- }
109
- }
110
-
111
103
  function removeChannelConfigs(program, target, channels) {
112
104
  let next = program;
113
105
  for (const channel of channels) {
@@ -167,7 +159,7 @@ function cleanupPositionGuides(program, removed) {
167
159
  }
168
160
 
169
161
  function reconcileCategoricalLegend(program, target, channels) {
170
- const entry = ["series", "color"]
162
+ const entry = ["series", "color", "stroke"]
171
163
  .map(kind => [kind, program.guideConfigs.legend?.[kind]])
172
164
  .find(([, config]) =>
173
165
  config?.target === target &&
@@ -182,16 +174,12 @@ function reconcileCategoricalLegend(program, target, channels) {
182
174
  if (remaining.length === 0) {
183
175
  return removeLegendKinds(program, [kind]);
184
176
  }
177
+ const resolved = resolveCategoricalLegendRevision(program, kind, config, remaining);
178
+ const revision = planLegendBlockTransitions(program, target, [
179
+ { ...resolved, kind: resolved.config.kind }
180
+ ])[0];
185
181
  const next = removeLegendKinds(program, [kind]);
186
- const options = { target, channels: remaining };
187
- for (const property of LEGEND_OPTION_KEYS) {
188
- if (
189
- (property === "columns" && config[property] === undefined) ||
190
- (property === "title" && config.inferredTitle)
191
- ) continue;
192
- options[property] = config[property];
193
- }
194
- return next.createLegend(options);
182
+ return createCategoricalLegendFromConfig(next, revision.config, revision.order);
195
183
  }
196
184
 
197
185
  function cleanupLegends(program, target, channels) {
@@ -228,8 +216,30 @@ export const removeEncoding = action(
228
216
  );
229
217
  }
230
218
  const layer = resolveTarget(this, args.target, args.channel);
219
+ if (args.channel === "group" && layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Remove active series layout before removing its group.");
220
+ if (args.channel === "theta" && layer.mark.type === "arc" &&
221
+ ["count", "sum"].includes(layer.encoding?.radius?.aggregate)) {
222
+ throw new Error("Remove measured radius before removing its category theta encoding.");
223
+ }
231
224
  const channels = activeCascade(layer, args.channel);
232
- assertSelectionCompatibility(this, layer.id, channels);
225
+ assertEncodingSelectionCompatibility(this, layer.id, channels);
226
+ for (const kind of ["series", "color", "stroke"]) {
227
+ const config = this.guideConfigs.legend?.[kind];
228
+ const order = this.semanticSpec.guides.legend?.[kind]?.order;
229
+ if (config?.target === layer.id && channels.includes(order?.channel) &&
230
+ config.channels.some(channel => !channels.includes(channel))) {
231
+ throw new Error('Reset linked legend order to "scale" before removing its position encoding.');
232
+ }
233
+ }
234
+ if (["line", "area"].includes(layer.mark?.type)) {
235
+ const dataset = findDataset(this, layer.data);
236
+ if (args.channel === "group") {
237
+ assertPathGroupCompatible(this, layer, dataset, undefined);
238
+ }
239
+ const encoding = { ...layer.encoding };
240
+ for (const channel of channels) delete encoding[channel];
241
+ validatePathGroupAppearance(this, { ...layer, encoding }, dataset);
242
+ }
233
243
  const removedPositions = channels
234
244
  .filter(channel => POSITION_ENCODING_CHANNELS.includes(channel))
235
245
  .map(channel => ({
@@ -249,7 +259,7 @@ export const removeEncoding = action(
249
259
  next = restoreBarBaseline(next, layer, channels);
250
260
  next = cleanupLegends(next, layer.id, channels);
251
261
  next = cleanupPositionGuides(next, removedPositions);
252
- return rematerializeTarget(next, layer.id);
262
+ return applyDetachedScaleRematerialization(rematerializeTarget(next, layer.id), [layer]);
253
263
  }
254
264
  );
255
265
 
@@ -1,12 +1,10 @@
1
1
  import { action } from "../../core/action.js";
2
- import {
3
- validateRuleStroke,
4
- validateRuleStrokeWidth
5
- } from "../../grammar/ruleAppearance.js";
2
+ import { validateRuleStrokeWidth } from "../../grammar/ruleAppearance.js";
6
3
  import { readQuantitativeField } from "../../grammar/scales/index.js";
7
4
  import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
8
5
  import {
9
6
  applyEncodingScale,
7
+ applyDetachedScaleRematerialization,
10
8
  rematerializeEncoding,
11
9
  resolveReassignmentScaleOptions,
12
10
  resolveTarget,
@@ -14,33 +12,17 @@ import {
14
12
  validateOptions
15
13
  } from "./shared.js";
16
14
  import { findLayer } from "../../selectors/layers.js";
15
+ import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
16
+ import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
17
17
 
18
- const STROKE_OPTIONS = Object.freeze(["target", "value"]);
19
18
  const WIDTH_OPTIONS = Object.freeze([
20
19
  "target", "value", "field", "fieldType", "scale"
21
20
  ]);
22
21
 
23
- const encodeStroke = action(
24
- {
25
- op: "encodeStroke",
26
- description: "Set a constant graphical stroke on a rule mark."
27
- },
28
- function (args = {}) {
29
- validateOptions(args, STROKE_OPTIONS, "encodeStroke");
30
- const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
31
- return this
32
- ._withMarkConfig(id, {
33
- ...this.markConfigs[id],
34
- stroke: validateRuleStroke(args.value)
35
- })
36
- .rematerializeRuleMark({ id });
37
- }
38
- );
39
-
40
22
  const encodeStrokeWidth = action(
41
23
  {
42
24
  op: "encodeStrokeWidth",
43
- description: "Set a constant graphical stroke width on a rule mark."
25
+ description: "Assign constant or field-driven line or rule stroke width."
44
26
  },
45
27
  function (args = {}) {
46
28
  validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
@@ -50,9 +32,14 @@ const encodeStrokeWidth = action(
50
32
  throw new Error("encodeStrokeWidth requires exactly one of value or field.");
51
33
  }
52
34
  if (hasValue) {
53
- const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
35
+ if (args.fieldType !== undefined || args.scale !== undefined) {
36
+ throw new Error("Constant stroke width does not accept fieldType or scale.");
37
+ }
38
+ validateRuleStrokeWidth(args.value);
39
+ const { id, layer } = resolveTarget(this, args.target, ["rule", "line"], "rule or line mark");
40
+ assertEncodingSelectionCompatibility(this, id, ["strokeWidth"]);
54
41
  let next = this.guideConfigs.legend?.strokeWidth?.target === id
55
- ? this.removeLegend({ target: id })
42
+ ? this.removeLegend({ target: id, channels: ["strokeWidth"] })
56
43
  : this;
57
44
  if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
58
45
  next = next.editSemantic({
@@ -60,12 +47,15 @@ const encodeStrokeWidth = action(
60
47
  remove: true
61
48
  });
62
49
  }
63
- return next
64
- ._withMarkConfig(id, {
50
+ next = next._withMarkConfig(id, {
65
51
  ...next.markConfigs[id],
66
- strokeWidth: validateRuleStrokeWidth(args.value)
67
- })
68
- .rematerializeRuleMark({ id });
52
+ strokeWidth: args.value
53
+ });
54
+ if (layer.mark.type === "line") {
55
+ return rematerializeEncoding(next, id, "strokeWidth", undefined, layer);
56
+ }
57
+ next = next.rematerializeRuleMark({ id });
58
+ return applyDetachedScaleRematerialization(next, [layer]);
69
59
  }
70
60
  const { id, dataset, layer } = resolveTarget(
71
61
  this,
@@ -83,6 +73,9 @@ const encodeStrokeWidth = action(
83
73
  `encodeStrokeWidth field "${args.field}" cannot contain negative values.`
84
74
  );
85
75
  }
76
+ validatePathSeriesAppearance(dataset.values, {
77
+ ...layer, encoding: { ...layer.encoding, strokeWidth: { field: args.field, fieldType } }
78
+ });
86
79
  const previous = layer.encoding?.strokeWidth;
87
80
  const requestedScale = resolveReassignmentScaleOptions(
88
81
  previous,
@@ -100,11 +93,10 @@ const encodeStrokeWidth = action(
100
93
  next = applyEncodingScale(next, scale, requestedScale, {
101
94
  reassignment: previous?.scale === scale.id
102
95
  });
103
- return rematerializeEncoding(next, id, "strokeWidth", scale.id);
96
+ return rematerializeEncoding(next, id, "strokeWidth", scale.id, layer);
104
97
  }
105
98
  );
106
99
 
107
100
  export function registerRuleAppearanceEncodingActions(ProgramClass) {
108
- ProgramClass.prototype.encodeStroke = encodeStroke;
109
101
  ProgramClass.prototype.encodeStrokeWidth = encodeStrokeWidth;
110
102
  }
@@ -0,0 +1,105 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateColorLayout } from "../../grammar/seriesLayout.js";
3
+ import { deriveAreaSeries, deriveDensityAreaSeries, layoutDensityAreaSeries } from "../../grammar/areaSeries.js";
4
+ import { readSeriesIdentity } from "../../grammar/pathSeries.js";
5
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../../grammar/bars/policy.js";
6
+ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
7
+ import { findSemanticScale, hasSemanticScaleReferences } from "../../selectors/scales.js";
8
+ import { findUpstreamTransform } from "../../materialization/dataProvenance.js";
9
+ import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
10
+ import { resolveScalePreview } from "../scales/preview.js";
11
+ import { getPositionEncodingMaterializationSteps } from "../../materialization/marks/index.js";
12
+ import { resolveTarget, setEncodingProperties, validateOptions } from "./shared.js";
13
+
14
+ export function stackLayoutMode(stack) {
15
+ return stack === "normalize" ? "fill" : stack === "center" ? "center" : stack === null ? "overlay" : "stack";
16
+ }
17
+
18
+ export const layoutSeries = action({ op: "layoutSeries", description: "Assign series placement independently of color." }, function (args = {}) {
19
+ validateOptions(args, ["target", "mode"], "layoutSeries");
20
+ const mode = validateColorLayout(args.mode);
21
+ const { id: target, layer, dataset } = resolveTarget(this, args.target, ["bar", "area"], "series mark");
22
+ const encoding = Object.fromEntries(Object.entries(layer.encoding ?? {}).map(([key, value]) => [key, { ...value }]));
23
+ const group = encoding.group;
24
+ const color = encoding.color;
25
+ if (group === undefined && color?.field !== undefined && ["nominal", "ordinal"].includes(color.fieldType)) {
26
+ encoding.group = { field: color.field, fieldType: "nominal", inferredFrom: "color" };
27
+ }
28
+ for (const channel of ["x", "y"]) if (encoding[channel]) delete encoding[channel].stack;
29
+ if (color) delete color.layout;
30
+ const updated = { ...layer, layout: { mode }, encoding };
31
+ const density = findUpstreamTransform(this, dataset, "density");
32
+ const horizon = findUpstreamTransform(this, dataset, "horizon");
33
+ let endpoint, offset, offsetScale, offsetChannel;
34
+ if (layer.mark.type === "area") {
35
+ if (horizon !== undefined) throw new Error("Horizon owns its layout; layoutSeries is not supported.");
36
+ if (mode === "group") throw new Error("Area does not support group layout.");
37
+ if (density !== undefined) layoutDensityAreaSeries(deriveDensityAreaSeries(dataset.values, updated, density), mode);
38
+ else {
39
+ if (mode !== "overlay" && encoding.x2 === undefined && encoding.y2 === undefined && encoding.y?.fieldType === "quantitative") {
40
+ endpoint = { channel: "y2", datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
41
+ encoding.y2 = { datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
42
+ }
43
+ if (encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Area layout requires complete positions.");
44
+ deriveAreaSeries(dataset.values, updated);
45
+ }
46
+ } else {
47
+ const channels = resolveBarChannels(updated), grain = resolveBarGrain(updated);
48
+ if (grain === undefined || encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Series layout requires a complete bar.");
49
+ if (mode === "center") throw new Error("Centered bars are not supported.");
50
+ if (grain === BAR_GRAINS.ranged && mode !== "overlay") throw new Error("Ranged bars support only overlay layout.");
51
+ if (grain === BAR_GRAINS.aggregate) deriveBarAggregates(dataset.values, updated);
52
+ if (mode === "group" && grain === BAR_GRAINS.aggregate) {
53
+ if (encoding.group === undefined) throw new Error("Grouped bars require a group encoding.");
54
+ readSeriesIdentity(dataset.values, updated);
55
+ offsetChannel = `${channels.category}Offset`;
56
+ offsetScale = resolveOffsetScaleDefinition(this, { id: encoding[offsetChannel]?.scale ?? offsetChannel }, offsetChannel);
57
+ offset = { ...(encoding.group.field === undefined ? {} : { field: encoding.group.field }), fieldType: encoding[offsetChannel]?.fieldType ?? "nominal", scale: offsetScale.id };
58
+ encoding[offsetChannel] = offset;
59
+ } else for (const channel of ["xOffset", "yOffset"]) delete encoding[channel];
60
+ }
61
+ const scales = offsetScale === undefined ? this.semanticSpec.scales
62
+ : [...this.semanticSpec.scales.filter(scale => scale.id !== offsetScale.id), offsetScale];
63
+ const preview = { ...this, markConfigs: this.markConfigs, canvasConfig: this.canvasConfig,
64
+ resolvedScales: { ...this.resolvedScales },
65
+ semanticSpec: { ...this.semanticSpec, scales,
66
+ layers: this.semanticSpec.layers.map(item => item.id === target ? updated : item) } };
67
+ for (const id of new Set([encoding.x?.scale, encoding.y?.scale, offset?.scale].filter(Boolean))) {
68
+ preview.resolvedScales[id] = resolveScalePreview(preview, id).resolvedScale;
69
+ }
70
+ let next = this.editSemantic({ property: `layer[${target}].layout.mode`, value: mode });
71
+ if (group === undefined && encoding.group !== undefined) next = setEncodingProperties(next, target, "group", encoding.group);
72
+ for (const channel of ["x", "y"]) if (Object.hasOwn(layer.encoding?.[channel] ?? {}, "stack")) {
73
+ next = next.editSemantic({ property: `layer[${target}].encoding.${channel}.stack`, remove: true });
74
+ }
75
+ if (color && Object.hasOwn(layer.encoding.color, "layout")) next = next.editSemantic({ property: `layer[${target}].encoding.color.layout`, remove: true });
76
+ if (endpoint !== undefined) {
77
+ next = next[endpoint.channel === "y2" ? "encodeY2" : "encodeX2"]({ target, datum: 0 });
78
+ }
79
+ if (layer.mark.type === "bar") {
80
+ for (const channel of ["xOffset", "yOffset"]) {
81
+ if (layer.encoding?.[channel] !== undefined) next = next.editSemantic({ property: `layer[${target}].encoding.${channel}`, remove: true });
82
+ if (channel !== offsetChannel) next = next._withoutMaterializationConfig(["marks", target, channel]);
83
+ }
84
+ if (offset !== undefined) {
85
+ next = offset.field === undefined
86
+ ? setEncodingProperties(next, target, offsetChannel, offset).createScale(offsetScale)
87
+ : next[offsetChannel === "xOffset" ? "encodeXOffset" : "encodeYOffset"]({ target, field: offset.field, fieldType: offset.fieldType, scale: offsetScale });
88
+ next = next._withoutMaterializationConfig(["marks", target, offsetChannel]);
89
+ if (findSemanticScale(this, offsetScale.id) === undefined) next = next._withMarkConfig(target, { ...next.markConfigs[target], seriesOffsetScale: offsetScale.id });
90
+ } else {
91
+ const owned = next.markConfigs[target]?.seriesOffsetScale;
92
+ if (owned !== undefined) {
93
+ const referenced = hasSemanticScaleReferences(next.semanticSpec, owned);
94
+ if (!referenced) next = next.editSemantic({ property: `scale[${owned}]`, remove: true });
95
+ next = next._withoutMaterializationConfig(["marks", target, "seriesOffsetScale"]);
96
+ }
97
+ }
98
+ }
99
+ for (const step of getPositionEncodingMaterializationSteps(next, updated, encoding.y.scale)) next = next[step.op](step.args);
100
+ return next;
101
+ });
102
+
103
+ export function registerSeriesLayoutAction(ProgramClass) {
104
+ ProgramClass.prototype.layoutSeries = layoutSeries;
105
+ }
@@ -1,15 +1,14 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
1
2
  import { validateUserId } from "../../core/identifiers.js";
2
3
  import { findDataset } from "../../selectors/datasets.js";
3
4
  import { resolveEligibleLayer } from "../../selectors/layers.js";
4
5
  import { hasMaterializedLegend } from "../../materialization/legends.js";
5
- import { applyMaterializationPlan } from "../../materialization/dependencies.js";
6
+ import { applyDetachedScaleRematerialization, applyMaterializationPlan } from "../../materialization/dependencies.js";
6
7
  import { planEncodingRematerialization } from "../../materialization/encodings.js";
7
8
  import { findSemanticScale } from "../../selectors/scales.js";
8
9
  import { validateOptionObject } from "../../core/validation.js";
9
- import {
10
- getMarkGraphicTypes,
11
- getPositionChannelDefinition
12
- } from "../../core/vocabulary.js";
10
+ import { getMarkGraphicTypes, getPositionChannelDefinition } from "../../core/vocabulary.js";
11
+ import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
13
12
 
14
13
  export function validateOptions(args, supported, operation) {
15
14
  validateOptionObject(args, supported, operation);
@@ -31,11 +30,16 @@ export function setEncodingProperties(
31
30
  return next;
32
31
  }
33
32
 
34
- export function rematerializeEncoding(program, target, channel, scale) {
35
- return applyMaterializationPlan(
33
+ export { applyDetachedScaleRematerialization };
34
+
35
+ export function rematerializeEncoding(program, target, channel, scale, previousLayer) {
36
+ const next = applyMaterializationPlan(
36
37
  program,
37
38
  planEncodingRematerialization(program, { target, channel, scale })
38
39
  );
40
+ return previousLayer === undefined
41
+ ? next
42
+ : applyDetachedScaleRematerialization(next, [previousLayer]);
39
43
  }
40
44
 
41
45
  export function clearMarkGraphic(program, target) {
@@ -50,19 +54,6 @@ export function clearMarkGraphic(program, target) {
50
54
  }
51
55
 
52
56
 
53
- export function validateLineSeriesCompatibility(layer, channel, field) {
54
- if (layer.mark?.type !== "line") return;
55
- for (const companion of ["group", "color", "strokeDash"]) {
56
- if (companion === channel) continue;
57
- const companionField = layer.encoding?.[companion]?.field;
58
- if (companionField !== undefined && companionField !== field) {
59
- throw new Error(
60
- `Line ${channel} field "${field}" must match ${companion} field "${companionField}".`
61
- );
62
- }
63
- }
64
- }
65
-
66
57
  export function resolveReassignmentScaleOptions(encoding, options) {
67
58
  if (encoding?.scale === undefined || options?.id !== undefined) return options;
68
59
  return { ...options, id: encoding.scale };
@@ -72,13 +63,14 @@ export function applyEncodingScale(
72
63
  program,
73
64
  definition,
74
65
  options = {},
75
- { reassignment = false } = {}
66
+ { reassignment = false, allowTypeChange = false } = {}
76
67
  ) {
77
68
  const existing = findSemanticScale(program, definition.id);
78
69
  if (existing === undefined) {
79
70
  return program.createScale(definition);
80
71
  }
81
72
  if (existing.type !== definition.type) {
73
+ if (reassignment && allowTypeChange) return program.editScale({ ...definition });
82
74
  throw new Error(
83
75
  `Scale "${definition.id}" cannot change type from "${existing.type}" to "${definition.type}".`
84
76
  );
@@ -88,7 +80,7 @@ export function applyEncodingScale(
88
80
  for (const property of [
89
81
  "domain", "range", "nice", "zero", "clamp", "reverse",
90
82
  "base", "exponent", "constant", "paddingInner", "paddingOuter",
91
- "padding", "align", "unknown", "interpolate"
83
+ "padding", "align", "unknown", "interpolate", "radialMapping"
92
84
  ]) {
93
85
  if (Object.hasOwn(options, property)) patch[property] = definition[property];
94
86
  }
@@ -114,33 +106,100 @@ export function rebindPositionGuides(
114
106
  const direction = definition.gridDirection;
115
107
  const grid = program.semanticSpec.guides.grid?.[direction];
116
108
  const ownsAxis = axis?.scale === previousScale;
109
+ const refreshesReboundAxis = axis?.scale === nextScale;
117
110
  const ownsGrid = grid?.scale === previousScale;
118
- if (!ownsAxis && !ownsGrid) return program;
111
+ if (!ownsAxis && !ownsGrid && !refreshesReboundAxis) return program;
119
112
 
120
113
  const remaining = program.semanticSpec.layers.some(layer =>
121
- layer.id !== target && layer.encoding?.[channel]?.scale === previousScale
114
+ layer.id !== target && !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === previousScale
122
115
  );
123
- if (remaining) {
116
+ if ((ownsAxis || ownsGrid) && remaining) {
124
117
  throw new Error(
125
118
  `Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`
126
119
  );
127
120
  }
128
121
 
129
122
  let next = program;
130
- if (ownsAxis) {
131
- next = next.editSemantic({
132
- property: `guide.axis.${definition.guideChannel}.scale`,
133
- value: nextScale
134
- });
135
- for (const component of ["line", "ticks", "labels", "title"]) {
136
- const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
137
- if (config?.scale === previousScale) {
138
- next = next._withGuideConfig(definition.guideChannel, component, {
139
- ...config,
140
- scale: nextScale
141
- });
123
+ if (ownsAxis || refreshesReboundAxis) {
124
+ const previousAxisConfig = next.guideConfigs.axis?.[definition.guideChannel];
125
+ const previousTicks = previousAxisConfig?.ticks;
126
+ const previousLabels = previousAxisConfig?.labels;
127
+ const labelsFollowTicks = previousTicks !== undefined &&
128
+ previousLabels !== undefined &&
129
+ previousTicks.mode === previousLabels.mode &&
130
+ (previousTicks.mode === "count"
131
+ ? previousTicks.count === previousLabels.count
132
+ : previousTicks.values?.length === previousLabels.values?.length &&
133
+ previousTicks.values.every(
134
+ (value, index) => Object.is(value, previousLabels.values[index])
135
+ ));
136
+ if (ownsAxis) {
137
+ next = next.editSemantic({
138
+ property: `guide.axis.${definition.guideChannel}.scale`,
139
+ value: nextScale
140
+ });
141
+ for (const component of ["line", "ticks", "labels", "title"]) {
142
+ const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
143
+ if (config?.scale === previousScale) {
144
+ next = next._withGuideConfig(definition.guideChannel, component, {
145
+ ...config,
146
+ scale: nextScale
147
+ });
148
+ }
142
149
  }
143
150
  }
151
+ const resolved = next.resolvedScales[nextScale] ??
152
+ findSemanticScale(next, nextScale);
153
+ const discrete = ["ordinal", "band", "point"].includes(resolved?.type);
154
+ let ticks = next.guideConfigs.axis?.[definition.guideChannel]?.ticks;
155
+ if (ticks !== undefined && discrete && ticks.mode !== "values") {
156
+ ticks = {
157
+ ...ticks,
158
+ mode: "values",
159
+ values: resolved.domain,
160
+ inferredValues: true
161
+ };
162
+ delete ticks.count;
163
+ next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
164
+ } else if (
165
+ ticks !== undefined &&
166
+ !discrete &&
167
+ ticks.mode === "values" &&
168
+ ticks.inferredValues === true
169
+ ) {
170
+ ticks = {
171
+ ...ticks,
172
+ mode: "count",
173
+ count: DEFAULT_TICK_COUNT,
174
+ inferredValues: true
175
+ };
176
+ delete ticks.values;
177
+ next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
178
+ } else if (
179
+ ticks !== undefined &&
180
+ discrete &&
181
+ ticks.inferredValues === true
182
+ ) {
183
+ ticks = { ...ticks, values: resolved.domain };
184
+ next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
185
+ }
186
+ if (previousLabels !== undefined && labelsFollowTicks && ticks !== undefined) {
187
+ const labels = {
188
+ ...next.guideConfigs.axis[definition.guideChannel].labels,
189
+ mode: ticks.mode,
190
+ inferredValues: ticks.inferredValues === true,
191
+ ...(ticks.mode === "values"
192
+ ? { values: ticks.values }
193
+ : { count: ticks.count })
194
+ };
195
+ if (ticks.mode === "values") delete labels.count;
196
+ else delete labels.values;
197
+ next = next._withGuideConfig(
198
+ definition.guideChannel,
199
+ "labels",
200
+ labels
201
+ );
202
+ }
144
203
  }
145
204
  if (ownsGrid) {
146
205
  next = next
@@ -195,3 +254,14 @@ export function resolveTarget(
195
254
 
196
255
  return { id, dataset, layer };
197
256
  }
257
+
258
+ export function inferSeriesGroup(program, layer, field, origin) {
259
+ const group = layer.encoding?.group;
260
+ if (group !== undefined && group.inferredFrom !== origin) {
261
+ if (origin === "offset" && (group.fields !== undefined || group.field !== field)) {
262
+ throw new Error("Offset field conflicts with the stored series group.");
263
+ }
264
+ return program;
265
+ }
266
+ return setEncodingProperties(program, layer.id, "group", { field, fieldType: "nominal", inferredFrom: origin });
267
+ }