ggaction 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -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.");
@@ -4,10 +4,18 @@ import {
4
4
  normalizeHistogramBin,
5
5
  resolveHistogramBins
6
6
  } from "./histogram.js";
7
+ import {
8
+ normalizeStatisticalWeight,
9
+ readStatisticalWeights,
10
+ sumEntryWeights,
11
+ summarizeStatisticalWeights,
12
+ validateWeightedNumericFields,
13
+ weightedRows
14
+ } from "./weightedStatistics.js";
7
15
 
8
16
  const TRANSFORM_KEYS = Object.freeze([
9
17
  "type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
10
- "members", "as", "resolved"
18
+ "members", "as", "weight", "resolved"
11
19
  ]);
12
20
  const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
13
21
  const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
@@ -50,7 +58,8 @@ export function normalizeBinTransform({
50
58
  zero,
51
59
  includeEmpty,
52
60
  members,
53
- as
61
+ as,
62
+ weight
54
63
  } = {}) {
55
64
  const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
56
65
  if (selected.length > 1) {
@@ -71,7 +80,10 @@ export function normalizeBinTransform({
71
80
  zero: zero ?? false,
72
81
  includeEmpty: includeEmpty ?? true,
73
82
  members: resolvedMembers,
74
- as: normalizeAs(field, as, resolvedMembers)
83
+ as: normalizeAs(field, as, resolvedMembers),
84
+ ...(weight === undefined
85
+ ? {}
86
+ : { weight: normalizeStatisticalWeight(weight, "Bin weight") })
75
87
  };
76
88
  validateBinTransform(transform);
77
89
  return cloneAndFreeze(transform);
@@ -86,6 +98,9 @@ export function validateBinTransform(transform) {
86
98
  throw new Error(`Unsupported bin transform "${transform.type}".`);
87
99
  }
88
100
  requireField(transform.field, "Bin field");
101
+ if (transform.weight !== undefined) {
102
+ normalizeStatisticalWeight(transform.weight, "Bin weight");
103
+ }
89
104
  normalizeHistogramBin(transform.bin);
90
105
  if (
91
106
  transform.extent !== "auto" &&
@@ -140,30 +155,60 @@ function readValues(rows, field) {
140
155
  export function deriveBinRows(rows, transform) {
141
156
  validateBinTransform(transform);
142
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]);
143
174
  if (
144
175
  transform.extent !== "auto" &&
145
- values.some(value => value < transform.extent[0] || value > transform.extent[1])
176
+ contributingValues.some(value =>
177
+ value < transform.extent[0] || value > transform.extent[1]
178
+ )
146
179
  ) {
147
180
  throw new RangeError("Bin extent must contain the histogram data extent.");
148
181
  }
149
182
  const resolved = resolveHistogramBins({
150
- values,
183
+ values: contributingValues,
151
184
  bin: transform.bin,
152
185
  domain: transform.extent,
153
186
  nice: transform.nice,
154
187
  zero: transform.zero
155
188
  });
156
189
  const members = resolved.boundaries.slice(0, -1).map(() => []);
157
- values.forEach((value, index) => {
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];
158
195
  const bin = findHistogramBinIndex(value, resolved.boundaries);
159
- if (bin !== -1) members[bin].push(rows[index]);
196
+ if (bin !== -1) members[bin].push(entry);
160
197
  });
161
198
  const output = members.flatMap((group, index) =>
162
199
  !transform.includeEmpty && group.length === 0 ? [] : [{
163
200
  [transform.as.lower]: resolved.boundaries[index],
164
201
  [transform.as.upper]: resolved.boundaries[index + 1],
165
- [transform.as.count]: group.length,
166
- ...(transform.members ? { [transform.as.members]: group } : {})
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
+ : {})
167
212
  }]
168
213
  );
169
214
  return {
@@ -0,0 +1,279 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const TRANSFORM_KEYS = Object.freeze([
4
+ "type", "key", "groupBy", "values", "sequence", "fill", "members"
5
+ ]);
6
+ const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
7
+ const MAX_OUTPUT_ROWS = 10_000;
8
+
9
+ function rejectUnknownKeys(value, supported, label) {
10
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
11
+ if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
12
+ }
13
+
14
+ function requireField(value, label) {
15
+ if (typeof value !== "string" || value.length === 0) {
16
+ throw new TypeError(`${label} must be a non-empty string.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function scalarType(value, label) {
22
+ if (value === null) return "null";
23
+ if (typeof value === "string" || typeof value === "boolean") return typeof value;
24
+ if (typeof value === "number" && Number.isFinite(value)) return "number";
25
+ throw new TypeError(`${label} must be a JSON-safe scalar.`);
26
+ }
27
+
28
+ function scalarKey(value, label) {
29
+ const type = scalarType(value, label);
30
+ if (type === "number") return `number:${Object.is(value, -0) ? 0 : value}`;
31
+ if (type === "string") return `string:${value.length}:${value}`;
32
+ return `${type}:${value}`;
33
+ }
34
+
35
+ function normalizeGroupBy(value) {
36
+ if (value === undefined) return [];
37
+ return Array.isArray(value) ? [...value] : [value];
38
+ }
39
+
40
+ function validateGroupBy(groupBy, key) {
41
+ if (!Array.isArray(groupBy) || groupBy.some(field =>
42
+ typeof field !== "string" || field.length === 0
43
+ )) {
44
+ throw new TypeError("Complete groupBy must contain field names.");
45
+ }
46
+ if (new Set(groupBy).size !== groupBy.length) {
47
+ throw new Error("Complete groupBy fields must be unique.");
48
+ }
49
+ if (groupBy.includes(key)) {
50
+ throw new Error("Complete key must be distinct from groupBy fields.");
51
+ }
52
+ }
53
+
54
+ function validateValues(values) {
55
+ if (!Array.isArray(values) || values.length === 0) {
56
+ throw new TypeError("Complete values must be a non-empty array.");
57
+ }
58
+ let type;
59
+ const seen = new Set();
60
+ values.forEach((value, index) => {
61
+ const current = scalarType(value, `Complete values[${index}]`);
62
+ if (type === undefined) type = current;
63
+ else if (type !== current) {
64
+ throw new TypeError("Complete values must contain one scalar type.");
65
+ }
66
+ const identity = scalarKey(value, `Complete values[${index}]`);
67
+ if (seen.has(identity)) throw new Error("Complete values must be unique.");
68
+ seen.add(identity);
69
+ });
70
+ }
71
+
72
+ function validateSequence(sequence) {
73
+ if (!isPlainObject(sequence)) {
74
+ throw new TypeError("Complete sequence must be a plain object.");
75
+ }
76
+ rejectUnknownKeys(sequence, SEQUENCE_KEYS, "complete sequence");
77
+ if (![sequence.start, sequence.end, sequence.step].every(Number.isFinite)) {
78
+ throw new TypeError("Complete sequence must contain finite start, end, and step values.");
79
+ }
80
+ if (sequence.start > sequence.end) {
81
+ throw new RangeError("Complete sequence start must be at or before end.");
82
+ }
83
+ if (sequence.step <= 0) {
84
+ throw new RangeError("Complete sequence step must be positive.");
85
+ }
86
+ }
87
+
88
+ function validateFill(fill, protectedFields) {
89
+ if (!isPlainObject(fill)) throw new TypeError("Complete fill must be a plain object.");
90
+ for (const [field, value] of Object.entries(fill)) {
91
+ requireField(field, "Complete fill field");
92
+ if (protectedFields.has(field)) {
93
+ throw new Error(`Complete fill cannot replace protected field "${field}".`);
94
+ }
95
+ scalarType(value, `Complete fill field "${field}"`);
96
+ }
97
+ }
98
+
99
+ export function validateCompleteTransform(transform) {
100
+ if (!isPlainObject(transform)) {
101
+ throw new TypeError("Complete transform must be a plain object.");
102
+ }
103
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "complete transform");
104
+ if (transform.type !== "complete") {
105
+ throw new Error(`Unsupported complete transform "${transform.type}".`);
106
+ }
107
+ const key = requireField(transform.key, "Complete key");
108
+ validateGroupBy(transform.groupBy, key);
109
+ const hasValues = Object.hasOwn(transform, "values");
110
+ const hasSequence = Object.hasOwn(transform, "sequence");
111
+ if (hasValues && hasSequence) {
112
+ throw new Error("Complete values and sequence are mutually exclusive.");
113
+ }
114
+ if (hasValues) validateValues(transform.values);
115
+ if (hasSequence) validateSequence(transform.sequence);
116
+ const members = Object.hasOwn(transform, "members")
117
+ ? requireField(transform.members, "Complete members field")
118
+ : undefined;
119
+ validateFill(transform.fill, new Set([key, ...transform.groupBy, members].filter(Boolean)));
120
+ return transform;
121
+ }
122
+
123
+ export function normalizeCompleteTransform(args = {}) {
124
+ const transform = {
125
+ type: "complete",
126
+ key: args.key,
127
+ groupBy: normalizeGroupBy(args.groupBy),
128
+ fill: isPlainObject(args.fill) ? { ...args.fill } : args.fill ?? {}
129
+ };
130
+ if (Object.hasOwn(args, "values")) {
131
+ transform.values = Array.isArray(args.values) ? [...args.values] : args.values;
132
+ }
133
+ if (Object.hasOwn(args, "sequence")) {
134
+ transform.sequence = isPlainObject(args.sequence) ? { ...args.sequence } : args.sequence;
135
+ }
136
+ if (Object.hasOwn(args, "members")) transform.members = args.members;
137
+ validateCompleteTransform(transform);
138
+ return cloneAndFreeze(transform);
139
+ }
140
+
141
+ function define(record, field, value) {
142
+ Object.defineProperty(record, field, {
143
+ value, enumerable: true, writable: true, configurable: true
144
+ });
145
+ }
146
+
147
+ function sequenceDomain(sequence) {
148
+ const domain = [];
149
+ for (let index = 0; index <= MAX_OUTPUT_ROWS; index += 1) {
150
+ const value = sequence.start + index * sequence.step;
151
+ if (!Number.isFinite(value)) {
152
+ throw new RangeError("Complete sequence value is outside the finite numeric range.");
153
+ }
154
+ if (value > sequence.end) return domain;
155
+ domain.push(value === 0 ? 0 : value);
156
+ }
157
+ throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
158
+ }
159
+
160
+ function observedDomain(rows, key) {
161
+ const domain = [];
162
+ const seen = new Set();
163
+ let type;
164
+ rows.forEach((row, index) => {
165
+ const value = row[key];
166
+ const current = scalarType(value, `Complete key "${key}" at row ${index}`);
167
+ if (type === undefined) type = current;
168
+ else if (type !== current) {
169
+ throw new TypeError(`Complete key "${key}" must contain one scalar type.`);
170
+ }
171
+ const identity = scalarKey(value, `Complete key "${key}" at row ${index}`);
172
+ if (!seen.has(identity)) {
173
+ seen.add(identity);
174
+ domain.push(value);
175
+ }
176
+ });
177
+ return domain;
178
+ }
179
+
180
+ function validateRows(rows, transform) {
181
+ if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
182
+ throw new TypeError("Complete source rows must be plain objects.");
183
+ }
184
+ const required = [transform.key, ...transform.groupBy];
185
+ const groupTypes = new Map();
186
+ const fields = [];
187
+ const fieldSet = new Set();
188
+ rows.forEach((row, index) => {
189
+ Object.keys(row).forEach(field => {
190
+ if (!fieldSet.has(field)) {
191
+ fieldSet.add(field);
192
+ fields.push(field);
193
+ }
194
+ });
195
+ required.forEach(field => {
196
+ if (!Object.hasOwn(row, field)) {
197
+ throw new Error(`Complete source does not contain field "${field}" at row ${index}.`);
198
+ }
199
+ const type = scalarType(row[field], `Complete field "${field}" at row ${index}`);
200
+ const previous = groupTypes.get(field);
201
+ if (previous !== undefined && previous !== type) {
202
+ throw new TypeError(`Complete field "${field}" must contain one scalar type.`);
203
+ }
204
+ groupTypes.set(field, type);
205
+ });
206
+ });
207
+ if (transform.members !== undefined && fieldSet.has(transform.members)) {
208
+ throw new Error(`Complete members field "${transform.members}" already exists.`);
209
+ }
210
+ return fields;
211
+ }
212
+
213
+ function groupRows(rows, transform) {
214
+ if (rows.length === 0) return transform.groupBy.length === 0
215
+ ? [{ values: [], tuple: [] }]
216
+ : [];
217
+ const groups = new Map();
218
+ rows.forEach((row, index) => {
219
+ const tuple = transform.groupBy.map(field => row[field]);
220
+ const identity = tuple.map((value, fieldIndex) =>
221
+ scalarKey(value, `Complete group field "${transform.groupBy[fieldIndex]}"`)
222
+ ).join("\0");
223
+ if (!groups.has(identity)) groups.set(identity, { values: [], tuple });
224
+ groups.get(identity).values.push({ row, index });
225
+ });
226
+ return [...groups.values()];
227
+ }
228
+
229
+ function syntheticRow(fields, group, keyValue, transform) {
230
+ const row = {};
231
+ for (const field of [...fields, ...Object.keys(transform.fill)]) define(row, field, null);
232
+ transform.groupBy.forEach((field, index) => define(row, field, group.tuple[index]));
233
+ define(row, transform.key, keyValue);
234
+ for (const [field, value] of Object.entries(transform.fill)) define(row, field, value);
235
+ if (transform.members !== undefined) define(row, transform.members, []);
236
+ return row;
237
+ }
238
+
239
+ export function deriveCompleteRows(rows, transform) {
240
+ validateCompleteTransform(transform);
241
+ const fields = validateRows(rows, transform);
242
+ const domain = transform.values !== undefined
243
+ ? [...transform.values]
244
+ : transform.sequence !== undefined
245
+ ? sequenceDomain(transform.sequence)
246
+ : observedDomain(rows, transform.key);
247
+ if (domain.length > 0) validateValues(domain);
248
+ const allowed = new Set(domain.map(value => scalarKey(value, "Complete domain value")));
249
+ const groups = groupRows(rows, transform);
250
+ if (groups.length !== 0 && domain.length > Math.floor(MAX_OUTPUT_ROWS / groups.length)) {
251
+ throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
252
+ }
253
+ const output = [];
254
+ for (const group of groups) {
255
+ const lookup = new Map();
256
+ for (const entry of group.values) {
257
+ const identity = scalarKey(entry.row[transform.key], `Complete key "${transform.key}"`);
258
+ if (!allowed.has(identity)) {
259
+ throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
260
+ }
261
+ if (lookup.has(identity)) {
262
+ throw new Error("Complete source contains a duplicate group and key combination.");
263
+ }
264
+ lookup.set(identity, entry);
265
+ }
266
+ for (const keyValue of domain) {
267
+ const identity = scalarKey(keyValue, "Complete domain value");
268
+ const entry = lookup.get(identity);
269
+ if (entry === undefined) {
270
+ output.push(syntheticRow(fields, group, keyValue, transform));
271
+ } else {
272
+ const row = { ...entry.row };
273
+ if (transform.members !== undefined) define(row, transform.members, [entry.index]);
274
+ output.push(row);
275
+ }
276
+ }
277
+ }
278
+ return cloneAndFreeze(output);
279
+ }