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
@@ -0,0 +1,52 @@
1
+ import { findUpstreamTransform } from "../dataProvenance.js";
2
+ import { readPathSeriesFields, validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
+
4
+ export function validatePathGroupAppearance(program, layer, dataset) {
5
+ if (layer.mark.type === "area" && findUpstreamTransform(program, dataset, "density")) {
6
+ // Density placement owns a category/split grain beyond the visible group field.
7
+ readPathSeriesFields(dataset.values, layer);
8
+ return;
9
+ }
10
+ validatePathSeriesAppearance(dataset.values, layer);
11
+ }
12
+
13
+ export function assertPathGroupCompatible(program, layer, dataset, group) {
14
+ if (layer.encoding?.parallel !== undefined) {
15
+ throw new Error("Parallel paths have row identity and do not support encodeGroup.");
16
+ }
17
+ const field = group?.field;
18
+ for (const [type, editor] of [
19
+ ["density", "editDensity"],
20
+ ["horizon", "editHorizon"],
21
+ ["regression", "editRegression"],
22
+ ["interval", "editErrorBand"]
23
+ ]) {
24
+ const transform = findUpstreamTransform(program, dataset, type);
25
+ if (transform === undefined) continue;
26
+ const fields = type === "horizon" ? [transform.as.group]
27
+ : type === "interval"
28
+ ? transform.groupBy.filter(value =>
29
+ value !== layer.encoding?.x?.field && value !== layer.encoding?.y?.field
30
+ )
31
+ : [transform.groupBy];
32
+ if (group?.fields !== undefined || group === undefined ||
33
+ field === undefined || !fields.includes(field)) {
34
+ throw new Error(
35
+ `${type === "density" ? "Density area" : type + " path"} mark "${layer.id}" must group by ${fields.length === 1 && fields[0] !== undefined ? JSON.stringify(fields[0]) : "its owned field"}; use ${editor}.`
36
+ );
37
+ }
38
+ }
39
+ if (layer.layout?.mode !== undefined) {
40
+ if (group === undefined && !["overlay"].includes(layer.layout.mode)) throw new Error("Remove active series layout before removing its group.");
41
+ return;
42
+ }
43
+ if (layer.mark?.type === "area" && (
44
+ layer.encoding?.y?.stack === "center" ||
45
+ ![undefined, "overlay"].includes(layer.encoding?.color?.layout)
46
+ )) {
47
+ if (group?.fields !== undefined ||
48
+ (layer.encoding?.color !== undefined && field !== layer.encoding.color.field)) {
49
+ throw new Error("Area layout requires one group field matching its color field.");
50
+ }
51
+ }
52
+ }
@@ -1,8 +1,11 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
2
+ import { findLayer } from "../../selectors/layers.js";
1
3
  import {
2
4
  POSITION_CHANNELS,
3
5
  POSITION_ENCODING_CHANNELS
4
6
  } from "../../core/vocabulary.js";
5
7
  import { getMarkMaterializationPolicy } from "./policies.js";
8
+ import { buildMaterializationPlan } from "../planner.js";
6
9
 
7
10
  export {
8
11
  canMaterializeArc,
@@ -13,10 +16,32 @@ export {
13
16
  canMaterializeRect,
14
17
  canMaterializeRule,
15
18
  canMaterializeText,
16
- canMaterializeTick
19
+ canMaterializeTick,
20
+ isTextSource
17
21
  } from "./capabilities.js";
18
22
 
19
- export function getMarkRematerializationStep(layer) {
23
+ export function getLayerScaleIds(layer) {
24
+ if (isSourceOwnedText(layer)) return [];
25
+ return [
26
+ ...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
27
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
28
+ ].filter(scale => scale !== undefined);
29
+ }
30
+
31
+ export function getScaleConsumerMarkSteps(program, scaleIds) {
32
+ return program.semanticSpec.layers
33
+ .filter(layer => getLayerScaleIds(layer).some(id => scaleIds.includes(id)))
34
+ .map(layer => getMarkMaterializationStep(program, layer))
35
+ .filter(step => step !== undefined);
36
+ }
37
+
38
+ export function getMarkRematerializationStep(program, layer) {
39
+ if (program.markConfigs?.[layer.id]?.statisticalReference !== undefined) {
40
+ return { op: "rematerializeStatisticalReference", args: { id: layer.id } };
41
+ }
42
+ if (program.markConfigs?.[layer.id]?.markFilter?.empty === true) {
43
+ return { op: "materializeEmptyMark", args: { id: layer.id } };
44
+ }
20
45
  const policy = getMarkMaterializationPolicy(layer);
21
46
  return policy === undefined
22
47
  ? undefined
@@ -28,57 +53,61 @@ export function getMarkMaterializationStep(program, layer) {
28
53
  if (policy === undefined || !policy.canMaterialize(program, layer)) {
29
54
  return undefined;
30
55
  }
31
- return getMarkRematerializationStep(layer);
56
+ return getMarkRematerializationStep(program, layer);
32
57
  }
33
58
 
34
59
  export function getSourceDependentMarkSteps(program, sourceId) {
35
- return program.semanticSpec.layers.flatMap(layer =>
36
- layer.source === sourceId &&
37
- getMarkMaterializationPolicy(layer)?.sourceDependent === true
38
- ? [getMarkMaterializationStep(program, layer)].filter(
60
+ return (program.semanticSpec.layers ?? []).flatMap(layer => {
61
+ const sourceOwned = layer.source === sourceId &&
62
+ getMarkMaterializationPolicy(layer)?.sourceDependent === true;
63
+ const statistical = program.markConfigs?.[layer.id]
64
+ ?.statisticalReference?.source === sourceId;
65
+ return sourceOwned || statistical
66
+ ? [getMarkMaterializationStep(program, layer) ??
67
+ getExistingMarkRematerializationStep(program, layer)].filter(
39
68
  step => step !== undefined
40
69
  )
41
- : []
42
- );
70
+ : [];
71
+ });
43
72
  }
44
73
 
45
74
  export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
46
75
  const policy = getMarkMaterializationPolicy(layer);
47
76
  if (policy === undefined) return [];
48
77
  const complete = policy.canMaterialize(program, layer);
49
- const mark = getMarkRematerializationStep(layer);
78
+ const mark = getMarkRematerializationStep(program, layer);
50
79
  const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
51
80
  .filter(candidate =>
52
81
  candidate.id !== layer.id &&
82
+ !isSourceOwnedText(candidate) &&
53
83
  POSITION_CHANNELS.some(channel =>
54
84
  candidate.encoding?.[channel]?.scale === scaleId
55
85
  )
56
86
  )
57
87
  .map(candidate => getMarkMaterializationStep(program, candidate))
58
88
  .filter(step => step !== undefined);
89
+ const dependentMarks = [mark, ...sharedConsumerMarks]
90
+ .flatMap(step => getSourceDependentMarkSteps(program, step.args.id));
59
91
  const scale = {
60
92
  op: "rematerializeScale",
61
93
  args: sharedConsumerMarks.length === 0
62
94
  ? { id: scaleId }
63
95
  : { id: scaleId, marks: false }
64
96
  };
65
- if (!complete) {
66
- if (policy.positionEncoding.incomplete === "scale") {
67
- return [scale, ...sharedConsumerMarks];
68
- }
69
- return policy.positionEncoding.scaleFirst
97
+ const direct = !complete && policy.positionEncoding.incomplete === "scale"
98
+ ? [scale, ...sharedConsumerMarks]
99
+ : policy.positionEncoding.scaleFirst
70
100
  ? [scale, mark, ...sharedConsumerMarks]
71
101
  : [mark, ...sharedConsumerMarks];
72
- }
73
- return policy.positionEncoding.scaleFirst
74
- ? [scale, mark, ...sharedConsumerMarks]
75
- : [mark, ...sharedConsumerMarks];
102
+ return buildMaterializationPlan({ marks: [...direct, ...dependentMarks] });
76
103
  }
77
104
 
78
105
  export function getScaleConsumerMaterializationMode(layer, channel) {
79
106
  const policy = getMarkMaterializationPolicy(layer);
80
107
  if (policy === undefined) return "direct";
81
108
  if (POSITION_ENCODING_CHANNELS.includes(channel)) {
109
+ // Attached labels require their source's completed geometry, after scales.
110
+ if (layer.source !== undefined && policy.sourceDependent === true) return "defer";
82
111
  return policy.scaleApplication.position ?? policy.scaleApplication.default;
83
112
  }
84
113
  if (policy.scaleApplication.deferredChannels?.includes(channel)) {
@@ -99,7 +128,7 @@ export function getExistingMarkRematerializationStep(program, layer) {
99
128
  ) {
100
129
  return undefined;
101
130
  }
102
- return getMarkRematerializationStep(layer);
131
+ return getMarkRematerializationStep(program, layer);
103
132
  }
104
133
 
105
134
  export function getEncodingMaterializationStages(program, layer, channel, scale) {
@@ -111,22 +140,34 @@ export function getEncodingMaterializationStages(program, layer, channel, scale)
111
140
  args: { id: scale, guides: false, marks: false }
112
141
  }]
113
142
  : [];
114
- const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
115
- scale !== undefined;
143
+ const shared = scale !== undefined && (
144
+ scales.length > 0 || policy.encoding?.sharedChannels?.includes(channel) === true
145
+ );
116
146
  const candidates = shared
117
147
  ? program.semanticSpec.layers.filter(candidate =>
118
- candidate.mark?.type === layer.mark?.type &&
119
- candidate.encoding?.[channel]?.scale === scale
148
+ candidate.id === layer.id || candidate.encoding?.[channel]?.scale === scale
120
149
  )
121
150
  : [layer];
122
151
  const marks = candidates.flatMap(candidate => {
123
- if (policy.encoding?.skipRematerialization?.(program, candidate) === true) {
152
+ const candidatePolicy = getMarkMaterializationPolicy(candidate);
153
+ if (candidatePolicy?.encoding?.skipRematerialization?.(program, candidate) === true) {
124
154
  return [];
125
155
  }
126
- const step = policy.encoding?.completeOnly === true
156
+ const step = candidate.id !== layer.id || candidatePolicy?.encoding?.completeOnly === true
127
157
  ? getMarkMaterializationStep(program, candidate)
128
- : getMarkRematerializationStep(candidate);
158
+ : getMarkRematerializationStep(program, candidate);
129
159
  return step === undefined ? [] : [step];
130
160
  });
131
161
  return { scales, marks };
132
162
  }
163
+
164
+ // Reuse a plan's scale stage only when it covers every scale consumed by the mark.
165
+ export function reusePlannedMarkScales(program, steps, scaleIds) {
166
+ const resolved = new Set(scaleIds);
167
+ return steps.map(step => {
168
+ const layer = findLayer(program, step.args?.id);
169
+ if (layer === undefined || getMarkMaterializationPolicy(layer)?.acceptsResolvedScales !== true ||
170
+ !getLayerScaleIds(layer).every(id => resolved.has(id))) return step;
171
+ return { ...step, args: { ...step.args, scales: false } };
172
+ });
173
+ }
@@ -40,7 +40,9 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
40
40
  line: Object.freeze({
41
41
  canMaterialize: canMaterializeLine,
42
42
  op: "rematerializeLineMark",
43
+ acceptsResolvedScales: true,
43
44
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
45
+ encoding: Object.freeze({ scaleFirst: true, completeOnly: true }),
44
46
  scaleApplication: Object.freeze({ default: "defer" })
45
47
  }),
46
48
  area: Object.freeze({
@@ -48,7 +50,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
48
50
  op: "rematerializeAreaMark",
49
51
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
50
52
  encoding: Object.freeze({
51
- sharedChannels: Object.freeze(["color"]),
53
+ sharedChannels: Object.freeze(["color", "stroke"]),
52
54
  skipRematerialization: isIntentionallyEmptyArea
53
55
  }),
54
56
  scaleApplication: Object.freeze({ default: "defer" })
@@ -56,6 +58,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
56
58
  arc: Object.freeze({
57
59
  canMaterialize: canMaterializeArc,
58
60
  op: "rematerializeArcMark",
61
+ acceptsResolvedScales: true,
59
62
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
60
63
  encoding: Object.freeze({ completeOnly: true }),
61
64
  scaleApplication: Object.freeze({ default: "defer" })
@@ -2,28 +2,42 @@ import {
2
2
  mapOrdinalPositionValues,
3
3
  readScaleField
4
4
  } from "../grammar/scales/index.js";
5
- import { RECT_MODES, resolveRectMode } from "../grammar/rects.js";
5
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
6
+ import { resolveCoordinateBounds } from "./coordinateBounds.js";
7
+ import { RECT_MODES, resolveRectMode, rectUsesFields } from "../grammar/rects.js";
6
8
  import { DEFAULT_RECT_MARK } from "./rectConfig.js";
7
9
  import { mapScaleConsumerValues } from "./scales/map.js";
10
+ import { materializeRectItem } from "../grammar/roundedRect.js";
11
+ import { requestedStrokeDetails } from "../grammar/strokeStyle.js";
8
12
 
9
- function optionalValues(rows, encoding) {
13
+ function optionalValues(rows, encoding, length, channel) {
14
+ if (Object.hasOwn(encoding, "datum")) {
15
+ const value = normalizePositionDatum(encoding.datum, encoding.fieldType, channel, encoding.temporalUnit, "Rect");
16
+ return Array.from({ length }, () => value);
17
+ }
10
18
  return readScaleField(rows, encoding.field, encoding.fieldType, {
11
- allowUnknown: true
19
+ allowUnknown: true, temporalUnit: encoding.temporalUnit
12
20
  });
13
21
  }
14
22
 
15
23
  function requiredChannels(layer, mode) {
16
- return mode === RECT_MODES.ranged
17
- ? ["x", "x2", "y", "y2", ...(layer.encoding?.color ? ["color"] : [])]
18
- : ["x", "y", ...(layer.encoding?.color ? ["color"] : [])];
24
+ const position = mode === RECT_MODES.xSpan ? ["x", "x2"]
25
+ : mode === RECT_MODES.ySpan ? ["y", "y2"]
26
+ : mode === RECT_MODES.ranged ? ["x", "x2", "y", "y2"] : ["x", "y"];
27
+ return [
28
+ ...position,
29
+ ...(layer.encoding?.color ? ["color"] : []),
30
+ ...(layer.encoding?.stroke ? ["stroke"] : [])
31
+ ];
19
32
  }
20
33
 
21
34
  export function resolveRectConsumerValues(layer, dataset, channel) {
22
- const requested = optionalValues(dataset.values, layer.encoding[channel]);
35
+ const length = rectUsesFields(layer) ? dataset.values.length : 1;
36
+ const requested = optionalValues(dataset.values, layer.encoding[channel], length, channel);
23
37
  const mode = resolveRectMode(layer);
24
38
  if (mode === undefined) return requested;
25
39
  const complete = requiredChannels(layer, mode).map(candidate =>
26
- optionalValues(dataset.values, layer.encoding[candidate])
40
+ optionalValues(dataset.values, layer.encoding[candidate], length, candidate)
27
41
  );
28
42
  return requested.map((value, index) =>
29
43
  complete.every(values => values[index] !== undefined) ? value : undefined
@@ -56,37 +70,42 @@ function mappedEncoding(program, layer, dataset, channel) {
56
70
  };
57
71
  }
58
72
 
59
- function appearance(config, fill) {
73
+ function appearance(config, fill, stroke) {
60
74
  return {
61
75
  fill,
62
76
  opacity: config.opacity,
63
- stroke: config.stroke === false ? "transparent" : config.stroke,
64
- strokeWidth: config.stroke === false ? 0 : config.strokeWidth
77
+ stroke: stroke ?? (config.stroke === false ? "transparent" : config.stroke),
78
+ strokeWidth: stroke === undefined && config.stroke === false
79
+ ? 0
80
+ : config.strokeWidth ?? 1,
81
+ ...requestedStrokeDetails(config, "Rect mark")
65
82
  };
66
83
  }
67
84
 
68
85
  export function resolveRectRows(program, layer, dataset) {
69
86
  const mode = resolveRectMode(layer);
70
87
  if (mode === undefined) return [];
71
- const x = mappedEncoding(program, layer, dataset, "x");
72
- const y = mappedEncoding(program, layer, dataset, "y");
73
- const x2 = mode === RECT_MODES.ranged
74
- ? mappedEncoding(program, layer, dataset, "x2")
75
- : undefined;
76
- const y2 = mode === RECT_MODES.ranged
77
- ? mappedEncoding(program, layer, dataset, "y2")
78
- : undefined;
88
+ const bounds = resolveCoordinateBounds(program, layer.coordinate);
89
+ const x = layer.encoding.x === undefined ? undefined : mappedEncoding(program, layer, dataset, "x");
90
+ const y = layer.encoding.y === undefined ? undefined : mappedEncoding(program, layer, dataset, "y");
91
+ const x2 = layer.encoding.x2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "x2");
92
+ const y2 = layer.encoding.y2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "y2");
79
93
  const color = layer.encoding?.color === undefined
80
94
  ? undefined
81
95
  : mappedEncoding(program, layer, dataset, "color").values;
96
+ const stroke = layer.encoding?.stroke === undefined
97
+ ? undefined
98
+ : mappedEncoding(program, layer, dataset, "stroke").values;
82
99
  const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
83
100
 
84
- return dataset.values.flatMap((row, index) => {
101
+ const rows = rectUsesFields(layer) ? dataset.values : [{}];
102
+ return rows.flatMap((row, index) => {
85
103
  const fill = color === undefined ? config.fill : color[index];
86
104
  if (
87
- !Number.isFinite(x.values[index]) ||
88
- !Number.isFinite(y.values[index]) ||
89
- typeof fill !== "string"
105
+ (x !== undefined && !Number.isFinite(x.values[index])) ||
106
+ (y !== undefined && !Number.isFinite(y.values[index])) ||
107
+ typeof fill !== "string" ||
108
+ (stroke !== undefined && typeof stroke[index] !== "string")
90
109
  ) return [];
91
110
 
92
111
  let geometry;
@@ -103,36 +122,30 @@ export function resolveRectRows(program, layer, dataset) {
103
122
  height
104
123
  };
105
124
  } else {
106
- const endX = x2.values[index];
107
- const endY = y2.values[index];
125
+ const startX = x?.values[index] ?? bounds.x;
126
+ const startY = y?.values[index] ?? bounds.y;
127
+ const endX = x2?.values[index] ?? bounds.x + bounds.width;
128
+ const endY = y2?.values[index] ?? bounds.y + bounds.height;
108
129
  if (!Number.isFinite(endX) || !Number.isFinite(endY)) return [];
109
130
  geometry = {
110
- x: Math.min(x.values[index], endX),
111
- y: Math.min(y.values[index], endY),
112
- width: Math.abs(endX - x.values[index]),
113
- height: Math.abs(endY - y.values[index])
131
+ x: Math.min(startX, endX),
132
+ y: Math.min(startY, endY),
133
+ width: Math.abs(endX - startX),
134
+ height: Math.abs(endY - startY)
114
135
  };
115
136
  if (geometry.width <= 0 || geometry.height <= 0) return [];
116
137
  }
117
138
  return [{
118
139
  row,
119
140
  sourceIndex: index,
120
- channels: Object.fromEntries(
121
- ["x", "y", "x2", "y2", "color"].flatMap(channel => {
122
- const encoding = layer.encoding?.[channel];
123
- return encoding?.field === undefined
124
- ? []
125
- : [[channel, row[encoding.field]]];
126
- })
127
- ),
128
- properties: { ...geometry, ...appearance(config, fill) }
141
+ properties: { ...geometry, ...appearance(config, fill, stroke?.[index]) }
129
142
  }];
130
143
  });
131
144
  }
132
145
 
133
146
  export function resolveRectGraphicItems(program, layer, dataset) {
134
- return resolveRectRows(program, layer, dataset).map(item => ({
135
- type: "rect",
136
- properties: item.properties
137
- }));
147
+ const radius = program.markConfigs[layer.id]?.cornerRadius ?? 0;
148
+ return resolveRectRows(program, layer, dataset).map(item =>
149
+ materializeRectItem(item.properties, radius)
150
+ );
138
151
  }
@@ -4,6 +4,7 @@ import {
4
4
  validateUnitInterval
5
5
  } from "../core/validation.js";
6
6
  import { DEFAULT_COLORS } from "../theme/defaults.js";
7
+ import { requestedRectStyleDetails } from "../grammar/roundedRect.js";
7
8
 
8
9
  export const DEFAULT_RECT_MARK = Object.freeze({
9
10
  fill: DEFAULT_COLORS.mark,
@@ -13,7 +14,10 @@ export const DEFAULT_RECT_MARK = Object.freeze({
13
14
  });
14
15
 
15
16
  export function normalizeRectMarkConfig(patch, previous = DEFAULT_RECT_MARK) {
16
- const result = { ...previous };
17
+ const result = {
18
+ ...previous,
19
+ ...requestedRectStyleDetails(patch, "Rect")
20
+ };
17
21
  if (Object.hasOwn(patch, "fill")) {
18
22
  result.fill = validateNonEmptyString(patch.fill, "Rect fill");
19
23
  }
@@ -5,15 +5,13 @@ import {
5
5
  import {
6
6
  mapOrdinalOffsetValues,
7
7
  mapOrdinalPositionValues,
8
- readNominalField,
9
- readQuantitativeField,
10
- readScaleField,
11
- readTemporalField
8
+ readScaleField
12
9
  } from "../grammar/scales/index.js";
13
10
  import {
14
11
  finiteMidpoint,
15
12
  requireFiniteResult
16
13
  } from "../grammar/numeric.js";
14
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
17
15
  import { mapScaleConsumerValues } from "./scales/map.js";
18
16
 
19
17
  export function resolveRowEncodingValues(program, layer, dataset, channel) {
@@ -26,15 +24,17 @@ export function resolveRowEncodingValues(program, layer, dataset, channel) {
26
24
  );
27
25
  }
28
26
  const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
29
- const values = Object.hasOwn(scale, "unknown")
30
- ? readScaleField(dataset.values, encoding.field, encoding.fieldType, {
31
- allowUnknown: true
32
- })
33
- : categorical
34
- ? readNominalField(dataset.values, encoding.field)
35
- : encoding.fieldType === "temporal"
36
- ? readTemporalField(dataset.values, encoding.field)
37
- : readQuantitativeField(dataset.values, encoding.field);
27
+ const values = Object.hasOwn(encoding, "datum")
28
+ ? Array(dataset.values.length).fill(normalizePositionDatum(
29
+ encoding.datum,
30
+ encoding.fieldType,
31
+ channel,
32
+ encoding.temporalUnit,
33
+ layer.mark.type[0].toUpperCase() + layer.mark.type.slice(1)
34
+ ))
35
+ : readScaleField(dataset.values, encoding.field, encoding.fieldType, {
36
+ allowUnknown: Object.hasOwn(scale, "unknown"), temporalUnit: encoding.temporalUnit
37
+ });
38
38
  if (categorical && OFFSET_POSITION_CHANNELS.includes(channel)) {
39
39
  return mapOrdinalOffsetValues(values, scale);
40
40
  }
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../grammar/text.js";
1
2
  import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
2
3
  import { materializedLegendUsesScale } from "./legends.js";
3
4
  import { buildMaterializationPlan } from "./planner.js";
@@ -19,10 +20,12 @@ const GRIDS = Object.freeze([
19
20
 
20
21
  function usesPositionalScale(program, id) {
21
22
  return program.semanticSpec.layers.some(layer =>
22
- POSITION_ENCODING_CHANNELS.some(
23
- channel => layer.encoding?.[channel]?.scale === id
24
- ) || (layer.encoding?.parallel?.dimensions ?? []).some(
25
- dimension => dimension.scale === id
23
+ !isSourceOwnedText(layer) && (
24
+ POSITION_ENCODING_CHANNELS.some(
25
+ channel => layer.encoding?.[channel]?.scale === id
26
+ ) || (layer.encoding?.parallel?.dimensions ?? []).some(
27
+ dimension => dimension.scale === id
28
+ )
26
29
  )
27
30
  );
28
31
  }
@@ -50,7 +53,7 @@ export function needsCanvasScaleRematerialization(program, scale) {
50
53
  (scale.range === "auto" ||
51
54
  usesRadialScale(program, scale.id) ||
52
55
  semanticGuideUsesScale(program, scale.id)) &&
53
- program.resolvedScales[scale.id] !== undefined &&
56
+ (program.resolvedScales[scale.id] !== undefined || scale.radialMapping !== undefined) &&
54
57
  usesPositionalScale(program, scale.id)
55
58
  );
56
59
  }
@@ -4,21 +4,24 @@ import {
4
4
  isOrdinalScaleType,
5
5
  mapContinuousScaleValues,
6
6
  mapDiscretizedColors,
7
+ mapSizeValues,
7
8
  mapOrdinalValues,
8
9
  mapSequentialColors
9
10
  } from "../../grammar/scales/index.js";
10
11
 
11
12
  export function mapScaleConsumerValues(values, resolvedScale, channel) {
12
- if (channel === "color" && isDiscretizedColorScaleType(resolvedScale.type)) {
13
+ if (channel === "size") return mapSizeValues(values, resolvedScale);
14
+ if (["color", "stroke"].includes(channel) && isDiscretizedColorScaleType(resolvedScale.type)) {
13
15
  return mapDiscretizedColors(values, resolvedScale);
14
16
  }
15
- if (channel === "color" && isContinuousColorScaleType(resolvedScale.type)) {
17
+ if (["color", "stroke"].includes(channel) && isContinuousColorScaleType(resolvedScale.type)) {
16
18
  return mapSequentialColors(
17
19
  values,
18
20
  resolvedScale.domain,
19
21
  resolvedScale.range,
20
22
  {
21
23
  interpolation: resolvedScale.interpolate,
24
+ midpoint: resolvedScale.midpoint,
22
25
  clamp: resolvedScale.clamp ?? false,
23
26
  ...(Object.hasOwn(resolvedScale, "unknown")
24
27
  ? { unknown: resolvedScale.unknown }
@@ -27,7 +30,7 @@ export function mapScaleConsumerValues(values, resolvedScale, channel) {
27
30
  );
28
31
  }
29
32
  if (
30
- ["color", "strokeDash", "shape"].includes(channel) &&
33
+ ["color", "stroke", "strokeDash", "shape"].includes(channel) &&
31
34
  isOrdinalScaleType(resolvedScale.type)
32
35
  ) {
33
36
  return mapOrdinalValues(values, resolvedScale.domain, resolvedScale.range, {
@@ -1,3 +1,4 @@
1
+ import { validateMeasuredRadiusScale } from "../../../grammar/scales/radial.js";
1
2
  export function resolveArcAutoPositionRange({
2
3
  consumers,
3
4
  scale,
@@ -35,3 +36,44 @@ export function resolveArcAutoPositionRange({
35
36
  }
36
37
  return range;
37
38
  }
39
+
40
+
41
+ export function resolveMeasuredRadiusDomain({ scale, channel, allValues }) {
42
+ if (scale.radialMapping === undefined) return undefined;
43
+ if (channel !== "radius") throw new Error("Measured radius scales support only radius consumers.");
44
+ const maximum = allValues.reduce((maximum, value) => Math.max(maximum, value), -Infinity);
45
+ if (!Number.isFinite(maximum) || maximum <= 0 || allValues.some(value => value < 0)) {
46
+ throw new Error("Measured radius requires finite non-negative aggregates and a positive maximum.");
47
+ }
48
+ const domain = scale.domain === "auto" ? [0, maximum] : scale.domain;
49
+ if (Array.isArray(domain) && domain[1] < maximum) {
50
+ throw new RangeError("Measured radius domain maximum must cover every category aggregate.");
51
+ }
52
+ return domain;
53
+ }
54
+
55
+ export function validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales }) {
56
+ if (scale.radialMapping === undefined) return;
57
+ validateMeasuredRadiusScale({ ...scale, domain, range }, { allowAuto: true });
58
+ for (const consumer of consumers) {
59
+ const { layer, encoding } = consumer;
60
+ const theta = layer.encoding?.theta;
61
+ const thetaScale = thetaScales[layer.id];
62
+ if (!isPendingMeasuredRadiusConsumer(consumer) && (layer.mark?.type !== "arc" || !["count", "sum"].includes(encoding.aggregate) ||
63
+ !["nominal", "ordinal"].includes(theta?.fieldType) || theta.aggregate !== undefined ||
64
+ thetaScale?.type !== "band" || (thetaScale.paddingInner ?? 0) !== 0 || (thetaScale.paddingOuter ?? 0) !== 0)) {
65
+ throw new Error("Measured radius requires only equal-angle categorical Arc consumers.");
66
+ }
67
+ const config = markConfigs[layer.id] ?? {};
68
+ if ((config.padAngle ?? 0) !== 0) throw new Error("Measured Arc requires padAngle 0.");
69
+ if (scale.range !== "auto" && config.innerRadiusExplicit === true &&
70
+ Math.abs(config.innerRadius - range[0] / range[1]) > Number.EPSILON * 8) {
71
+ throw new Error("Measured Arc innerRadius must agree with its explicit radius range.");
72
+ }
73
+ }
74
+ }
75
+
76
+ export function isPendingMeasuredRadiusConsumer({ layer, channel, encoding }) {
77
+ return layer.mark?.type === "arc" && channel === "radius" &&
78
+ ["count", "sum"].includes(encoding.aggregate) && layer.encoding?.theta === undefined;
79
+ }
@@ -1,11 +1,11 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { resolveHistogramBins } from "../../../grammar/histogram.js";
2
3
 
3
4
  export function resolveBinnedPositionDomain({
4
5
  valuesByConsumer,
5
6
  channel,
6
7
  scale,
7
- id,
8
- allValues
8
+ id
9
9
  }) {
10
10
  const binnedPositions = valuesByConsumer.filter(
11
11
  ({ consumer }) =>
@@ -13,7 +13,10 @@ export function resolveBinnedPositionDomain({
13
13
  consumer.encoding.bin !== undefined
14
14
  );
15
15
  if (binnedPositions.length === 0) return undefined;
16
- if (channel !== "x" || binnedPositions.length !== valuesByConsumer.length) {
16
+ const independentConsumers = valuesByConsumer.filter(({ consumer, scalePolicy }) =>
17
+ scalePolicy !== false && !isSourceOwnedText(consumer.layer)
18
+ );
19
+ if (channel !== "x" || binnedPositions.length !== independentConsumers.length) {
17
20
  throw new Error(
18
21
  `Binned scale "${id}" cannot be shared with an unbinned consumer.`
19
22
  );
@@ -25,7 +28,7 @@ export function resolveBinnedPositionDomain({
25
28
  throw new Error(`Binned scale "${id}" requires one shared bin definition.`);
26
29
  }
27
30
  return resolveHistogramBins({
28
- values: allValues,
31
+ values: binnedPositions.flatMap(item => item.values).filter(value => value !== undefined),
29
32
  bin: binnedPositions[0].consumer.encoding.bin,
30
33
  domain: scale.domain,
31
34
  nice: scale.nice ?? true,