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
@@ -5,9 +5,18 @@ import {
5
5
  validateAggregate,
6
6
  validateAggregateFieldValues
7
7
  } from "./aggregate.js";
8
+ import {
9
+ calculateWeightedAggregate,
10
+ normalizeStatisticalWeight,
11
+ readStatisticalWeights,
12
+ summarizeStatisticalWeights,
13
+ validateWeightedAggregate,
14
+ validateWeightedNumericFields,
15
+ weightedRows
16
+ } from "./weightedStatistics.js";
8
17
 
9
18
  const TRANSFORM_KEYS = Object.freeze([
10
- "type", "groupBy", "aggregates", "members"
19
+ "type", "groupBy", "aggregates", "members", "weight"
11
20
  ]);
12
21
  const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
13
22
  const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
@@ -42,14 +51,17 @@ function normalizeAggregate(value) {
42
51
  };
43
52
  }
44
53
 
45
- export function normalizeSummaryTransform({ groupBy, aggregates, members } = {}) {
54
+ export function normalizeSummaryTransform({ groupBy, aggregates, members, weight } = {}) {
46
55
  const transform = {
47
56
  type: "summary",
48
57
  groupBy: normalizeGroupBy(groupBy),
49
58
  aggregates: Array.isArray(aggregates)
50
59
  ? aggregates.map(normalizeAggregate)
51
60
  : aggregates,
52
- ...(members === undefined ? {} : { members })
61
+ ...(members === undefined ? {} : { members }),
62
+ ...(weight === undefined
63
+ ? {}
64
+ : { weight: normalizeStatisticalWeight(weight, "Summary weight") })
53
65
  };
54
66
  validateSummaryTransform(transform);
55
67
  return cloneAndFreeze(transform);
@@ -85,6 +97,7 @@ export function validateSummaryTransform(transform) {
85
97
  }
86
98
  rejectUnknownKeys(aggregate, AGGREGATE_KEYS, `summary aggregate ${index}`);
87
99
  const operation = validateAggregate(aggregate.op);
100
+ if (transform.weight !== undefined) validateWeightedAggregate(operation);
88
101
  if (operation === "count") {
89
102
  if (Object.hasOwn(aggregate, "field")) {
90
103
  throw new Error("Summary count does not accept a field.");
@@ -104,6 +117,9 @@ export function validateSummaryTransform(transform) {
104
117
  throw new Error(`Summary members field "${members}" collides with another output.`);
105
118
  }
106
119
  }
120
+ if (transform.weight !== undefined) {
121
+ normalizeStatisticalWeight(transform.weight, "Summary weight");
122
+ }
107
123
  return transform;
108
124
  }
109
125
 
@@ -136,6 +152,9 @@ function requireSourceFields(rows, transform) {
136
152
  throw new Error(`Summary source does not contain order field "${orderBy}".`);
137
153
  }
138
154
  }
155
+ if (transform.weight !== undefined && !fields.has(transform.weight.field)) {
156
+ throw new Error(`Summary source does not contain weight field "${transform.weight.field}".`);
157
+ }
139
158
  }
140
159
 
141
160
  function validateAggregateValues(rows, aggregate) {
@@ -150,21 +169,42 @@ function validateAggregateValues(rows, aggregate) {
150
169
  export function deriveSummaryRows(rows, transform) {
151
170
  validateSummaryTransform(transform);
152
171
  requireSourceFields(rows, transform);
153
- for (const aggregate of transform.aggregates) {
154
- validateAggregateValues(rows, aggregate);
172
+ let weightEntries;
173
+ if (transform.weight === undefined) {
174
+ for (const aggregate of transform.aggregates) {
175
+ validateAggregateValues(rows, aggregate);
176
+ }
177
+ } else {
178
+ weightEntries = readStatisticalWeights(rows, transform.weight, "Summary").entries;
179
+ validateWeightedNumericFields(
180
+ weightEntries,
181
+ [...new Set(transform.aggregates.flatMap(aggregate =>
182
+ aggregate.field === undefined ? [] : [aggregate.field]
183
+ ))],
184
+ "Summary"
185
+ );
155
186
  }
156
187
 
157
188
  const groups = new Map();
158
189
  if (transform.groupBy.length === 0) {
159
- groups.set("all", { values: {}, rows });
190
+ groups.set("all", {
191
+ values: {},
192
+ rows,
193
+ ...(weightEntries === undefined ? {} : { entries: weightEntries })
194
+ });
160
195
  } else {
161
- rows.forEach(row => {
196
+ rows.forEach((row, index) => {
162
197
  const values = Object.fromEntries(transform.groupBy.map(field => [field, row[field]]));
163
198
  const key = transform.groupBy.map(field =>
164
199
  scalarKey(row[field], `Summary group field "${field}"`)
165
200
  ).join("|");
166
- const group = groups.get(key) ?? { values, rows: [] };
201
+ const group = groups.get(key) ?? {
202
+ values,
203
+ rows: [],
204
+ ...(weightEntries === undefined ? {} : { entries: [] })
205
+ };
167
206
  group.rows.push(row);
207
+ if (weightEntries !== undefined) group.entries.push(weightEntries[index]);
168
208
  groups.set(key, group);
169
209
  });
170
210
  }
@@ -172,12 +212,34 @@ export function deriveSummaryRows(rows, transform) {
172
212
  throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
173
213
  }
174
214
 
175
- return [...groups.values()].map(group => ({
176
- ...group.values,
177
- ...Object.fromEntries(transform.aggregates.map(aggregate => [
178
- aggregate.as,
179
- aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
180
- ])),
181
- ...(transform.members === undefined ? {} : { [transform.members]: group.rows })
182
- }));
215
+ return [...groups.values()].map(group => {
216
+ const weightSummary = transform.weight === undefined
217
+ ? undefined
218
+ : summarizeStatisticalWeights(
219
+ group.entries,
220
+ transform.weight.kind,
221
+ "Summary group"
222
+ );
223
+ return {
224
+ ...group.values,
225
+ ...Object.fromEntries(transform.aggregates.map(aggregate => [
226
+ aggregate.as,
227
+ weightSummary === undefined
228
+ ? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
229
+ : calculateWeightedAggregate(
230
+ weightSummary,
231
+ aggregate.field,
232
+ aggregate.op,
233
+ `Summary aggregate "${aggregate.as}"`
234
+ )
235
+ ])),
236
+ ...(transform.members === undefined
237
+ ? {}
238
+ : {
239
+ [transform.members]: weightSummary === undefined
240
+ ? group.rows
241
+ : weightedRows(weightSummary)
242
+ })
243
+ };
244
+ });
183
245
  }
@@ -33,10 +33,47 @@ export const TIME_UNITS = cloneAndFreeze([
33
33
  "day",
34
34
  "hour",
35
35
  "minute",
36
- "second"
36
+ "second",
37
+ "week",
38
+ "weekday"
37
39
  ]);
38
40
 
39
- const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as", "temporalUnit"]);
41
+ const TRANSFORM_KEYS = Object.freeze([
42
+ "type", "field", "unit", "as", "temporalUnit", "timeZone",
43
+ "weekStartsOn", "weekRule"
44
+ ]);
45
+
46
+ const CALENDAR_FORMATTERS = new Map();
47
+
48
+ function formatter(timeZone) {
49
+ if (!CALENDAR_FORMATTERS.has(timeZone)) {
50
+ let value;
51
+ try {
52
+ value = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
53
+ timeZone,
54
+ year: "numeric",
55
+ month: "2-digit",
56
+ day: "2-digit",
57
+ hour: "2-digit",
58
+ minute: "2-digit",
59
+ second: "2-digit",
60
+ hourCycle: "h23"
61
+ });
62
+ } catch {
63
+ throw new RangeError(`Unsupported time zone "${timeZone}".`);
64
+ }
65
+ CALENDAR_FORMATTERS.set(timeZone, value);
66
+ }
67
+ return CALENDAR_FORMATTERS.get(timeZone);
68
+ }
69
+
70
+ function validateTimeZone(timeZone) {
71
+ if (typeof timeZone !== "string" || timeZone.length === 0) {
72
+ throw new TypeError("Time-unit timeZone must be a non-empty IANA time-zone name.");
73
+ }
74
+ formatter(timeZone);
75
+ return timeZone;
76
+ }
40
77
 
41
78
  function requireField(value, label) {
42
79
  if (typeof value !== "string" || value.length === 0) {
@@ -69,12 +106,38 @@ export function validateTimeUnitTransform(transform) {
69
106
  if (!TIME_UNITS.includes(transform.unit)) {
70
107
  throw new Error(`Unsupported time unit "${transform.unit}".`);
71
108
  }
109
+ if (Object.hasOwn(transform, "timeZone")) validateTimeZone(transform.timeZone);
110
+ const hasWeekStartsOn = Object.hasOwn(transform, "weekStartsOn");
111
+ const hasWeekRule = Object.hasOwn(transform, "weekRule");
112
+ if (transform.unit === "week") {
113
+ if (!Number.isInteger(transform.weekStartsOn) ||
114
+ transform.weekStartsOn < 0 || transform.weekStartsOn > 6) {
115
+ throw new RangeError("Time-unit weekStartsOn must be an integer from 0 through 6.");
116
+ }
117
+ if (!["calendar", "iso"].includes(transform.weekRule)) {
118
+ throw new Error(`Unsupported time-unit weekRule "${transform.weekRule}".`);
119
+ }
120
+ if (transform.weekRule === "iso" && transform.weekStartsOn !== 1) {
121
+ throw new Error("Time-unit ISO weeks require weekStartsOn 1 (Monday).");
122
+ }
123
+ } else if (hasWeekStartsOn || hasWeekRule) {
124
+ throw new Error("Time-unit weekStartsOn and weekRule require unit week.");
125
+ }
72
126
  return transform;
73
127
  }
74
128
 
75
- export function normalizeTimeUnitTransform({ field, unit, as, temporalUnit } = {}) {
129
+ export function normalizeTimeUnitTransform(args = {}) {
130
+ const { field, unit, as, temporalUnit, timeZone } = args;
76
131
  const transform = { type: "timeUnit", field, unit, as,
77
- ...(temporalUnit === undefined ? {} : { temporalUnit }) };
132
+ ...(temporalUnit === undefined ? {} : { temporalUnit }),
133
+ ...(timeZone === undefined ? {} : { timeZone }) };
134
+ if (unit === "week") {
135
+ transform.weekStartsOn = args.weekStartsOn ?? 1;
136
+ transform.weekRule = args.weekRule ?? "calendar";
137
+ } else {
138
+ if (Object.hasOwn(args, "weekStartsOn")) transform.weekStartsOn = args.weekStartsOn;
139
+ if (Object.hasOwn(args, "weekRule")) transform.weekRule = args.weekRule;
140
+ }
78
141
  validateTimeUnitTransform(transform);
79
142
  return cloneAndFreeze(transform);
80
143
  }
@@ -96,6 +159,10 @@ export function floorUtcTimeUnit(timestamp, unit) {
96
159
  if (unit === "quarter") return utcBucket(year, Math.floor(month / 3) * 3);
97
160
  if (unit === "month") return utcBucket(year, month);
98
161
  const day = date.getUTCDate();
162
+ if (unit === "weekday") return date.getUTCDay();
163
+ if (unit === "week") {
164
+ throw new Error("floorUtcTimeUnit requires a calendar week policy.");
165
+ }
99
166
  if (unit === "day") return utcBucket(year, month, day);
100
167
  const hour = date.getUTCHours();
101
168
  if (unit === "hour") return utcBucket(year, month, day, hour);
@@ -104,6 +171,150 @@ export function floorUtcTimeUnit(timestamp, unit) {
104
171
  return Math.floor(timestamp / 1000) * 1000;
105
172
  }
106
173
 
174
+ function weekdayFromDate(year, month, day) {
175
+ return new Date(utcTimestamp(year, month - 1, day)).getUTCDay();
176
+ }
177
+
178
+ export function calendarParts(timestamp, timeZone) {
179
+ if (!validTimestamp(timestamp)) {
180
+ throw new TypeError("Time-unit timestamp must represent a valid date.");
181
+ }
182
+ validateTimeZone(timeZone);
183
+ const parts = Object.fromEntries(formatter(timeZone).formatToParts(timestamp)
184
+ .filter(part => part.type !== "literal")
185
+ .map(part => [part.type, Number(part.value)]));
186
+ const millisecond = ((timestamp % 1000) + 1000) % 1000;
187
+ return {
188
+ year: parts.year,
189
+ month: parts.month,
190
+ day: parts.day,
191
+ hour: parts.hour,
192
+ minute: parts.minute,
193
+ second: parts.second,
194
+ millisecond,
195
+ weekday: weekdayFromDate(parts.year, parts.month, parts.day)
196
+ };
197
+ }
198
+
199
+ function civilTimestamp(parts) {
200
+ const timestamp = utcTimestamp(
201
+ parts.year,
202
+ parts.month - 1,
203
+ parts.day,
204
+ parts.hour ?? 0,
205
+ parts.minute ?? 0
206
+ ) + (parts.second ?? 0) * 1000 + (parts.millisecond ?? 0);
207
+ if (!validTimestamp(timestamp)) {
208
+ throw new RangeError("Time-unit calendar boundary is outside the supported Date range.");
209
+ }
210
+ return timestamp;
211
+ }
212
+
213
+ function sameCivil(left, right) {
214
+ return ["year", "month", "day", "hour", "minute", "second", "millisecond"]
215
+ .every(key => left[key] === right[key]);
216
+ }
217
+
218
+ function sameBucket(candidate, target, unit) {
219
+ const keys = unit === "year" ? ["year"]
220
+ : unit === "quarter" ? ["year", "month"]
221
+ : unit === "month" ? ["year", "month"]
222
+ : unit === "day" || unit === "week" ? ["year", "month", "day"]
223
+ : unit === "hour" ? ["year", "month", "day", "hour"]
224
+ : unit === "minute" ? ["year", "month", "day", "hour", "minute"]
225
+ : ["year", "month", "day", "hour", "minute", "second"];
226
+ if (unit === "quarter") {
227
+ return candidate.year === target.year &&
228
+ Math.floor((candidate.month - 1) / 3) === Math.floor((target.month - 1) / 3);
229
+ }
230
+ return keys.every(key => candidate[key] === target[key]);
231
+ }
232
+
233
+ export function resolveCalendarBoundary(localParts, timeZone, unit) {
234
+ const naive = civilTimestamp(localParts);
235
+ const offsets = new Set();
236
+ for (let delta = -72; delta <= 72; delta += 6) {
237
+ const sample = naive + delta * 3_600_000;
238
+ if (!validTimestamp(sample)) continue;
239
+ offsets.add(civilTimestamp(calendarParts(sample, timeZone)) - sample);
240
+ }
241
+ const exact = [];
242
+ const gaps = [];
243
+ for (const offset of offsets) {
244
+ const candidate = naive - offset;
245
+ if (!validTimestamp(candidate)) continue;
246
+ const roundTrip = calendarParts(candidate, timeZone);
247
+ if (sameCivil(roundTrip, localParts)) exact.push(candidate);
248
+ else if (sameBucket(roundTrip, localParts, unit) &&
249
+ civilTimestamp(roundTrip) >= naive) {
250
+ gaps.push({ candidate, local: civilTimestamp(roundTrip) });
251
+ }
252
+ }
253
+ if (exact.length > 0) return Math.min(...exact);
254
+ if (gaps.length > 0) {
255
+ gaps.sort((left, right) => left.local - right.local || left.candidate - right.candidate);
256
+ return gaps[0].candidate;
257
+ }
258
+ throw new RangeError(`Time-unit ${unit} boundary does not exist in time zone "${timeZone}".`);
259
+ }
260
+
261
+ function addCalendarDays(parts, delta) {
262
+ const date = new Date(utcTimestamp(parts.year, parts.month - 1, parts.day));
263
+ date.setUTCDate(date.getUTCDate() + delta);
264
+ return {
265
+ ...parts,
266
+ year: date.getUTCFullYear(),
267
+ month: date.getUTCMonth() + 1,
268
+ day: date.getUTCDate(),
269
+ weekday: date.getUTCDay()
270
+ };
271
+ }
272
+
273
+ function boundaryParts(parts, transform) {
274
+ const result = {
275
+ year: parts.year,
276
+ month: parts.month,
277
+ day: parts.day,
278
+ hour: parts.hour,
279
+ minute: parts.minute,
280
+ second: parts.second,
281
+ millisecond: 0
282
+ };
283
+ if (transform.unit === "year") Object.assign(result, { month: 1, day: 1, hour: 0, minute: 0, second: 0 });
284
+ else if (transform.unit === "quarter") Object.assign(result, {
285
+ month: Math.floor((parts.month - 1) / 3) * 3 + 1,
286
+ day: 1, hour: 0, minute: 0, second: 0
287
+ });
288
+ else if (transform.unit === "month") Object.assign(result, { day: 1, hour: 0, minute: 0, second: 0 });
289
+ else if (["day", "week"].includes(transform.unit)) Object.assign(result, { hour: 0, minute: 0, second: 0 });
290
+ else if (transform.unit === "hour") Object.assign(result, { minute: 0, second: 0 });
291
+ else if (transform.unit === "minute") result.second = 0;
292
+ if (transform.unit === "week") {
293
+ const distance = (parts.weekday - transform.weekStartsOn + 7) % 7;
294
+ return addCalendarDays(result, -distance);
295
+ }
296
+ return result;
297
+ }
298
+
299
+ export function bucketTime(timestamp, transform) {
300
+ const timeZone = transform.timeZone ?? "UTC";
301
+ if (transform.unit === "weekday") {
302
+ return timeZone === "UTC"
303
+ ? new Date(timestamp).getUTCDay()
304
+ : calendarParts(timestamp, timeZone).weekday;
305
+ }
306
+ if (timeZone === "UTC") {
307
+ if (transform.unit === "week") {
308
+ const date = new Date(timestamp);
309
+ const distance = (date.getUTCDay() - transform.weekStartsOn + 7) % 7;
310
+ return utcBucket(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - distance);
311
+ }
312
+ return floorUtcTimeUnit(timestamp, transform.unit);
313
+ }
314
+ const parts = calendarParts(timestamp, timeZone);
315
+ return resolveCalendarBoundary(boundaryParts(parts, transform), timeZone, transform.unit);
316
+ }
317
+
107
318
  export function deriveTimeUnitRows(rows, transform) {
108
319
  validateTimeUnitTransform(transform);
109
320
  if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
@@ -121,7 +332,7 @@ export function deriveTimeUnitRows(rows, transform) {
121
332
  const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
122
333
  return {
123
334
  ...row,
124
- [transform.as]: floorUtcTimeUnit(timestamp, transform.unit)
335
+ [transform.as]: bucketTime(timestamp, transform)
125
336
  };
126
337
  });
127
338
  return cloneAndFreeze(values);
@@ -2,6 +2,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
2
2
  bin: Object.freeze({ facetTopology: "statistical" }),
3
3
  bin2d: Object.freeze({ facetTopology: "statistical" }),
4
4
  computed: Object.freeze({ facetTopology: "rowPreserving" }),
5
+ complete: Object.freeze({ facetTopology: "statistical" }),
5
6
  boxOutlier: Object.freeze({ facetTopology: "statistical" }),
6
7
  boxSummary: Object.freeze({ facetTopology: "statistical" }),
7
8
  density: Object.freeze({ facetTopology: "statistical" }),
@@ -11,10 +12,13 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
11
12
  gradientProfile: Object.freeze({ facetTopology: "statistical" }),
12
13
  horizon: Object.freeze({ facetTopology: "statistical" }),
13
14
  interval: Object.freeze({ facetTopology: "statistical" }),
15
+ impute: Object.freeze({ facetTopology: "statistical" }),
14
16
  markFilter: Object.freeze({ provenanceTransparent: true }),
17
+ normalize: Object.freeze({ facetTopology: "statistical" }),
15
18
  regression: Object.freeze({ facetTopology: "statistical" }),
16
19
  summary: Object.freeze({ facetTopology: "statistical" }),
17
20
  stack: Object.freeze({ facetTopology: "statistical" }),
21
+ statisticalReference: Object.freeze({ facetTopology: "statistical" }),
18
22
  timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
19
23
  window: Object.freeze({ facetTopology: "statistical" })
20
24
  });