ggaction 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +10 -3
  11. package/src/actions/basic.js +4 -2
  12. package/src/actions/boxPlots/components.js +14 -4
  13. package/src/actions/boxPlots/edit.js +31 -5
  14. package/src/actions/boxPlots/materialize.js +13 -4
  15. package/src/actions/boxPlots/options.js +48 -16
  16. package/src/actions/canvas/actions.js +3 -1
  17. package/src/actions/canvas/fitting.js +211 -0
  18. package/src/actions/canvas/index.js +2 -0
  19. package/src/actions/categoryOrder/index.js +18 -6
  20. package/src/actions/charts/area.js +51 -0
  21. package/src/actions/charts/bar.js +13 -6
  22. package/src/actions/charts/beeswarm.js +70 -0
  23. package/src/actions/charts/density.js +48 -0
  24. package/src/actions/charts/ecdf.js +186 -0
  25. package/src/actions/charts/endpoints.js +473 -0
  26. package/src/actions/charts/heatmap.js +4 -3
  27. package/src/actions/charts/histogram.js +5 -4
  28. package/src/actions/charts/horizon.js +73 -0
  29. package/src/actions/charts/index.js +37 -0
  30. package/src/actions/charts/interval-regression.js +172 -0
  31. package/src/actions/charts/line.js +8 -6
  32. package/src/actions/charts/parallel.js +8 -2
  33. package/src/actions/charts/pie.js +52 -0
  34. package/src/actions/charts/polar.js +134 -0
  35. package/src/actions/charts/radar.js +305 -0
  36. package/src/actions/charts/radial.js +50 -0
  37. package/src/actions/charts/raincloud.js +422 -0
  38. package/src/actions/charts/rug-strip.js +283 -0
  39. package/src/actions/charts/scatter.js +11 -3
  40. package/src/actions/charts/shared.js +69 -10
  41. package/src/actions/composition/actions.js +129 -3
  42. package/src/actions/coordinates/actions.js +1 -1
  43. package/src/actions/coordinates/edit.js +76 -0
  44. package/src/actions/data/bin.js +29 -0
  45. package/src/actions/data/bin2d.js +57 -185
  46. package/src/actions/data/complete.js +26 -0
  47. package/src/actions/data/computed.js +24 -0
  48. package/src/actions/data/create.js +2 -2
  49. package/src/actions/data/density.js +12 -20
  50. package/src/actions/data/derived.js +111 -15
  51. package/src/actions/data/ecdf.js +38 -0
  52. package/src/actions/data/edit.js +539 -0
  53. package/src/actions/data/filter.js +226 -30
  54. package/src/actions/data/fold.js +24 -0
  55. package/src/actions/data/impute.js +26 -0
  56. package/src/actions/data/index.js +38 -0
  57. package/src/actions/data/interval.js +2 -0
  58. package/src/actions/data/intervalEdit.js +86 -4
  59. package/src/actions/data/normalize.js +27 -0
  60. package/src/actions/data/regression.js +19 -21
  61. package/src/actions/data/revisionBindings.js +145 -0
  62. package/src/actions/data/shared.js +20 -4
  63. package/src/actions/data/stack.js +26 -0
  64. package/src/actions/data/summary.js +26 -0
  65. package/src/actions/data/timeUnit.js +4 -1
  66. package/src/actions/data/window.js +1 -1
  67. package/src/actions/encodings/appearance.js +32 -13
  68. package/src/actions/encodings/barWidth.js +8 -0
  69. package/src/actions/encodings/basic.js +2 -0
  70. package/src/actions/encodings/channels.js +415 -0
  71. package/src/actions/encodings/color/continuous.js +19 -59
  72. package/src/actions/encodings/color/index.js +30 -23
  73. package/src/actions/encodings/color/layout.js +3 -97
  74. package/src/actions/encodings/color/policy.js +5 -8
  75. package/src/actions/encodings/density/resolve.js +37 -5
  76. package/src/actions/encodings/density.js +124 -34
  77. package/src/actions/encodings/histogram.js +3 -2
  78. package/src/actions/encodings/horizon/resolve.js +8 -4
  79. package/src/actions/encodings/horizon.js +18 -8
  80. package/src/actions/encodings/index.js +6 -0
  81. package/src/actions/encodings/offset.js +30 -29
  82. package/src/actions/encodings/parallel.js +2 -8
  83. package/src/actions/encodings/position/apply.js +63 -11
  84. package/src/actions/encodings/position/index.js +52 -6
  85. package/src/actions/encodings/position/policies/arc.js +14 -0
  86. package/src/actions/encodings/position/policies/area.js +4 -42
  87. package/src/actions/encodings/position/policies/bar.js +57 -13
  88. package/src/actions/encodings/position/resolve.js +88 -26
  89. package/src/actions/encodings/ranged.js +123 -97
  90. package/src/actions/encodings/remove.js +44 -34
  91. package/src/actions/encodings/ruleAppearance.js +24 -32
  92. package/src/actions/encodings/seriesLayout.js +105 -0
  93. package/src/actions/encodings/shared.js +107 -37
  94. package/src/actions/encodings/stroke.js +226 -0
  95. package/src/actions/encodings/strokeDash.js +9 -6
  96. package/src/actions/encodings/temporal.js +7 -0
  97. package/src/actions/encodings/text.js +30 -15
  98. package/src/actions/errorBands/create.js +41 -8
  99. package/src/actions/errorBands/edit.js +360 -25
  100. package/src/actions/errorBars/create.js +56 -17
  101. package/src/actions/errorBars/edit.js +372 -21
  102. package/src/actions/facets/actions.js +619 -41
  103. package/src/actions/facets/derive.js +139 -25
  104. package/src/actions/gradientPlots/components.js +43 -13
  105. package/src/actions/gradientPlots/edit.js +14 -4
  106. package/src/actions/gradientPlots/materialize.js +8 -8
  107. package/src/actions/gradientPlots/options.js +10 -2
  108. package/src/actions/guides/applicability.js +28 -19
  109. package/src/actions/guides/axes/axes.js +4 -3
  110. package/src/actions/guides/axes/axis.js +18 -18
  111. package/src/actions/guides/axes/components.js +43 -0
  112. package/src/actions/guides/axes/edit.js +16 -51
  113. package/src/actions/guides/axes/labels.js +184 -28
  114. package/src/actions/guides/axes/lines.js +13 -7
  115. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  116. package/src/actions/guides/axes/parallel/policy.js +137 -0
  117. package/src/actions/guides/axes/parallel/resolve.js +50 -25
  118. package/src/actions/guides/axes/parallel.js +93 -217
  119. package/src/actions/guides/axes/policy.js +21 -67
  120. package/src/actions/guides/axes/tickGroups.js +15 -8
  121. package/src/actions/guides/axes/ticks.js +11 -6
  122. package/src/actions/guides/axes/titles.js +32 -11
  123. package/src/actions/guides/basic.js +2 -0
  124. package/src/actions/guides/facade.js +106 -0
  125. package/src/actions/guides/facadeAxes.js +200 -0
  126. package/src/actions/guides/facadeLegend.js +63 -0
  127. package/src/actions/guides/grids/grid.js +37 -33
  128. package/src/actions/guides/grids/resolve.js +17 -4
  129. package/src/actions/guides/guides.js +54 -31
  130. package/src/actions/guides/legends/blocks.js +426 -0
  131. package/src/actions/guides/legends/categorical/actions.js +309 -279
  132. package/src/actions/guides/legends/categorical/components.js +21 -17
  133. package/src/actions/guides/legends/categorical/index.js +37 -33
  134. package/src/actions/guides/legends/categorical/layout.js +123 -354
  135. package/src/actions/guides/legends/categorical/options.js +32 -16
  136. package/src/actions/guides/legends/categorical/recipes.js +114 -9
  137. package/src/actions/guides/legends/categorical/resolve.js +30 -15
  138. package/src/actions/guides/legends/categorical/symbols.js +87 -33
  139. package/src/actions/guides/legends/continuous/common.js +99 -26
  140. package/src/actions/guides/legends/continuous/gradient.js +120 -89
  141. package/src/actions/guides/legends/continuous/index.js +8 -2
  142. package/src/actions/guides/legends/continuous/interval.js +79 -95
  143. package/src/actions/guides/legends/continuous/opacity.js +113 -89
  144. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  145. package/src/actions/guides/legends/creation.js +40 -0
  146. package/src/actions/guides/legends/edit.js +321 -90
  147. package/src/actions/guides/legends/focused.js +2 -2
  148. package/src/actions/guides/legends/index.js +2 -0
  149. package/src/actions/guides/legends/lane.js +149 -102
  150. package/src/actions/guides/legends/lifecycle.js +143 -0
  151. package/src/actions/guides/legends/remove.js +43 -78
  152. package/src/actions/guides/legends/sampling.js +140 -0
  153. package/src/actions/guides/legends/size.js +213 -116
  154. package/src/actions/guides/legends/strokeWidth.js +121 -73
  155. package/src/actions/guides/legends/target.js +123 -0
  156. package/src/actions/guides/legends/transition.js +192 -0
  157. package/src/actions/guides/polar/axes/facade.js +81 -66
  158. package/src/actions/guides/polar/axes/labels.js +28 -8
  159. package/src/actions/guides/polar/axes/lines.js +12 -5
  160. package/src/actions/guides/polar/axes/shared.js +22 -0
  161. package/src/actions/guides/polar/axes/ticks.js +6 -5
  162. package/src/actions/guides/polar/axes/titles.js +6 -5
  163. package/src/actions/guides/polar/grids.js +8 -8
  164. package/src/actions/guides/polar/resolve.js +12 -8
  165. package/src/actions/guides/reuse.js +44 -0
  166. package/src/actions/guides/tickValues.js +19 -21
  167. package/src/actions/index.js +15 -2
  168. package/src/actions/intervals/resolve.js +35 -15
  169. package/src/actions/marks/arc/actions.js +78 -20
  170. package/src/actions/marks/area/actions.js +51 -12
  171. package/src/actions/marks/area/materialize.js +22 -3
  172. package/src/actions/marks/bar/create.js +11 -2
  173. package/src/actions/marks/bar/edit.js +20 -3
  174. package/src/actions/marks/bar/materialize.js +33 -9
  175. package/src/actions/marks/index.js +14 -0
  176. package/src/actions/marks/line/actions.js +71 -113
  177. package/src/actions/marks/line/materialize.js +57 -56
  178. package/src/actions/marks/point/create.js +8 -2
  179. package/src/actions/marks/point/edit.js +29 -21
  180. package/src/actions/marks/point/index.js +1 -0
  181. package/src/actions/marks/point/jitter.js +3 -0
  182. package/src/actions/marks/point/materialize.js +154 -16
  183. package/src/actions/marks/point/packing.js +68 -0
  184. package/src/actions/marks/rect/actions.js +33 -12
  185. package/src/actions/marks/references.js +658 -0
  186. package/src/actions/marks/remove.js +87 -4
  187. package/src/actions/marks/rule/actions.js +77 -9
  188. package/src/actions/marks/rule/appearance.js +26 -0
  189. package/src/actions/marks/shared.js +13 -13
  190. package/src/actions/marks/text/actions.js +296 -13
  191. package/src/actions/marks/text/index.js +4 -0
  192. package/src/actions/marks/text/layout.js +156 -8
  193. package/src/actions/marks/text/removal.js +173 -0
  194. package/src/actions/marks/text/selection.js +73 -0
  195. package/src/actions/marks/tick/actions.js +22 -5
  196. package/src/actions/primitives/createGraphics.js +3 -1
  197. package/src/actions/primitives/editGraphics.js +26 -0
  198. package/src/actions/primitives/semantic.js +3 -2
  199. package/src/actions/primitives/semanticAction.js +120 -10
  200. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  201. package/src/actions/primitives/semanticValidation/index.js +9 -1
  202. package/src/actions/primitives/semanticValidation/layer.js +23 -1
  203. package/src/actions/primitives/semanticValidation/scale.js +38 -8
  204. package/src/actions/regression/components.js +26 -11
  205. package/src/actions/regression/create.js +11 -3
  206. package/src/actions/regression/edit.js +40 -13
  207. package/src/actions/regression/resolve.js +1 -1
  208. package/src/actions/resources/index.js +7 -0
  209. package/src/actions/resources/remove.js +172 -0
  210. package/src/actions/scales/channels.js +212 -0
  211. package/src/actions/scales/consumers/common.js +21 -54
  212. package/src/actions/scales/consumers/families.js +28 -14
  213. package/src/actions/scales/consumers/index.js +42 -4
  214. package/src/actions/scales/consumers/seriesLayout.js +57 -12
  215. package/src/actions/scales/create.js +58 -26
  216. package/src/actions/scales/definitions.js +109 -39
  217. package/src/actions/scales/edit.js +101 -34
  218. package/src/actions/scales/editPolicy.js +95 -35
  219. package/src/actions/scales/index.js +6 -0
  220. package/src/actions/scales/materialize.js +14 -49
  221. package/src/actions/scales/offset.js +48 -0
  222. package/src/actions/scales/parallel.js +71 -0
  223. package/src/actions/scales/patch.js +1 -1
  224. package/src/actions/scales/preview.js +140 -0
  225. package/src/actions/scales/quantitativeColor.js +9 -5
  226. package/src/actions/selection/actions.js +154 -30
  227. package/src/actions/selection/index.js +3 -0
  228. package/src/actions/theme/actions.js +98 -0
  229. package/src/actions/theme/composition.js +238 -0
  230. package/src/actions/theme/index.js +16 -0
  231. package/src/actions/theme/reconcile.js +1130 -0
  232. package/src/actions/theme/state.js +160 -0
  233. package/src/actions/titles/actions.js +11 -10
  234. package/src/actions/titles/resolve.js +1 -40
  235. package/src/actions/violinPlots/create.js +90 -43
  236. package/src/actions/violinPlots/edit.js +192 -0
  237. package/src/actions/violinPlots/index.js +2 -0
  238. package/src/core/ChartProgram.js +7 -0
  239. package/src/core/action.js +32 -2
  240. package/src/core/compositionState.js +141 -8
  241. package/src/core/programState.js +20 -0
  242. package/src/core/resourceReferences.js +332 -0
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/grammar/aggregate.js +66 -23
  245. package/src/grammar/arcs.js +58 -1
  246. package/src/grammar/areaEndpoints.js +46 -0
  247. package/src/grammar/areaSeries.js +87 -41
  248. package/src/grammar/bars/aggregate.js +37 -13
  249. package/src/grammar/bars/geometry.js +44 -0
  250. package/src/grammar/bars/policy.js +9 -7
  251. package/src/grammar/bin.js +226 -0
  252. package/src/grammar/categoryOrder.js +34 -0
  253. package/src/grammar/complete.js +279 -0
  254. package/src/grammar/computed.js +351 -0
  255. package/src/grammar/coordinates.js +37 -0
  256. package/src/grammar/density.js +239 -37
  257. package/src/grammar/displayLabels.js +57 -0
  258. package/src/grammar/ecdf.js +198 -0
  259. package/src/grammar/facets/dependencies.js +133 -1
  260. package/src/grammar/facets/guides.js +3 -0
  261. package/src/grammar/facets/index.js +123 -52
  262. package/src/grammar/facets/scales.js +15 -18
  263. package/src/grammar/fold.js +123 -0
  264. package/src/grammar/horizon.js +6 -4
  265. package/src/grammar/impute.js +340 -0
  266. package/src/grammar/interval.js +37 -20
  267. package/src/grammar/lineSeries.js +20 -63
  268. package/src/grammar/markFilter.js +22 -4
  269. package/src/grammar/markLabelSelection.js +57 -0
  270. package/src/grammar/markLabels.js +110 -0
  271. package/src/grammar/normalize.js +366 -0
  272. package/src/grammar/parallelAxisTitles.js +16 -0
  273. package/src/grammar/pathSeries.js +109 -0
  274. package/src/grammar/pointPacking.js +183 -0
  275. package/src/grammar/pointShapes.js +13 -4
  276. package/src/grammar/polar.js +90 -8
  277. package/src/grammar/positionDatum.js +12 -0
  278. package/src/grammar/rects.js +9 -1
  279. package/src/grammar/regression/derive.js +4 -0
  280. package/src/grammar/regression/index.js +8 -2
  281. package/src/grammar/regression/models.js +22 -7
  282. package/src/grammar/regression/parameters.js +63 -13
  283. package/src/grammar/rotation.js +29 -0
  284. package/src/grammar/roundedRect.js +180 -0
  285. package/src/grammar/rules.js +8 -21
  286. package/src/grammar/scales/color.js +23 -1
  287. package/src/grammar/scales/colorConsumers.js +57 -0
  288. package/src/grammar/scales/continuous.js +2 -2
  289. package/src/grammar/scales/definition.js +42 -5
  290. package/src/grammar/scales/discretized.js +11 -5
  291. package/src/grammar/scales/fields.js +38 -8
  292. package/src/grammar/scales/index.js +1 -0
  293. package/src/grammar/scales/mapping.js +2 -0
  294. package/src/grammar/scales/ordinal.js +12 -3
  295. package/src/grammar/scales/policies.js +2 -2
  296. package/src/grammar/scales/radial.js +55 -0
  297. package/src/grammar/scales/size.js +456 -0
  298. package/src/grammar/scales/types.js +1 -0
  299. package/src/grammar/schemas/concreteGraphic.js +14 -2
  300. package/src/grammar/schemas/graphic.js +15 -0
  301. package/src/grammar/schemas/graphicBounds.js +179 -30
  302. package/src/grammar/schemas/semanticPath.js +23 -4
  303. package/src/grammar/stack.js +170 -0
  304. package/src/grammar/statisticalReference.js +103 -0
  305. package/src/grammar/statistics/confidenceInterval.js +101 -0
  306. package/src/grammar/strokeStyle.js +77 -0
  307. package/src/grammar/summary.js +245 -0
  308. package/src/grammar/text.js +22 -16
  309. package/src/grammar/timeUnit.js +234 -13
  310. package/src/grammar/transformTopology.js +10 -0
  311. package/src/grammar/transforms.js +418 -16
  312. package/src/grammar/valueFormat.js +112 -0
  313. package/src/grammar/weightedStatistics.js +374 -0
  314. package/src/grammar/window.js +232 -21
  315. package/src/layout/aspect.js +80 -0
  316. package/src/layout/facets.js +100 -12
  317. package/src/layout/guideCollisions.js +12 -0
  318. package/src/layout/labels.js +343 -0
  319. package/src/layout/legend.js +2 -10
  320. package/src/layout/legendItems.js +102 -0
  321. package/src/layout/legendLane.js +100 -11
  322. package/src/materialization/bars/aggregate.js +56 -147
  323. package/src/materialization/bars/histogram.js +104 -20
  324. package/src/materialization/bars/ranged.js +19 -3
  325. package/src/materialization/bars/resolve.js +7 -4
  326. package/src/materialization/categorySlotOffset.js +40 -0
  327. package/src/materialization/composition.js +3 -1
  328. package/src/materialization/compositionSnapshot.js +1 -1
  329. package/src/materialization/coordinateBounds.js +204 -0
  330. package/src/materialization/dependencies.js +93 -16
  331. package/src/materialization/encodings.js +1 -1
  332. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  333. package/src/materialization/facetGuides/placement.js +8 -2
  334. package/src/materialization/facetGuides/preparation.js +12 -7
  335. package/src/materialization/facetHeaders.js +346 -0
  336. package/src/materialization/facets.js +23 -66
  337. package/src/materialization/guides/layout.js +57 -0
  338. package/src/materialization/guides/resources.js +28 -2
  339. package/src/materialization/legends.js +11 -2
  340. package/src/materialization/marks/capabilities.js +20 -5
  341. package/src/materialization/marks/grouping.js +52 -0
  342. package/src/materialization/marks/index.js +68 -27
  343. package/src/materialization/marks/policies.js +4 -1
  344. package/src/materialization/rect.js +55 -42
  345. package/src/materialization/rectConfig.js +5 -1
  346. package/src/materialization/rowEncoding.js +13 -13
  347. package/src/materialization/scaleGuideDependencies.js +8 -5
  348. package/src/materialization/scales/map.js +6 -3
  349. package/src/materialization/scales/policies/arc.js +42 -0
  350. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  351. package/src/materialization/scales/policies/offset.js +50 -8
  352. package/src/materialization/scales/policies/series.js +5 -1
  353. package/src/materialization/scales/resolve.js +65 -17
  354. package/src/materialization/selection/compatibility.js +11 -0
  355. package/src/materialization/selection/items/arc.js +28 -4
  356. package/src/materialization/selection/items/bar.js +42 -71
  357. package/src/materialization/selection/items/common.js +27 -6
  358. package/src/materialization/selection/items/path.js +3 -13
  359. package/src/materialization/selection/items/rect.js +35 -8
  360. package/src/materialization/selection/path.js +1 -1
  361. package/src/materialization/selection/point.js +3 -1
  362. package/src/materialization/text.js +324 -14
  363. package/src/mcp/adapter.js +2 -2
  364. package/src/renderers/canvas/circle.js +2 -3
  365. package/src/renderers/canvas/line.js +2 -3
  366. package/src/renderers/canvas/native.js +4 -4
  367. package/src/renderers/canvas/path.js +2 -3
  368. package/src/renderers/canvas/rect.js +2 -3
  369. package/src/renderers/canvas/stroke.js +11 -0
  370. package/src/renderers/svg.js +19 -0
  371. package/src/selectors/datasets.js +44 -0
  372. package/src/selectors/layers.js +7 -2
  373. package/src/selectors/scales.js +6 -0
  374. package/src/theme/defaults.js +140 -0
  375. package/types/basic.d.ts +17 -2
  376. package/types/index.d.ts +211 -0
  377. package/types/program.d.ts +2182 -204
  378. package/src/materialization/bars/grouped.js +0 -144
@@ -1,15 +1,11 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
- import {
3
- readNominalField,
4
- readQuantitativeField,
5
- readTemporalField
6
- } from "./scales/index.js";
7
- import {
8
- layoutSeriesPartition,
9
- validateColorLayout
10
- } from "./seriesLayout.js";
2
+ import { readNominalField, readQuantitativeField, readTemporalField } from "./scales/index.js";
3
+ import { layoutSeriesPartition, validateColorLayout } from "./seriesLayout.js";
11
4
  import { stableOrderPathValues } from "./pathOrder.js";
12
5
  import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
6
+ import { validatePathSeriesAppearance } from "./pathSeries.js";
7
+ import { readAreaEndpoint, validateAreaEndpointPair } from "./areaEndpoints.js";
8
+ import { validateGeneratedItemLimit } from "../core/validation.js";
13
9
 
14
10
  function indexSegments(segments) {
15
11
  const indexed = [];
@@ -21,7 +17,7 @@ export function deriveAreaSeries(rows, layer) {
21
17
  if (layer?.mark?.type !== "area") {
22
18
  throw new Error("Area series derivation requires a semantic area mark.");
23
19
  }
24
- const { x, y, x2, y2, group, color } = layer.encoding ?? {};
20
+ const { x, y, x2, y2 } = layer.encoding ?? {};
25
21
  const vertical =
26
22
  ["quantitative", "temporal"].includes(x?.fieldType) &&
27
23
  y?.fieldType === "quantitative" &&
@@ -37,26 +33,19 @@ export function deriveAreaSeries(rows, layer) {
37
33
  `Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
38
34
  );
39
35
  }
40
- if (group !== undefined && group.fieldType !== "nominal") {
41
- throw new Error(`Area group encoding on mark "${layer.id}" must be nominal.`);
42
- }
36
+ const grouping = validatePathSeriesAppearance(rows, layer);
43
37
  const orientation = vertical ? "vertical" : "horizontal";
44
38
  const independent = vertical ? x : y;
45
39
  const independentValues = independent.fieldType === "temporal"
46
- ? readTemporalField(rows, independent.field)
40
+ ? readTemporalField(rows, independent.field, independent.temporalUnit)
47
41
  : readQuantitativeField(rows, independent.field);
48
- const lower = vertical
49
- ? readQuantitativeField(rows, y.field)
50
- : readQuantitativeField(rows, x.field);
51
- const upper = vertical
52
- ? readQuantitativeField(rows, y2.field)
53
- : readQuantitativeField(rows, x2.field);
54
- const groupValues = group === undefined
55
- ? rows.map(() => undefined)
56
- : readNominalField(rows, group.field);
57
- const colorValues = color === undefined
58
- ? rows.map(() => undefined)
59
- : readNominalField(rows, color.field);
42
+ if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
43
+ const primary = vertical ? y : x;
44
+ const secondary = vertical ? y2 : x2;
45
+ validateAreaEndpointPair(primary, secondary);
46
+ const missing = layer.mark.missing ?? "error";
47
+ const lower = readAreaEndpoint(rows, primary, missing);
48
+ const upper = readAreaEndpoint(rows, secondary, missing);
60
49
  const pathOrder = layer.encoding?.pathOrder;
61
50
  const orderValues = pathOrder === undefined
62
51
  ? undefined
@@ -64,32 +53,24 @@ export function deriveAreaSeries(rows, layer) {
64
53
  const groups = new Map();
65
54
 
66
55
  for (let index = 0; index < rows.length; index += 1) {
67
- const key = groupValues[index];
56
+ const tuple = grouping.map(field => rows[index][field]);
57
+ const key = JSON.stringify(tuple);
68
58
  const series = groups.get(key) ?? {
69
- key: {
70
- ...(group === undefined ? {} : { [group.field]: key }),
71
- ...(color === undefined ? {} : { [color.field]: colorValues[index] })
72
- },
59
+ key: Object.fromEntries(grouping.map((field, i) => [field, tuple[i]])),
73
60
  values: []
74
61
  };
75
- if (
76
- color !== undefined &&
77
- !Object.is(series.key[color.field], colorValues[index])
78
- ) {
79
- throw new Error(
80
- `Area series "${String(key)}" must have one color value.`
81
- );
82
- }
83
62
  series.values.push(vertical
84
63
  ? {
85
64
  x: independentValues[index],
86
65
  y: lower[index],
87
66
  y2: upper[index],
67
+ ...(missing === "break" ? { sourceIndex: index } : {}),
88
68
  ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
89
69
  }
90
70
  : {
91
71
  x: lower[index],
92
72
  x2: upper[index],
73
+ ...(missing === "break" ? { sourceIndex: index } : {}),
93
74
  y: independentValues[index],
94
75
  ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
95
76
  });
@@ -98,7 +79,8 @@ export function deriveAreaSeries(rows, layer) {
98
79
  if (groups.size === 0) {
99
80
  throw new Error(`Area mark "${layer.id}" has no values.`);
100
81
  }
101
- const series = [...groups.values()].map(item => {
82
+ layoutRawAreaGroups(groups, { layer, vertical, primary, secondary });
83
+ const series = [...groups.values()].flatMap(item => {
102
84
  const key = vertical ? "x" : "y";
103
85
  const values = pathOrder === undefined
104
86
  ? item.values.sort((left, right) => left[key] - right[key])
@@ -107,6 +89,22 @@ export function deriveAreaSeries(rows, layer) {
107
89
  item.values.map(value => value.pathOrder),
108
90
  pathOrder.order
109
91
  );
92
+ if (missing === "break") {
93
+ const segments = [];
94
+ let segment = [];
95
+ const emit = () => {
96
+ if (segment.length >= 2) segments.push({ key: item.key,
97
+ sourceIndices: segment.map(value => value.sourceIndex),
98
+ values: segment.map(({ sourceIndex, ...value }) => value) });
99
+ segment = [];
100
+ };
101
+ for (const value of values) {
102
+ if ((vertical ? value.y == null || value.y2 == null : value.x == null || value.x2 == null)) emit();
103
+ else segment.push(value);
104
+ }
105
+ emit();
106
+ return segments;
107
+ }
110
108
  if (values.length < 2) {
111
109
  throw new Error(
112
110
  `Area series on mark "${layer.id}" requires at least two points.`
@@ -114,6 +112,7 @@ export function deriveAreaSeries(rows, layer) {
114
112
  }
115
113
  return { key: item.key, values };
116
114
  });
115
+ if (series.length === 0) throw new Error(`Area mark "${layer.id}" has no valid segment with at least two points.`);
117
116
  return cloneAndFreeze({
118
117
  orientation,
119
118
  xValues: series.flatMap(item => item.values.flatMap(value =>
@@ -151,7 +150,7 @@ export function deriveCenteredAreaSeries(rows, layer) {
151
150
  );
152
151
  }
153
152
  const xValues = x.fieldType === "temporal"
154
- ? readTemporalField(rows, x.field)
153
+ ? readTemporalField(rows, x.field, x.temporalUnit)
155
154
  : readQuantitativeField(rows, x.field);
156
155
  const yValues = readQuantitativeField(rows, y.field);
157
156
  const groups = readNominalField(rows, group.field);
@@ -359,3 +358,50 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
359
358
  }))
360
359
  });
361
360
  }
361
+
362
+ export function layoutRawAreaGroups(groups, { layer, vertical, primary, secondary }) {
363
+ const mode = layer.layout?.mode ?? "overlay";
364
+ validateColorLayout(mode);
365
+ if (mode === "overlay") return;
366
+ if (mode === "group" || (mode === "center" && !vertical)) {
367
+ throw new Error(`Area layout "${mode}" is incompatible with this orientation.`);
368
+ }
369
+ const primaryDatum = Object.hasOwn(primary, "datum");
370
+ const secondaryDatum = Object.hasOwn(secondary, "datum");
371
+ if (primaryDatum === secondaryDatum || (primaryDatum ? primary.datum : secondary.datum) !== 0) {
372
+ throw new Error("Stacked area requires one value field and one zero datum endpoint.");
373
+ }
374
+ const position = vertical ? "x" : "y";
375
+ const lower = vertical ? "y" : "x", upper = `${lower}2`;
376
+ const measure = primaryDatum ? upper : lower;
377
+ const series = [...groups.values()];
378
+ const indices = series.map(item => {
379
+ const index = new Map();
380
+ for (const value of item.values) {
381
+ if (index.has(value[position])) throw new Error("Area layout requires unique group/position rows.");
382
+ index.set(value[position], value);
383
+ }
384
+ return index;
385
+ });
386
+ const positions = [...new Set(series.flatMap(item => item.values.map(value => value[position])))].sort((a, b) => a - b);
387
+ validateGeneratedItemLimit(positions.length * series.length, "Area layout cell count");
388
+ if (indices.some(index => index.size !== positions.length || positions.some(value => !index.has(value)))) {
389
+ throw new Error("Area layout requires an aligned row for every group and position.");
390
+ }
391
+ for (const position of positions) {
392
+ const points = indices.map(index => index.get(position));
393
+ if (points.some(point => point[measure] == null)) {
394
+ for (const point of points) { point[lower] = null; point[upper] = null; }
395
+ continue;
396
+ }
397
+ const segments = layoutSeriesPartition(points.map(point => point[measure]), mode);
398
+ const byIndex = new Map(segments.map(segment => [segment.index, segment]));
399
+ let endpoint = mode === "center" ? segments[0]?.start ?? 0 : 0;
400
+ points.forEach((point, index) => {
401
+ const segment = byIndex.get(index);
402
+ point[lower] = segment?.start ?? (mode === "diverging" ? 0 : endpoint);
403
+ point[upper] = segment?.end ?? (mode === "diverging" ? 0 : endpoint);
404
+ if (segment !== undefined && mode !== "diverging") endpoint = segment.end;
405
+ });
406
+ }
407
+ }
@@ -1,15 +1,9 @@
1
+ import { readSeriesIdentity } from "../pathSeries.js";
1
2
  import { cloneAndFreeze } from "../../core/immutable.js";
2
3
  import { readNominalField, readTemporalField } from "../scales/index.js";
3
- import {
4
- aggregateRows,
5
- validateAggregateFieldType,
6
- validateAggregateFieldValues,
7
- } from "../aggregate.js";
8
- import {
9
- BAR_GRAINS,
10
- resolveBarChannels,
11
- resolveBarGrain
12
- } from "./policy.js";
4
+ import { aggregateRows, validateAggregateFieldType, validateAggregateFieldValues } from "../aggregate.js";
5
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain, resolveBarColorLayout } from "./policy.js";
6
+ import { layoutSeriesPartition } from "../seriesLayout.js";
13
7
 
14
8
  function requireAggregateBarEncoding(layer) {
15
9
  if (layer?.mark?.type !== "bar") {
@@ -33,7 +27,7 @@ export function deriveBarAggregates(rows, layer) {
33
27
  const { channels, category, measure } = requireAggregateBarEncoding(layer);
34
28
  validateAggregateFieldValues(rows, measure.field, measure.fieldType);
35
29
  const categoryValues = category.fieldType === "temporal"
36
- ? readTemporalField(rows, category.field)
30
+ ? readTemporalField(rows, category.field, category.temporalUnit)
37
31
  : readNominalField(rows, category.field);
38
32
  const color = layer.encoding?.color;
39
33
  let colorValues;
@@ -52,18 +46,26 @@ export function deriveBarAggregates(rows, layer) {
52
46
  );
53
47
  }
54
48
  }
49
+ const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
55
50
  const groups = new Map();
56
51
 
57
52
  for (let index = 0; index < rows.length; index += 1) {
58
53
  const categoryValue = categoryValues[index];
59
54
  const colorValue = colorValues?.[index];
60
- const key = JSON.stringify([categoryValue, colorValue]);
55
+ const seriesValue = identity?.values[index];
56
+ const key = JSON.stringify([categoryValue, identity === undefined ? colorValue : seriesValue]);
61
57
  const group = groups.get(key) ?? {
62
58
  category: categoryValue,
59
+ ...(identity === undefined ? {} : { series: seriesValue }),
63
60
  ...(colorValues === undefined ? {} : { color: colorValue }),
64
- rows: []
61
+ rows: [],
62
+ sourceIndices: []
65
63
  };
64
+ if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
65
+ throw new Error("Bar color requires one categorical value within each aggregate cell.");
66
+ }
66
67
  group.rows.push(rows[index]);
68
+ group.sourceIndices.push(index);
67
69
  groups.set(key, group);
68
70
  }
69
71
 
@@ -82,6 +84,8 @@ export function deriveBarAggregates(rows, layer) {
82
84
  x: channels.category === "x" ? group.category : value,
83
85
  y: channels.category === "y" ? group.category : value,
84
86
  ...(color === undefined ? {} : { color: aggregateColor }),
87
+ ...(identity === undefined ? {} : { series: group.series }),
88
+ sourceIndices: group.sourceIndices,
85
89
  count: group.rows.length
86
90
  }];
87
91
  });
@@ -96,3 +100,23 @@ export function deriveBarAggregates(rows, layer) {
96
100
  values
97
101
  });
98
102
  }
103
+
104
+ export function deriveSeriesBarCells(rows, layer, categoryDomain, seriesDomain) {
105
+ const channels = resolveBarChannels(layer);
106
+ const values = deriveBarAggregates(rows, layer).values;
107
+ const identity = readSeriesIdentity(rows, layer);
108
+ const categories = categoryDomain ?? [...new Set(values.map(cell => cell[channels.category]))].sort((a, b) => a - b);
109
+ const seriesKey = cell => layer.encoding.group !== undefined ? cell.series : layer.encoding.color?.fieldType === "quantitative" ? undefined : cell.color;
110
+ const series = seriesDomain ?? (layer.encoding.group !== undefined ? identity.domain : [...new Set(values.map(seriesKey))]);
111
+ const cells = new Map(values.map(cell => [JSON.stringify([cell[channels.category], seriesKey(cell)]), cell]));
112
+ const layout = resolveBarColorLayout(layer);
113
+ return categories.flatMap(category => {
114
+ const partition = series.map(key => cells.get(JSON.stringify([category, key])));
115
+ const segments = layout === "group"
116
+ ? partition.flatMap((cell, index) => cell === undefined ? [] : [{ index, start: 0, end: cell[channels.measure] }])
117
+ : layoutSeriesPartition(partition.map(cell => cell?.[channels.measure] ?? 0), layout);
118
+ return segments.flatMap(segment => partition[segment.index] === undefined ? [] : [{
119
+ cell: partition[segment.index], start: segment.start, end: segment.end, seriesIndex: segment.index
120
+ }]);
121
+ });
122
+ }
@@ -45,6 +45,50 @@ export function normalizeOffsetPadding(options = {}, existing, channel = "xOffse
45
45
  return cloneAndFreeze({ paddingInner, paddingOuter });
46
46
  }
47
47
 
48
+ export function normalizeOffsetScalePolicy(
49
+ options = {},
50
+ existing,
51
+ channel = "xOffset"
52
+ ) {
53
+ const hasPadding = Object.hasOwn(options, "padding");
54
+ const hasInner = Object.hasOwn(options, "paddingInner");
55
+ const hasOuter = Object.hasOwn(options, "paddingOuter");
56
+ if (hasPadding && (hasInner || hasOuter)) {
57
+ throw new Error(
58
+ `${channel} padding cannot be combined with paddingInner or paddingOuter.`
59
+ );
60
+ }
61
+ const padding = hasPadding ? options.padding : undefined;
62
+ if (hasPadding && (!Number.isFinite(padding) || padding < 0 || padding >= 1)) {
63
+ throw new RangeError(
64
+ `${channel} padding must be from 0 (inclusive) to 1 (exclusive).`
65
+ );
66
+ }
67
+ const paddingInner = hasPadding
68
+ ? padding
69
+ : hasInner ? options.paddingInner : existing?.paddingInner ?? 0;
70
+ const paddingOuter = hasPadding
71
+ ? padding
72
+ : hasOuter ? options.paddingOuter : existing?.paddingOuter ?? 0;
73
+ const align = Object.hasOwn(options, "align")
74
+ ? options.align
75
+ : existing?.align ?? 0.5;
76
+ if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
77
+ throw new RangeError(
78
+ `${channel} paddingInner must be from 0 (inclusive) to 1 (exclusive).`
79
+ );
80
+ }
81
+ if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
82
+ throw new RangeError(
83
+ `${channel} paddingOuter must be a non-negative finite number.`
84
+ );
85
+ }
86
+ if (!Number.isFinite(align) || align < 0 || align > 1) {
87
+ throw new RangeError(`${channel} align must be between 0 and 1.`);
88
+ }
89
+ return cloneAndFreeze({ paddingInner, paddingOuter, align });
90
+ }
91
+
48
92
  export function resolveBarWidth(config, slotBandwidth) {
49
93
  if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
50
94
  throw new Error("Bar width requires a positive resolved slot bandwidth.");
@@ -11,7 +11,7 @@ export const BAR_ORIENTATIONS = Object.freeze({
11
11
  horizontal: "horizontal"
12
12
  });
13
13
 
14
- function isCategory(encoding) {
14
+ export function isBarCategoryEncoding(encoding) {
15
15
  return ["nominal", "ordinal", "temporal"].includes(encoding?.fieldType);
16
16
  }
17
17
 
@@ -27,14 +27,14 @@ export function resolveBarOrientation(layer) {
27
27
  if (x?.bin !== undefined && y?.aggregate === "count") {
28
28
  return BAR_ORIENTATIONS.vertical;
29
29
  }
30
- if (isCategory(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
30
+ if (isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
31
31
  return BAR_ORIENTATIONS.vertical;
32
32
  }
33
- if (isCategory(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative") {
33
+ if (isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative") {
34
34
  return BAR_ORIENTATIONS.horizontal;
35
35
  }
36
- if (isCategory(x) && isMeasure(y)) return BAR_ORIENTATIONS.vertical;
37
- if (isMeasure(x) && isCategory(y)) return BAR_ORIENTATIONS.horizontal;
36
+ if (isBarCategoryEncoding(x) && isMeasure(y)) return BAR_ORIENTATIONS.vertical;
37
+ if (isMeasure(x) && isBarCategoryEncoding(y)) return BAR_ORIENTATIONS.horizontal;
38
38
  return undefined;
39
39
  }
40
40
 
@@ -63,8 +63,8 @@ export function resolveBarGrain(layer) {
63
63
  return BAR_GRAINS.histogram;
64
64
  }
65
65
  if (
66
- (isCategory(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") ||
67
- (isCategory(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative")
66
+ (isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") ||
67
+ (isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative")
68
68
  ) return BAR_GRAINS.ranged;
69
69
  if (resolveBarOrientation(layer) !== undefined) {
70
70
  return BAR_GRAINS.aggregate;
@@ -73,6 +73,7 @@ export function resolveBarGrain(layer) {
73
73
  }
74
74
 
75
75
  export function inferBarColorLayout(layer) {
76
+ if (layer?.layout?.mode !== undefined) return layer.layout.mode;
76
77
  if (layer?.encoding?.color?.layout !== undefined) {
77
78
  return layer.encoding.color.layout;
78
79
  }
@@ -84,6 +85,7 @@ export function inferBarColorLayout(layer) {
84
85
  }
85
86
 
86
87
  export function resolveBarColorLayout(layer) {
88
+ if (layer?.layout?.mode !== undefined) return layer.layout.mode;
87
89
  if (layer?.encoding?.color?.layout !== undefined) {
88
90
  return layer.encoding.color.layout;
89
91
  }
@@ -0,0 +1,226 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ findHistogramBinIndex,
4
+ normalizeHistogramBin,
5
+ resolveHistogramBins
6
+ } from "./histogram.js";
7
+ import {
8
+ normalizeStatisticalWeight,
9
+ readStatisticalWeights,
10
+ sumEntryWeights,
11
+ summarizeStatisticalWeights,
12
+ validateWeightedNumericFields,
13
+ weightedRows
14
+ } from "./weightedStatistics.js";
15
+
16
+ const TRANSFORM_KEYS = Object.freeze([
17
+ "type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
18
+ "members", "as", "weight", "resolved"
19
+ ]);
20
+ const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
21
+ const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
22
+
23
+ function rejectUnknownKeys(value, supported, label) {
24
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
25
+ if (unknown !== undefined) {
26
+ throw new Error(`Unknown ${label} property "${unknown}".`);
27
+ }
28
+ }
29
+
30
+ function requireField(value, label) {
31
+ if (typeof value !== "string" || value.length === 0) {
32
+ throw new TypeError(`${label} must be a non-empty string.`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function normalizeAs(field, value, members) {
38
+ const requested = value ?? {};
39
+ if (!isPlainObject(requested)) return requested;
40
+ const result = {
41
+ ...requested,
42
+ lower: requested.lower ?? `${field}_start`,
43
+ upper: requested.upper ?? `${field}_end`,
44
+ count: requested.count ?? "count"
45
+ };
46
+ if (members) result.members = requested.members ?? "members";
47
+ else if (Object.hasOwn(requested, "members")) result.members = requested.members;
48
+ return result;
49
+ }
50
+
51
+ export function normalizeBinTransform({
52
+ field,
53
+ maxBins,
54
+ step,
55
+ boundaries,
56
+ extent,
57
+ nice,
58
+ zero,
59
+ includeEmpty,
60
+ members,
61
+ as,
62
+ weight
63
+ } = {}) {
64
+ const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
65
+ if (selected.length > 1) {
66
+ throw new Error("Bin data accepts only one of maxBins, step, or boundaries.");
67
+ }
68
+ const bin = normalizeHistogramBin(
69
+ boundaries !== undefined ? { boundaries }
70
+ : step !== undefined ? { step }
71
+ : { maxBins: maxBins ?? 10 }
72
+ );
73
+ const resolvedMembers = members ?? false;
74
+ const transform = {
75
+ type: "bin",
76
+ field,
77
+ bin,
78
+ extent: extent ?? "auto",
79
+ nice: nice ?? true,
80
+ zero: zero ?? false,
81
+ includeEmpty: includeEmpty ?? true,
82
+ members: resolvedMembers,
83
+ as: normalizeAs(field, as, resolvedMembers),
84
+ ...(weight === undefined
85
+ ? {}
86
+ : { weight: normalizeStatisticalWeight(weight, "Bin weight") })
87
+ };
88
+ validateBinTransform(transform);
89
+ return cloneAndFreeze(transform);
90
+ }
91
+
92
+ export function validateBinTransform(transform) {
93
+ if (!isPlainObject(transform)) {
94
+ throw new TypeError("Bin transform must be a plain object.");
95
+ }
96
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "bin transform");
97
+ if (transform.type !== "bin") {
98
+ throw new Error(`Unsupported bin transform "${transform.type}".`);
99
+ }
100
+ requireField(transform.field, "Bin field");
101
+ if (transform.weight !== undefined) {
102
+ normalizeStatisticalWeight(transform.weight, "Bin weight");
103
+ }
104
+ normalizeHistogramBin(transform.bin);
105
+ if (
106
+ transform.extent !== "auto" &&
107
+ (!Array.isArray(transform.extent) || transform.extent.length !== 2 ||
108
+ !transform.extent.every(Number.isFinite) || transform.extent[0] >= transform.extent[1])
109
+ ) {
110
+ throw new TypeError("Bin extent must be auto or two ascending finite numbers.");
111
+ }
112
+ for (const property of ["nice", "zero", "includeEmpty", "members"]) {
113
+ if (typeof transform[property] !== "boolean") {
114
+ throw new TypeError(`Bin ${property} must be a boolean.`);
115
+ }
116
+ }
117
+ if (!isPlainObject(transform.as)) {
118
+ throw new TypeError("Bin as must be a plain object.");
119
+ }
120
+ rejectUnknownKeys(transform.as, AS_KEYS, "bin as");
121
+ const required = ["lower", "upper", "count", ...(transform.members ? ["members"] : [])];
122
+ if (!transform.members && Object.hasOwn(transform.as, "members")) {
123
+ throw new Error("Bin as.members requires members: true.");
124
+ }
125
+ const outputs = required.map(key => requireField(transform.as[key], `Bin as.${key}`));
126
+ if (new Set(outputs).size !== outputs.length) {
127
+ throw new Error("Bin output fields must be unique.");
128
+ }
129
+ if (transform.resolved !== undefined) {
130
+ if (!isPlainObject(transform.resolved)) {
131
+ throw new TypeError("Bin resolved must be a plain object.");
132
+ }
133
+ rejectUnknownKeys(transform.resolved, RESOLVED_KEYS, "bin resolved");
134
+ resolveHistogramBins({
135
+ values: transform.resolved.boundaries ?? [],
136
+ bin: { boundaries: transform.resolved.boundaries },
137
+ domain: transform.resolved.domain,
138
+ nice: transform.nice,
139
+ zero: transform.zero
140
+ });
141
+ }
142
+ return transform;
143
+ }
144
+
145
+ function readValues(rows, field) {
146
+ return rows.map((row, index) => {
147
+ const value = row[field];
148
+ if (!Number.isFinite(value)) {
149
+ throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
150
+ }
151
+ return value;
152
+ });
153
+ }
154
+
155
+ export function deriveBinRows(rows, transform) {
156
+ validateBinTransform(transform);
157
+ const values = readValues(rows, transform.field);
158
+ const weights = transform.weight === undefined
159
+ ? undefined
160
+ : readStatisticalWeights(rows, transform.weight, "Bin");
161
+ if (weights !== undefined) {
162
+ validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
163
+ }
164
+ const weightSummary = weights === undefined
165
+ ? undefined
166
+ : summarizeStatisticalWeights(
167
+ weights.entries,
168
+ weights.definition.kind,
169
+ "Bin data"
170
+ );
171
+ const contributingValues = weightSummary === undefined
172
+ ? values
173
+ : weightSummary.positive.map(entry => entry.row[transform.field]);
174
+ if (
175
+ transform.extent !== "auto" &&
176
+ contributingValues.some(value =>
177
+ value < transform.extent[0] || value > transform.extent[1]
178
+ )
179
+ ) {
180
+ throw new RangeError("Bin extent must contain the histogram data extent.");
181
+ }
182
+ const resolved = resolveHistogramBins({
183
+ values: contributingValues,
184
+ bin: transform.bin,
185
+ domain: transform.extent,
186
+ nice: transform.nice,
187
+ zero: transform.zero
188
+ });
189
+ const members = resolved.boundaries.slice(0, -1).map(() => []);
190
+ const entries = weightSummary === undefined
191
+ ? rows.map((row, index) => ({ row, index }))
192
+ : weightSummary.positive;
193
+ entries.forEach(entry => {
194
+ const value = entry.row[transform.field];
195
+ const bin = findHistogramBinIndex(value, resolved.boundaries);
196
+ if (bin !== -1) members[bin].push(entry);
197
+ });
198
+ const output = members.flatMap((group, index) =>
199
+ !transform.includeEmpty && group.length === 0 ? [] : [{
200
+ [transform.as.lower]: resolved.boundaries[index],
201
+ [transform.as.upper]: resolved.boundaries[index + 1],
202
+ [transform.as.count]: weightSummary === undefined
203
+ ? group.length
204
+ : sumEntryWeights(group, weightSummary, "Bin mass"),
205
+ ...(transform.members
206
+ ? {
207
+ [transform.as.members]: weightSummary === undefined
208
+ ? group.map(entry => entry.row)
209
+ : weightedRows({ positive: group })
210
+ }
211
+ : {})
212
+ }]
213
+ );
214
+ return {
215
+ values: output,
216
+ resolved: {
217
+ domain: resolved.domain,
218
+ ...(resolved.step === undefined ? {} : { step: resolved.step }),
219
+ boundaries: resolved.boundaries
220
+ }
221
+ };
222
+ }
223
+
224
+ export function resolveBinTransform(result, transform) {
225
+ return cloneAndFreeze([{ ...transform, resolved: result.resolved }]);
226
+ }
@@ -2,6 +2,8 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { isNominalValue, readNominalField } from "./scales/fields.js";
3
3
  import { aggregateScalarValues } from "./aggregate.js";
4
4
 
5
+ export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
6
+
5
7
  const DIRECTIONS = ["ascending", "descending"];
6
8
  const AGGREGATES = ["sum", "mean", "min", "max"];
7
9
 
@@ -130,3 +132,35 @@ export function resolveCategoryOrder(rows, categoryField, order) {
130
132
  return direction * comparison || first.get(left) - first.get(right);
131
133
  }));
132
134
  }
135
+
136
+
137
+ export function normalizeLegendOrder(order) {
138
+ if (order === "scale") return order;
139
+ if (!isPlainObject(order)) {
140
+ throw new TypeError('Legend order must be "scale", { values }, or { channel }.');
141
+ }
142
+ const keys = Object.keys(order);
143
+ if (keys.length !== 1 || !["values", "channel"].includes(keys[0])) {
144
+ throw new Error("Legend order requires exactly one of values or channel.");
145
+ }
146
+ if (keys[0] === "values") return normalizeCategoryOrder({ values: order.values });
147
+ if (!CATEGORY_ORDER_CHANNELS.includes(order.channel)) {
148
+ throw new Error("Legend order channel must be x, y, or theta.");
149
+ }
150
+ return cloneAndFreeze({ channel: order.channel });
151
+ }
152
+
153
+ export function resolveLegendOrderDomain(domain, order, linked, observed = domain) {
154
+ if (order === undefined || order === "scale") return domain;
155
+ const normalized = normalizeLegendOrder(order);
156
+ if (normalized.values !== undefined) {
157
+ const seen = [...new Set(observed)].filter(value => domain.some(item => Object.is(item, value)));
158
+ const available = [...seen, ...domain.filter(value => !seen.some(item => Object.is(item, value)))];
159
+ return resolveCategoryOrder(available.map(value => ({ value })), "value", normalized);
160
+ }
161
+ if (!Array.isArray(linked) || linked.length !== domain.length ||
162
+ !domain.every(value => linked.some(other => Object.is(value, other)))) {
163
+ throw new Error("Linked legend order requires the same categorical domain.");
164
+ }
165
+ return cloneAndFreeze(linked);
166
+ }