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
@@ -10,6 +10,15 @@ import {
10
10
  requireFiniteResult,
11
11
  stableFiniteDeviation
12
12
  } from "./numeric.js";
13
+ import {
14
+ normalizeStatisticalWeight,
15
+ readStatisticalWeights,
16
+ summarizeStatisticalWeights,
17
+ validateWeightedNumericFields,
18
+ weightedBandwidth,
19
+ weightedEntriesExtent,
20
+ weightedKernelEstimate
21
+ } from "./weightedStatistics.js";
13
22
 
14
23
  const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
15
24
 
@@ -37,6 +46,25 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
37
46
  "shared", "independent"
38
47
  ]);
39
48
 
49
+ export function normalizeDensityTransform(args = {}) {
50
+ const transform = {
51
+ type: "density",
52
+ field: args.field,
53
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
54
+ bandwidth: args.bandwidth ?? "auto",
55
+ extent: args.extent ?? "auto",
56
+ steps: args.steps ?? 100,
57
+ kernel: args.kernel ?? "gaussian",
58
+ normalization: args.normalization ?? "unit",
59
+ as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
60
+ resolve: args.resolve ?? "shared",
61
+ ...(args.weight === undefined ? {} : { weight: args.weight }),
62
+ ...(args.placement === undefined ? {} : { placement: args.placement })
63
+ };
64
+ validateDensityTransform(transform);
65
+ return cloneAndFreeze(transform);
66
+ }
67
+
40
68
  export function validateDensitySteps(value, label = "Density steps") {
41
69
  if (!Number.isInteger(value) || value < 2) {
42
70
  throw new RangeError(`${label} must be an integer of at least 2.`);
@@ -261,10 +289,88 @@ function validateStoredDensityPlacement(value, groupBy) {
261
289
  return value;
262
290
  }
263
291
 
292
+ function validateResolvedBandwidths(value, transform) {
293
+ if (!Array.isArray(value) || value.length === 0) {
294
+ throw new TypeError(
295
+ "Density resolved bandwidths must be a non-empty array."
296
+ );
297
+ }
298
+ const normalized = value.map((item, index) => {
299
+ if (!isPlainObject(item)) {
300
+ throw new TypeError(
301
+ `Density resolved bandwidth ${index} must be a plain object.`
302
+ );
303
+ }
304
+ const unknown = Object.keys(item).find(
305
+ key => !["group", "split", "bandwidth"].includes(key)
306
+ );
307
+ if (unknown !== undefined) {
308
+ throw new Error(
309
+ `Unknown density resolved bandwidth property "${unknown}".`
310
+ );
311
+ }
312
+ if (!Number.isFinite(item.bandwidth) || item.bandwidth <= 0) {
313
+ throw new RangeError(
314
+ `Density resolved bandwidth ${index} must be positive and finite.`
315
+ );
316
+ }
317
+ if (transform.groupBy === undefined) {
318
+ if (Object.hasOwn(item, "group")) {
319
+ throw new Error(
320
+ "Density resolved bandwidth cannot store a group without groupBy."
321
+ );
322
+ }
323
+ } else if (!Object.hasOwn(item, "group") || !isNominalValue(item.group)) {
324
+ throw new TypeError(
325
+ `Density resolved bandwidth ${index} requires a nominal group.`
326
+ );
327
+ }
328
+ if (transform.placement?.split === undefined) {
329
+ if (Object.hasOwn(item, "split")) {
330
+ throw new Error(
331
+ "Density resolved bandwidth cannot store a split without split placement."
332
+ );
333
+ }
334
+ } else if (!Object.hasOwn(item, "split") || !isNominalValue(item.split)) {
335
+ throw new TypeError(
336
+ `Density resolved bandwidth ${index} requires a nominal split.`
337
+ );
338
+ }
339
+ return item;
340
+ });
341
+ for (let index = 0; index < normalized.length; index += 1) {
342
+ const duplicate = normalized.slice(0, index).some(item =>
343
+ Object.is(item.group, normalized[index].group) &&
344
+ Object.is(item.split, normalized[index].split)
345
+ );
346
+ if (duplicate) {
347
+ throw new Error(
348
+ `Density resolved bandwidth ${index} duplicates an earlier profile.`
349
+ );
350
+ }
351
+ }
352
+ return value;
353
+ }
354
+
355
+ function hasValidResolvedBandwidthState(resolved, transform) {
356
+ const hasBandwidth = Object.hasOwn(resolved, "bandwidth");
357
+ const hasBandwidths = Object.hasOwn(resolved, "bandwidths");
358
+ if (hasBandwidth === hasBandwidths) return false;
359
+ if (hasBandwidth) {
360
+ return Number.isFinite(resolved.bandwidth) && resolved.bandwidth > 0;
361
+ }
362
+ try {
363
+ validateResolvedBandwidths(resolved.bandwidths, transform);
364
+ return true;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+
264
370
  export function validateDensityTransform(transform) {
265
371
  const supported = [
266
372
  "type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
267
- "resolve", "kernel", "normalization", "placement", "resolved"
373
+ "resolve", "kernel", "normalization", "placement", "weight", "resolved"
268
374
  ];
269
375
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
270
376
  if (unknown !== undefined) {
@@ -274,6 +380,9 @@ export function validateDensityTransform(transform) {
274
380
  throw new Error(`Unsupported density transform "${transform.type}".`);
275
381
  }
276
382
  requireField(transform.field, "Density field");
383
+ if (transform.weight !== undefined) {
384
+ normalizeStatisticalWeight(transform.weight, "Density weight");
385
+ }
277
386
  if (transform.groupBy !== undefined) {
278
387
  requireField(transform.groupBy, "Density groupBy");
279
388
  }
@@ -301,10 +410,9 @@ export function validateDensityTransform(transform) {
301
410
  typeof resolved !== "object" ||
302
411
  Array.isArray(resolved) ||
303
412
  Object.keys(resolved).some(
304
- key => !["bandwidth", "extent", "splitDomain"].includes(key)
413
+ key => !["bandwidth", "bandwidths", "extent", "splitDomain"].includes(key)
305
414
  ) ||
306
- !Number.isFinite(resolved.bandwidth) ||
307
- resolved.bandwidth <= 0 ||
415
+ !hasValidResolvedBandwidthState(resolved, transform) ||
308
416
  !Array.isArray(resolved.extent) ||
309
417
  resolved.extent.length !== 2 ||
310
418
  !resolved.extent.every(Number.isFinite) ||
@@ -319,7 +427,7 @@ export function validateDensityTransform(transform) {
319
427
  })())
320
428
  ) {
321
429
  throw new TypeError(
322
- "Density resolved provenance requires a positive bandwidth, ascending finite extent, and optional two-value split domain."
430
+ "Density resolved provenance requires one positive bandwidth or profile bandwidth list, an ascending finite extent, and an optional two-value split domain."
323
431
  );
324
432
  }
325
433
  }
@@ -363,22 +471,26 @@ export function estimateDensityBandwidth(values) {
363
471
  return bandwidth;
364
472
  }
365
473
 
366
- function resolveBandwidth(value, sourceValues) {
474
+ function resolveBandwidth(value, sourceValues, weightSummary, field) {
367
475
  if (value === undefined || value === "auto") {
368
- return estimateDensityBandwidth(sourceValues);
476
+ return weightSummary === undefined
477
+ ? estimateDensityBandwidth(sourceValues)
478
+ : weightedBandwidth(weightSummary, field);
369
479
  }
370
480
  return validateDensityBandwidth(value);
371
481
  }
372
482
 
373
- function resolveExtent(value, sourceValues) {
483
+ function resolveExtent(value, sourceValues, weightSummary, field) {
374
484
  if (value === undefined || value === "auto") {
375
- let lower = Infinity;
376
- let upper = -Infinity;
377
- for (const sourceValue of sourceValues) {
378
- lower = Math.min(lower, sourceValue);
379
- upper = Math.max(upper, sourceValue);
380
- }
381
- const extent = [lower, upper];
485
+ const extent = weightSummary === undefined
486
+ ? sourceValues.reduce(
487
+ ([lower, upper], sourceValue) => [
488
+ Math.min(lower, sourceValue),
489
+ Math.max(upper, sourceValue)
490
+ ],
491
+ [Infinity, -Infinity]
492
+ )
493
+ : weightedEntriesExtent(weightSummary, field);
382
494
  if (extent[0] === extent[1]) {
383
495
  throw new Error("Density observed extent requires varying finite values.");
384
496
  }
@@ -407,6 +519,17 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
407
519
  return requireFiniteResult(stable, "Density estimate");
408
520
  }
409
521
 
522
+ function estimateWeightedAt(sample, summary, field, bandwidth, kernel, normalization) {
523
+ return weightedKernelEstimate(
524
+ summary,
525
+ field,
526
+ sample,
527
+ bandwidth,
528
+ KERNEL_FUNCTIONS[kernel],
529
+ normalization
530
+ );
531
+ }
532
+
410
533
  export function deriveKernelDensity(values, {
411
534
  field,
412
535
  groupBy,
@@ -416,7 +539,8 @@ export function deriveKernelDensity(values, {
416
539
  kernel = "gaussian",
417
540
  normalization = "unit",
418
541
  as,
419
- placement
542
+ placement,
543
+ weight
420
544
  } = {}) {
421
545
  if (!Array.isArray(values)) {
422
546
  throw new TypeError("Density values must be an array.");
@@ -437,29 +561,66 @@ export function deriveKernelDensity(values, {
437
561
  )];
438
562
  const resolvedKernel = validateDensityKernel(kernel);
439
563
  const resolvedNormalization = validateDensityNormalization(normalization);
440
- const validRows = values.filter(row =>
441
- row !== null &&
442
- typeof row === "object" &&
443
- Number.isFinite(row[sourceField]) &&
444
- (groupField === undefined || isNominalValue(row[groupField])) &&
445
- (placement?.split === undefined || isNominalValue(row[placement.split.field]))
446
- );
564
+ const statisticalWeights = weight === undefined
565
+ ? undefined
566
+ : readStatisticalWeights(values, weight, "Density");
567
+ if (statisticalWeights !== undefined) {
568
+ validateWeightedNumericFields(
569
+ statisticalWeights.entries,
570
+ [sourceField],
571
+ "Density"
572
+ );
573
+ }
574
+ const validEntries = statisticalWeights === undefined
575
+ ? undefined
576
+ : statisticalWeights.entries.filter(entry =>
577
+ (groupField === undefined || isNominalValue(entry.row[groupField])) &&
578
+ (placement?.split === undefined ||
579
+ isNominalValue(entry.row[placement.split.field]))
580
+ );
581
+ const validRows = statisticalWeights === undefined
582
+ ? values.filter(row =>
583
+ row !== null &&
584
+ typeof row === "object" &&
585
+ Number.isFinite(row[sourceField]) &&
586
+ (groupField === undefined || isNominalValue(row[groupField])) &&
587
+ (placement?.split === undefined || isNominalValue(row[placement.split.field]))
588
+ )
589
+ : validEntries.map(entry => entry.row);
447
590
  if (validRows.length === 0) {
448
591
  throw new Error("Density requires at least one valid field/group row.");
449
592
  }
450
- validateWorkLimit(validRows.length * steps, "Density computation");
451
- const sourceValues = validRows.map(row => row[sourceField]);
452
- const resolvedBandwidth = resolveBandwidth(bandwidth, sourceValues);
453
- const resolvedExtent = resolveExtent(extent, sourceValues);
593
+ const globalWeightSummary = statisticalWeights === undefined
594
+ ? undefined
595
+ : summarizeStatisticalWeights(
596
+ validEntries,
597
+ statisticalWeights.definition.kind,
598
+ "Density data"
599
+ );
600
+ validateWorkLimit(
601
+ (globalWeightSummary?.positive.length ?? validRows.length) * steps,
602
+ "Density computation"
603
+ );
604
+ const sourceValues = globalWeightSummary === undefined
605
+ ? validRows.map(row => row[sourceField])
606
+ : globalWeightSummary.positive.map(entry => entry.row[sourceField]);
607
+ const resolvedExtent = resolveExtent(
608
+ extent,
609
+ sourceValues,
610
+ globalWeightSummary,
611
+ sourceField
612
+ );
454
613
  const groupedValues = new Map();
455
- for (const row of validRows) {
614
+ const groupedSource = statisticalWeights === undefined ? validRows : validEntries;
615
+ for (const source of groupedSource) {
616
+ const row = statisticalWeights === undefined ? source : source.row;
456
617
  const group = groupField === undefined ? undefined : row[groupField];
457
618
  const split = placement?.split === undefined
458
619
  ? undefined
459
620
  : row[placement.split.field];
460
621
  const bySplit = groupedValues.get(group) ?? new Map();
461
622
  const groupValues = bySplit.get(split) ?? [];
462
- groupValues.push(row[sourceField]);
623
+ groupValues.push(statisticalWeights === undefined ? row[sourceField] : source);
463
624
  bySplit.set(split, groupValues);
464
625
  groupedValues.set(group, bySplit);
465
626
  }
@@ -492,6 +653,16 @@ export function deriveKernelDensity(values, {
492
653
  outputGroups * steps,
493
654
  "Density generated row count"
494
655
  );
656
+ const profileBandwidths = statisticalWeights !== undefined &&
657
+ bandwidth === "auto" && outputGroups > 1;
658
+ const resolvedBandwidth = profileBandwidths
659
+ ? undefined
660
+ : resolveBandwidth(
661
+ bandwidth,
662
+ sourceValues,
663
+ globalWeightSummary,
664
+ sourceField
665
+ );
495
666
  const extentSpan = resolvedExtent[1] - resolvedExtent[0];
496
667
  const sampleStep = extentSpan / (steps - 1);
497
668
  const samples = Array.from(
@@ -516,10 +687,30 @@ export function deriveKernelDensity(values, {
516
687
  );
517
688
  }
518
689
  const rows = [];
690
+ const resolvedBandwidths = [];
519
691
  for (const group of groups) {
520
692
  for (const split of splits) {
521
693
  const groupValues = groupedValues.get(group).get(split) ?? [];
522
694
  if (groupValues.length === 0) continue;
695
+ const groupWeightSummary = statisticalWeights === undefined
696
+ ? undefined
697
+ : summarizeStatisticalWeights(
698
+ groupValues,
699
+ statisticalWeights.definition.kind,
700
+ "Density group"
701
+ );
702
+ const groupBandwidth = resolvedBandwidth ?? weightedBandwidth(
703
+ groupWeightSummary,
704
+ sourceField,
705
+ "Density group auto bandwidth"
706
+ );
707
+ if (profileBandwidths) {
708
+ resolvedBandwidths.push({
709
+ ...(groupField === undefined ? {} : { group }),
710
+ ...(placement?.split === undefined ? {} : { split }),
711
+ bandwidth: groupBandwidth
712
+ });
713
+ }
523
714
  for (const sample of samples) {
524
715
  rows.push({
525
716
  ...(groupField === undefined
@@ -531,13 +722,22 @@ export function deriveKernelDensity(values, {
531
722
  ? {}
532
723
  : { [placement.split.field]: split }),
533
724
  [outputFields[0]]: sample,
534
- [outputFields[1]]: estimateAt(
535
- sample,
536
- groupValues,
537
- resolvedBandwidth,
538
- resolvedKernel,
539
- resolvedNormalization
540
- )
725
+ [outputFields[1]]: groupWeightSummary === undefined
726
+ ? estimateAt(
727
+ sample,
728
+ groupValues,
729
+ groupBandwidth,
730
+ resolvedKernel,
731
+ resolvedNormalization
732
+ )
733
+ : estimateWeightedAt(
734
+ sample,
735
+ groupWeightSummary,
736
+ sourceField,
737
+ groupBandwidth,
738
+ resolvedKernel,
739
+ resolvedNormalization
740
+ )
541
741
  });
542
742
  }
543
743
  }
@@ -551,7 +751,9 @@ export function deriveKernelDensity(values, {
551
751
  },
552
752
  groups,
553
753
  ...(splitDomain === undefined ? {} : { splitDomain }),
554
- bandwidth: resolvedBandwidth,
754
+ ...(resolvedBandwidth === undefined
755
+ ? { bandwidths: resolvedBandwidths }
756
+ : { bandwidth: resolvedBandwidth }),
555
757
  kernel: resolvedKernel,
556
758
  normalization: resolvedNormalization,
557
759
  extent: resolvedExtent,
@@ -0,0 +1,57 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+
3
+ const ENTRY_KEYS = Object.freeze(["value", "label"]);
4
+
5
+ export function sameDisplayLabelValue(left, right) {
6
+ return left === right;
7
+ }
8
+
9
+ function isDisplayLabelValue(value) {
10
+ return value === null || typeof value === "string" ||
11
+ typeof value === "boolean" ||
12
+ (typeof value === "number" && Number.isFinite(value));
13
+ }
14
+
15
+ export function normalizeDisplayLabelMap(value, label = "labelMap") {
16
+ if (!Array.isArray(value)) {
17
+ throw new TypeError(`${label} must be an array.`);
18
+ }
19
+ const normalized = [];
20
+ for (const [index, entry] of value.entries()) {
21
+ if (!isPlainObject(entry)) {
22
+ throw new TypeError(`${label}[${index}] must be a plain object.`);
23
+ }
24
+ const keys = Reflect.ownKeys(entry);
25
+ if (keys.length !== ENTRY_KEYS.length ||
26
+ ENTRY_KEYS.some(key => !Object.hasOwn(entry, key))) {
27
+ throw new Error(`${label}[${index}] must contain exactly value and label.`);
28
+ }
29
+ if (!isDisplayLabelValue(entry.value)) {
30
+ throw new TypeError(
31
+ `${label}[${index}].value must be a string, boolean, null, or finite number.`
32
+ );
33
+ }
34
+ if (typeof entry.label !== "string") {
35
+ throw new TypeError(`${label}[${index}].label must be a string.`);
36
+ }
37
+ if (normalized.some(previous =>
38
+ sameDisplayLabelValue(previous.value, entry.value))) {
39
+ throw new Error(`${label} contains a duplicate typed value.`);
40
+ }
41
+ normalized.push(Object.freeze({ value: entry.value, label: entry.label }));
42
+ }
43
+ return Object.freeze(normalized);
44
+ }
45
+
46
+ export function resolveDisplayLabel(value, map, fallback) {
47
+ if (map !== undefined) {
48
+ for (const entry of map) {
49
+ if (sameDisplayLabelValue(entry.value, value)) return entry.label;
50
+ }
51
+ }
52
+ const resolved = fallback(value);
53
+ if (typeof resolved !== "string") {
54
+ throw new TypeError("Display label fallback must return a string.");
55
+ }
56
+ return resolved;
57
+ }
@@ -0,0 +1,198 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { stableDecimal } from "./numeric.js";
3
+
4
+ const TRANSFORM_KEYS = Object.freeze([
5
+ "type", "field", "groupBy", "weight", "missing", "as", "resolved"
6
+ ]);
7
+ const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
8
+
9
+ function nonEmptyString(value, label) {
10
+ if (typeof value !== "string" || value.length === 0) {
11
+ throw new TypeError(`${label} must be a non-empty string.`);
12
+ }
13
+ }
14
+
15
+ function normalizeGroupBy(groupBy) {
16
+ const fields = groupBy === undefined
17
+ ? []
18
+ : Array.isArray(groupBy) ? [...groupBy] : [groupBy];
19
+ fields.forEach(field => nonEmptyString(field, "ECDF groupBy field"));
20
+ if (new Set(fields).size !== fields.length) {
21
+ throw new Error("ECDF groupBy fields must be distinct.");
22
+ }
23
+ return fields;
24
+ }
25
+
26
+ function validateOutputs(as, occupied) {
27
+ if (!isPlainObject(as)) throw new TypeError("ECDF as must be a plain object.");
28
+ const keys = Object.keys(as);
29
+ if (keys.length !== OUTPUT_KEYS.length ||
30
+ !OUTPUT_KEYS.every(key => Object.hasOwn(as, key))) {
31
+ throw new Error("ECDF as requires exactly value, cumulative, and probability.");
32
+ }
33
+ const values = OUTPUT_KEYS.map(key => as[key]);
34
+ values.forEach((field, index) => nonEmptyString(field, `ECDF as.${OUTPUT_KEYS[index]}`));
35
+ if (new Set(values).size !== values.length) {
36
+ throw new Error("ECDF output fields must be distinct.");
37
+ }
38
+ if (values.some(field => occupied.has(field))) {
39
+ throw new Error("ECDF output fields must not collide with input or grouping fields.");
40
+ }
41
+ }
42
+
43
+ function validateResolved(resolved, groupBy) {
44
+ if (resolved === undefined) return;
45
+ if (!isPlainObject(resolved) || !Array.isArray(resolved.groups)) {
46
+ throw new TypeError("ECDF resolved provenance must contain a groups array.");
47
+ }
48
+ for (const group of resolved.groups) {
49
+ if (!isPlainObject(group) || !isPlainObject(group.keys) ||
50
+ !Number.isFinite(group.denominator) || group.denominator <= 0 ||
51
+ !Number.isInteger(group.validCount) || group.validCount <= 0 ||
52
+ Object.keys(group.keys).length !== groupBy.length ||
53
+ !groupBy.every(field => Object.hasOwn(group.keys, field))) {
54
+ throw new TypeError("ECDF resolved group provenance is invalid.");
55
+ }
56
+ }
57
+ }
58
+
59
+ export function validateECDFTransform(transform) {
60
+ if (!isPlainObject(transform)) {
61
+ throw new TypeError("ECDF transform must be a plain object.");
62
+ }
63
+ const unknown = Object.keys(transform).find(key => !TRANSFORM_KEYS.includes(key));
64
+ if (unknown !== undefined) {
65
+ throw new Error(`Unknown ECDF transform property "${unknown}".`);
66
+ }
67
+ if (transform.type !== "ecdf") {
68
+ throw new Error(`Unsupported ECDF transform "${transform.type}".`);
69
+ }
70
+ nonEmptyString(transform.field, "ECDF field");
71
+ const groupBy = normalizeGroupBy(transform.groupBy);
72
+ if (groupBy.includes(transform.field)) {
73
+ throw new Error("ECDF field must not also be a grouping field.");
74
+ }
75
+ if (transform.weight !== undefined) {
76
+ nonEmptyString(transform.weight, "ECDF weight field");
77
+ if (transform.weight === transform.field || groupBy.includes(transform.weight)) {
78
+ throw new Error("ECDF weight field must be distinct from value and grouping fields.");
79
+ }
80
+ }
81
+ if (!["drop", "error"].includes(transform.missing)) {
82
+ throw new Error('ECDF missing must be "drop" or "error".');
83
+ }
84
+ validateOutputs(
85
+ transform.as,
86
+ new Set([transform.field, transform.weight, ...groupBy].filter(Boolean))
87
+ );
88
+ validateResolved(transform.resolved, groupBy);
89
+ }
90
+
91
+ export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop", as }) {
92
+ const transform = {
93
+ type: "ecdf",
94
+ field,
95
+ groupBy: normalizeGroupBy(groupBy),
96
+ ...(weight === undefined ? {} : { weight }),
97
+ missing,
98
+ as
99
+ };
100
+ validateECDFTransform(transform);
101
+ return cloneAndFreeze(transform);
102
+ }
103
+
104
+ function groupValue(value) {
105
+ return typeof value === "string" || typeof value === "boolean" ||
106
+ (typeof value === "number" && Number.isFinite(value));
107
+ }
108
+
109
+ function rejectOrDrop(transform, index, reason) {
110
+ if (transform.missing === "error") {
111
+ throw new TypeError(`ECDF row ${index} ${reason}.`);
112
+ }
113
+ return false;
114
+ }
115
+
116
+ export function deriveECDF(rows, transform) {
117
+ if (!Array.isArray(rows)) throw new TypeError("ECDF rows must be an array.");
118
+ validateECDFTransform(transform);
119
+ const groups = new Map();
120
+
121
+ rows.forEach((row, index) => {
122
+ if (!isPlainObject(row)) {
123
+ rejectOrDrop(transform, index, "must be a plain object");
124
+ return;
125
+ }
126
+ const value = row[transform.field];
127
+ if (!Number.isFinite(value)) {
128
+ rejectOrDrop(transform, index, `has an invalid value in "${transform.field}"`);
129
+ return;
130
+ }
131
+ const keys = transform.groupBy.map(field => row[field]);
132
+ if (!keys.every(groupValue)) {
133
+ rejectOrDrop(transform, index, "has an invalid grouping value");
134
+ return;
135
+ }
136
+ const weight = transform.weight === undefined ? 1 : row[transform.weight];
137
+ if (!Number.isFinite(weight)) {
138
+ rejectOrDrop(transform, index, `has an invalid weight in "${transform.weight}"`);
139
+ return;
140
+ }
141
+ if (weight < 0) {
142
+ throw new RangeError(`ECDF row ${index} has a negative weight.`);
143
+ }
144
+ const key = JSON.stringify(keys);
145
+ if (!groups.has(key)) {
146
+ groups.set(key, {
147
+ keys: Object.fromEntries(transform.groupBy.map((field, offset) => [field, keys[offset]])),
148
+ values: new Map(),
149
+ denominator: 0,
150
+ validCount: 0
151
+ });
152
+ }
153
+ const group = groups.get(key);
154
+ if (weight === 0) return;
155
+ group.values.set(value, (group.values.get(value) ?? 0) + weight);
156
+ group.denominator += weight;
157
+ group.validCount += 1;
158
+ });
159
+
160
+ if (groups.size === 0) {
161
+ throw new RangeError("ECDF denominator must be greater than zero.");
162
+ }
163
+
164
+ const values = [];
165
+ const provenance = [];
166
+ for (const group of groups.values()) {
167
+ if (!(group.denominator > 0) || !Number.isFinite(group.denominator)) {
168
+ throw new RangeError("ECDF denominator must be finite and greater than zero.");
169
+ }
170
+ const support = [...group.values.keys()].sort((left, right) => left - right);
171
+ values.push({
172
+ ...group.keys,
173
+ [transform.as.value]: support[0],
174
+ [transform.as.cumulative]: 0,
175
+ [transform.as.probability]: 0
176
+ });
177
+ let cumulative = 0;
178
+ for (const value of support) {
179
+ cumulative += group.values.get(value);
180
+ values.push({
181
+ ...group.keys,
182
+ [transform.as.value]: value,
183
+ [transform.as.cumulative]: stableDecimal(cumulative),
184
+ [transform.as.probability]: stableDecimal(cumulative / group.denominator)
185
+ });
186
+ }
187
+ provenance.push({
188
+ keys: group.keys,
189
+ denominator: stableDecimal(group.denominator),
190
+ validCount: group.validCount
191
+ });
192
+ }
193
+ return cloneAndFreeze({ values, resolved: { groups: provenance } });
194
+ }
195
+
196
+ export function resolveECDFTransform(result, transform) {
197
+ return cloneAndFreeze([{ ...transform, resolved: result.resolved }]);
198
+ }