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
@@ -1,12 +1,15 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import {
3
+ maximumMagnitude,
4
+ requireFiniteResult,
3
5
  stableFiniteMean,
4
6
  stableFinitePrefixSums,
5
7
  stableFiniteSum
6
8
  } from "./numeric.js";
9
+ import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
7
10
 
8
11
  const TRANSFORM_KEYS = [
9
- "type", "partitionBy", "sortBy", "operations"
12
+ "type", "partitionBy", "sortBy", "operations", "temporalUnit"
10
13
  ];
11
14
  const SORT_KEYS = ["field", "order"];
12
15
  const ORDER_VALUES = ["ascending", "descending"];
@@ -17,7 +20,15 @@ const OPERATION_VALUES = [
17
20
  const POSITION_OPERATIONS = new Set(OPERATION_VALUES.slice(0, 3));
18
21
  const OFFSET_OPERATIONS = new Set(OPERATION_VALUES.slice(4, 6));
19
22
  const MOVING_OPERATIONS = new Set(OPERATION_VALUES.slice(-2));
20
- const FRAME_KEYS = ["preceding", "following"];
23
+ const FRAME_KEYS = ["preceding", "following", "duration"];
24
+ const DURATION_KEYS = ["preceding", "following", "unit"];
25
+ const DURATION_UNITS = Object.freeze({
26
+ millisecond: 1,
27
+ second: 1_000,
28
+ minute: 60_000,
29
+ hour: 3_600_000,
30
+ day: 86_400_000
31
+ });
21
32
 
22
33
  function requireField(value, label) {
23
34
  if (typeof value !== "string" || value.length === 0) {
@@ -72,7 +83,7 @@ function operationKeys(operation) {
72
83
  return ["op", "field", "as", "offset", "default"];
73
84
  }
74
85
  if (MOVING_OPERATIONS.has(operation.op)) {
75
- return ["op", "field", "as", "frame"];
86
+ return ["op", "field", "as", "frame", "minPeriods", "missing"];
76
87
  }
77
88
  return ["op"];
78
89
  }
@@ -82,7 +93,29 @@ function validateFrame(frame, operation) {
82
93
  throw new TypeError(`Window ${operation} frame must be a plain object.`);
83
94
  }
84
95
  rejectUnknownKeys(frame, FRAME_KEYS, `window ${operation} frame`);
85
- for (const field of FRAME_KEYS) {
96
+ const duration = Object.hasOwn(frame, "duration");
97
+ const rows = ["preceding", "following"].some(field => Object.hasOwn(frame, field));
98
+ if (duration === rows) {
99
+ throw new Error(`Window ${operation} frame requires exactly one row or duration mode.`);
100
+ }
101
+ if (duration) {
102
+ if (!isPlainObject(frame.duration)) {
103
+ throw new TypeError(`Window ${operation} duration must be a plain object.`);
104
+ }
105
+ rejectUnknownKeys(frame.duration, DURATION_KEYS, `window ${operation} duration`);
106
+ for (const field of ["preceding", "following"]) {
107
+ if (!Number.isFinite(frame.duration[field]) || frame.duration[field] < 0) {
108
+ throw new RangeError(
109
+ `Window ${operation} duration ${field} must be a non-negative finite number.`
110
+ );
111
+ }
112
+ }
113
+ if (!Object.hasOwn(DURATION_UNITS, frame.duration.unit)) {
114
+ throw new Error(`Unsupported window duration unit "${frame.duration.unit}".`);
115
+ }
116
+ return;
117
+ }
118
+ for (const field of ["preceding", "following"]) {
86
119
  if (!Number.isInteger(frame[field]) || frame[field] < 0) {
87
120
  throw new RangeError(
88
121
  `Window ${operation} frame ${field} must be a non-negative integer.`
@@ -125,6 +158,12 @@ function validateOperations(operations, sortBy) {
125
158
  }
126
159
  if (MOVING_OPERATIONS.has(operation.op)) {
127
160
  validateFrame(operation.frame, operation.op);
161
+ if (!Number.isSafeInteger(operation.minPeriods) || operation.minPeriods <= 0) {
162
+ throw new RangeError(`Window ${operation.op} minPeriods must be a positive safe integer.`);
163
+ }
164
+ if (!["error", "skip"].includes(operation.missing)) {
165
+ throw new Error(`Unsupported window ${operation.op} missing policy "${operation.missing}".`);
166
+ }
128
167
  }
129
168
  });
130
169
  }
@@ -140,6 +179,18 @@ export function validateWindowTransform(transform) {
140
179
  validateFieldList(transform.partitionBy, "Window partitionBy");
141
180
  validateSortBy(transform.sortBy);
142
181
  validateOperations(transform.operations, transform.sortBy);
182
+ const durationOperations = transform.operations.filter(operation =>
183
+ MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
184
+ );
185
+ const hasTemporalUnit = Object.hasOwn(transform, "temporalUnit");
186
+ if (durationOperations.length > 0) {
187
+ validateTemporalUnit(transform.temporalUnit);
188
+ if (transform.sortBy.length !== 1 || transform.sortBy[0].order !== "ascending") {
189
+ throw new Error("Duration windows require exactly one ascending sortBy field.");
190
+ }
191
+ } else if (hasTemporalUnit) {
192
+ throw new Error("Window temporalUnit requires at least one duration operation.");
193
+ }
143
194
  return transform;
144
195
  }
145
196
 
@@ -152,7 +203,7 @@ function normalizeSortBy(value) {
152
203
  if (value === undefined) return [];
153
204
  if (!Array.isArray(value)) return value;
154
205
  return value.map(sort => isPlainObject(sort)
155
- ? { field: sort.field, order: sort.order ?? "ascending" }
206
+ ? { ...sort, order: sort.order ?? "ascending" }
156
207
  : sort
157
208
  );
158
209
  }
@@ -171,12 +222,20 @@ function normalizeOperations(value) {
171
222
  };
172
223
  }
173
224
  if (MOVING_OPERATIONS.has(operation.op) && isPlainObject(operation.frame)) {
225
+ const duration = isPlainObject(operation.frame.duration)
226
+ ? {
227
+ ...operation.frame.duration,
228
+ following: operation.frame.duration.following ?? 0
229
+ }
230
+ : undefined;
174
231
  return {
175
232
  ...operation,
176
- frame: {
233
+ frame: duration === undefined ? {
177
234
  ...operation.frame,
178
235
  following: operation.frame.following ?? 0
179
- }
236
+ } : { ...operation.frame, duration },
237
+ minPeriods: operation.minPeriods ?? 1,
238
+ missing: operation.missing ?? "error"
180
239
  };
181
240
  }
182
241
  return operation;
@@ -186,13 +245,15 @@ function normalizeOperations(value) {
186
245
  export function normalizeWindowTransform({
187
246
  partitionBy,
188
247
  sortBy,
189
- operations
248
+ operations,
249
+ temporalUnit
190
250
  } = {}) {
191
251
  const transform = {
192
252
  type: "window",
193
253
  partitionBy: normalizePartitionBy(partitionBy),
194
254
  sortBy: normalizeSortBy(sortBy),
195
- operations: normalizeOperations(operations)
255
+ operations: normalizeOperations(operations),
256
+ ...(temporalUnit === undefined ? {} : { temporalUnit })
196
257
  };
197
258
  validateWindowTransform(transform);
198
259
  return cloneAndFreeze(transform);
@@ -249,8 +310,14 @@ function validateSourceFields(rows, transform) {
249
310
  if (!sourceFields.has(field)) {
250
311
  throw new Error(`Window source does not contain field "${field}".`);
251
312
  }
313
+ rows.forEach((row, index) => {
314
+ if (!Object.hasOwn(row, field)) {
315
+ throw new Error(`Window source does not contain field "${field}" at row ${index}.`);
316
+ }
317
+ });
252
318
  }
253
319
  const available = new Set(sourceFields);
320
+ const generated = new Set();
254
321
  for (const operation of transform.operations) {
255
322
  if (
256
323
  !POSITION_OPERATIONS.has(operation.op) &&
@@ -258,10 +325,20 @@ function validateSourceFields(rows, transform) {
258
325
  ) {
259
326
  throw new Error(`Window source does not contain field "${operation.field}".`);
260
327
  }
328
+ if (!POSITION_OPERATIONS.has(operation.op) && !generated.has(operation.field)) {
329
+ rows.forEach((row, index) => {
330
+ if (!Object.hasOwn(row, operation.field)) {
331
+ throw new Error(
332
+ `Window source does not contain field "${operation.field}" at row ${index}.`
333
+ );
334
+ }
335
+ });
336
+ }
261
337
  if (available.has(operation.as)) {
262
338
  throw new Error(`Window output field "${operation.as}" already exists.`);
263
339
  }
264
340
  available.add(operation.as);
341
+ generated.add(operation.as);
265
342
  }
266
343
  }
267
344
 
@@ -316,6 +393,9 @@ function operationValues(
316
393
  const values = [];
317
394
  for (let index = first; index <= last; index += 1) {
318
395
  const value = partition[index].row[operation.field];
396
+ if (value === null || value === undefined) {
397
+ if (operation.missing === "skip") continue;
398
+ }
319
399
  if (!Number.isFinite(value)) {
320
400
  throw new TypeError(
321
401
  `Window ${operation.op} field "${operation.field}" must contain finite numbers.`
@@ -326,6 +406,18 @@ function operationValues(
326
406
  return values;
327
407
  }
328
408
 
409
+ function validateOperationValues(partition, operation) {
410
+ for (const entry of partition) {
411
+ const value = entry.row[operation.field];
412
+ if ((value === null || value === undefined) && operation.missing === "skip") continue;
413
+ if (!Number.isFinite(value)) {
414
+ throw new TypeError(
415
+ `Window ${operation.op} field "${operation.field}" must contain finite numbers.`
416
+ );
417
+ }
418
+ }
419
+ }
420
+
329
421
  function stableWindowOutput(operation, calculate) {
330
422
  const label = `Window ${operation.op} output "${operation.as}"`;
331
423
  try {
@@ -342,7 +434,98 @@ function setWindowOutput(entry, field, value) {
342
434
  });
343
435
  }
344
436
 
345
- function applyOperation(partition, operation, sortBy) {
437
+ function durationMilliseconds(duration, field) {
438
+ const value = duration[field] * DURATION_UNITS[duration.unit];
439
+ if (!Number.isFinite(value)) {
440
+ throw new RangeError(`Window duration ${field} is outside the finite numeric range.`);
441
+ }
442
+ return value;
443
+ }
444
+
445
+ function durationPositions(partition, transform) {
446
+ const field = transform.sortBy[0].field;
447
+ return partition.map((entry, index) => entry.durationTimestamp ??
448
+ normalizeTemporalValue(entry.row[field], field, index, transform.temporalUnit));
449
+ }
450
+
451
+ function validDateBoundary(value) {
452
+ return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
453
+ }
454
+
455
+ function applyDurationOperation(partition, operation, transform) {
456
+ validateOperationValues(partition, operation);
457
+ const positions = durationPositions(partition, transform);
458
+ const preceding = durationMilliseconds(operation.frame.duration, "preceding");
459
+ const following = durationMilliseconds(operation.frame.duration, "following");
460
+ const rawValues = partition.map(entry => entry.row[operation.field]);
461
+ const finiteValues = rawValues.filter(Number.isFinite);
462
+ const scale = maximumMagnitude(finiteValues) || 1;
463
+ let left = 0;
464
+ let right = 0;
465
+ let total = 0;
466
+ let correction = 0;
467
+ let count = 0;
468
+ let ordinary = 0;
469
+ let ordinaryReliable = true;
470
+ const add = value => {
471
+ if (!Number.isFinite(value)) return;
472
+ if (ordinaryReliable) {
473
+ ordinary += value;
474
+ if (!Number.isFinite(ordinary)) ordinaryReliable = false;
475
+ }
476
+ const scaled = value / scale;
477
+ const next = total + scaled;
478
+ correction += Math.abs(total) >= Math.abs(scaled)
479
+ ? total - next + scaled
480
+ : scaled - next + total;
481
+ total = next;
482
+ count += 1;
483
+ };
484
+ const remove = value => {
485
+ if (!Number.isFinite(value)) return;
486
+ if (ordinaryReliable) ordinary -= value;
487
+ const scaled = -value / scale;
488
+ const next = total + scaled;
489
+ correction += Math.abs(total) >= Math.abs(scaled)
490
+ ? total - next + scaled
491
+ : scaled - next + total;
492
+ total = next;
493
+ count -= 1;
494
+ };
495
+ let index = 0;
496
+ while (index < partition.length) {
497
+ const timestamp = positions[index];
498
+ const lower = timestamp - preceding;
499
+ const upper = timestamp + following;
500
+ if (!validDateBoundary(lower) || !validDateBoundary(upper)) {
501
+ throw new RangeError("Window duration boundary is outside the supported Date range.");
502
+ }
503
+ while (right < partition.length && positions[right] <= upper) {
504
+ add(rawValues[right]);
505
+ right += 1;
506
+ }
507
+ while (left < right && positions[left] < lower) {
508
+ remove(rawValues[left]);
509
+ left += 1;
510
+ }
511
+ let end = index + 1;
512
+ while (end < partition.length && positions[end] === timestamp) end += 1;
513
+ let value = null;
514
+ if (count >= operation.minPeriods) {
515
+ const sum = requireFiniteResult(ordinaryReliable ? ordinary : (total + correction) * scale,
516
+ `Window ${operation.op} output "${operation.as}"`);
517
+ value = operation.op === "movingMean"
518
+ ? requireFiniteResult(sum / count, `Window ${operation.op} output "${operation.as}"`)
519
+ : sum;
520
+ }
521
+ for (let peer = index; peer < end; peer += 1) {
522
+ setWindowOutput(partition[peer], operation.as, value);
523
+ }
524
+ index = end;
525
+ }
526
+ }
527
+
528
+ function applyOperation(partition, operation, sortBy, transform) {
346
529
  if (operation.op === "rowNumber") {
347
530
  partition.forEach((entry, index) => {
348
531
  setWindowOutput(entry, operation.as, index + 1);
@@ -355,7 +538,9 @@ function applyOperation(partition, operation, sortBy) {
355
538
  partition.forEach((entry, index) => {
356
539
  if (
357
540
  index > 0 &&
358
- compareEntries(entry, partition[index - 1], sortBy, false) !== 0
541
+ (Object.hasOwn(entry, "durationTimestamp")
542
+ ? entry.durationTimestamp !== partition[index - 1].durationTimestamp
543
+ : compareEntries(entry, partition[index - 1], sortBy, false) !== 0)
359
544
  ) {
360
545
  rank = index + 1;
361
546
  denseRank += 1;
@@ -375,6 +560,11 @@ function applyOperation(partition, operation, sortBy) {
375
560
  return;
376
561
  }
377
562
  if (MOVING_OPERATIONS.has(operation.op)) {
563
+ if (Object.hasOwn(operation.frame, "duration")) {
564
+ applyDurationOperation(partition, operation, transform);
565
+ return;
566
+ }
567
+ validateOperationValues(partition, operation);
378
568
  const calculate = operation.op === "movingMean"
379
569
  ? stableFiniteMean
380
570
  : stableFiniteSum;
@@ -385,9 +575,9 @@ function applyOperation(partition, operation, sortBy) {
385
575
  index + operation.frame.following
386
576
  );
387
577
  const values = operationValues(partition, operation, first, last);
388
- setWindowOutput(entry, operation.as, stableWindowOutput(operation, label =>
389
- calculate(values, label)
390
- ));
578
+ setWindowOutput(entry, operation.as, values.length < operation.minPeriods
579
+ ? null
580
+ : stableWindowOutput(operation, label => calculate(values, label)));
391
581
  });
392
582
  return;
393
583
  }
@@ -411,13 +601,28 @@ export function deriveWindowRows(rows, transform) {
411
601
  validateSourceFields(rows, transform);
412
602
  const entries = rows.map((row, index) => ({ index, row: { ...row } }));
413
603
  const partitions = partitionRows(entries, transform.partitionBy);
604
+ const hasDuration = transform.operations.some(operation =>
605
+ MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
606
+ );
414
607
  for (const partition of partitions) {
415
608
  validatePartitionSortValues(partition, transform);
416
- partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
609
+ if (hasDuration) {
610
+ const field = transform.sortBy[0].field;
611
+ partition.forEach(entry => {
612
+ entry.durationTimestamp = normalizeTemporalValue(
613
+ entry.row[field], field, entry.index, transform.temporalUnit
614
+ );
615
+ });
616
+ partition.sort((left, right) =>
617
+ left.durationTimestamp - right.durationTimestamp || left.index - right.index
618
+ );
619
+ } else {
620
+ partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
621
+ }
417
622
  }
418
623
  for (const operation of transform.operations) {
419
624
  for (const partition of partitions) {
420
- applyOperation(partition, operation, transform.sortBy);
625
+ applyOperation(partition, operation, transform.sortBy, transform);
421
626
  }
422
627
  }
423
628
  return cloneAndFreeze(entries.map(entry => entry.row));
@@ -0,0 +1,80 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const ALIGN_FRACTIONS = Object.freeze({ start: 0, center: 0.5, end: 1 });
4
+ const ASPECT_MODES = new Set(["frame", "data"]);
5
+
6
+ function validateAspect(value) {
7
+ if (!isPlainObject(value) || !ASPECT_MODES.has(value.mode)) {
8
+ throw new TypeError("Coordinate aspect resolver requires frame or data mode.");
9
+ }
10
+ if (!Number.isFinite(value.ratio) || value.ratio <= 0) {
11
+ throw new RangeError("Coordinate aspect ratio must be a positive finite number.");
12
+ }
13
+ const alignX = value.alignX ?? "center";
14
+ const alignY = value.alignY ?? "center";
15
+ if (!(alignX in ALIGN_FRACTIONS) || !(alignY in ALIGN_FRACTIONS)) {
16
+ throw new Error("Coordinate aspect alignment must be start, center, or end.");
17
+ }
18
+ return { mode: value.mode, ratio: value.ratio, alignX, alignY };
19
+ }
20
+
21
+ function validateBounds(bounds) {
22
+ if (
23
+ !isPlainObject(bounds) ||
24
+ ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
25
+ bounds.width < 0 ||
26
+ bounds.height < 0
27
+ ) {
28
+ throw new TypeError(
29
+ "Coordinate aspect requires finite, non-negative allocated bounds."
30
+ );
31
+ }
32
+ return bounds;
33
+ }
34
+
35
+ function domainSpan(domain, channel) {
36
+ if (
37
+ !Array.isArray(domain) ||
38
+ domain.length !== 2 ||
39
+ !domain.every(Number.isFinite)
40
+ ) {
41
+ throw new TypeError(
42
+ `Coordinate data aspect requires a finite two-value ${channel} domain.`
43
+ );
44
+ }
45
+ const span = Math.abs(domain[1] - domain[0]);
46
+ if (!Number.isFinite(span) || span <= 0) {
47
+ throw new RangeError(
48
+ `Coordinate data aspect requires a positive ${channel} domain span.`
49
+ );
50
+ }
51
+ return span;
52
+ }
53
+
54
+ export function resolveEffectiveBounds(allocatedBounds, requestedAspect, domains) {
55
+ const allocated = validateBounds(allocatedBounds);
56
+ if (requestedAspect === undefined || requestedAspect === "auto") {
57
+ return cloneAndFreeze(allocated);
58
+ }
59
+ const aspect = validateAspect(requestedAspect);
60
+ const ratio = aspect.mode === "frame"
61
+ ? aspect.ratio
62
+ : aspect.ratio * domainSpan(domains?.x, "x") /
63
+ domainSpan(domains?.y, "y");
64
+ if (!Number.isFinite(ratio) || ratio <= 0) {
65
+ throw new RangeError(
66
+ "Coordinate aspect must resolve to a positive finite frame ratio."
67
+ );
68
+ }
69
+ const width = Math.min(allocated.width, allocated.height * ratio);
70
+ const height = width / ratio;
71
+ if (![width, height].every(Number.isFinite)) {
72
+ throw new RangeError("Coordinate aspect bounds exceed the finite numeric range.");
73
+ }
74
+ return cloneAndFreeze({
75
+ x: allocated.x + (allocated.width - width) * ALIGN_FRACTIONS[aspect.alignX],
76
+ y: allocated.y + (allocated.height - height) * ALIGN_FRACTIONS[aspect.alignY],
77
+ width,
78
+ height
79
+ });
80
+ }
@@ -1,4 +1,4 @@
1
- import { cloneAndFreeze } from "../core/immutable.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import {
3
3
  DEFAULT_COMPOSITION_LAYOUT,
4
4
  normalizeCompositionAlign,
@@ -52,6 +52,49 @@ function alignedOffset(remaining, align) {
52
52
  return remaining / 2;
53
53
  }
54
54
 
55
+ const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
56
+
57
+ function normalizeHeaderLayout(value, rows) {
58
+ if (value === undefined) {
59
+ return {
60
+ outer: ZERO_SIDES,
61
+ cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
62
+ };
63
+ }
64
+ if (!isPlainObject(value) || Object.keys(value).some(
65
+ key => !["outer", "cellRows"].includes(key)
66
+ )) {
67
+ throw new TypeError("Facet headerLayout must contain outer and cellRows.");
68
+ }
69
+ const outer = { ...ZERO_SIDES, ...(value.outer ?? {}) };
70
+ if (!isPlainObject(value.outer ?? {}) || Object.keys(outer).some(
71
+ key => !Object.hasOwn(ZERO_SIDES, key)
72
+ )) {
73
+ throw new TypeError("Facet headerLayout.outer must contain side reservations.");
74
+ }
75
+ for (const [side, amount] of Object.entries(outer)) {
76
+ validateCompositionSpacing(amount, `Facet headerLayout.outer.${side}`);
77
+ }
78
+ const sourceRows = value.cellRows ?? {};
79
+ if (!isPlainObject(sourceRows) || Object.keys(sourceRows).some(
80
+ key => !["top", "bottom"].includes(key)
81
+ )) {
82
+ throw new TypeError("Facet headerLayout.cellRows must contain top and bottom arrays.");
83
+ }
84
+ const cellRows = Object.fromEntries(["top", "bottom"].map(side => {
85
+ const amounts = sourceRows[side] ?? Array(rows).fill(0);
86
+ if (!Array.isArray(amounts) || amounts.length !== rows) {
87
+ throw new RangeError(`Facet headerLayout.cellRows.${side} must match row count.`);
88
+ }
89
+ amounts.forEach((amount, index) => validateCompositionSpacing(
90
+ amount,
91
+ `Facet headerLayout.cellRows.${side}[${index}]`
92
+ ));
93
+ return [side, [...amounts]];
94
+ }));
95
+ return { outer, cellRows };
96
+ }
97
+
55
98
  export function resolveFacetLayout({
56
99
  children,
57
100
  columns,
@@ -63,7 +106,8 @@ export function resolveFacetLayout({
63
106
  sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
64
107
  sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
65
108
  sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
66
- sharedLegendPosition = "right"
109
+ sharedLegendPosition = "right",
110
+ headerLayout
67
111
  } = {}) {
68
112
  const values = children?.map(child => child?.value);
69
113
  const coordinates = resolveGridCoordinates(children ?? [], columns);
@@ -106,6 +150,7 @@ export function resolveFacetLayout({
106
150
  : 0;
107
151
  const rowCount = coordinates?.rows ??
108
152
  Math.ceil(resolvedChildren.length / resolvedColumns);
153
+ const headers = normalizeHeaderLayout(headerLayout, rowCount);
109
154
  const columnWidths = Array(resolvedColumns).fill(-Infinity);
110
155
  const rowHeights = Array(rowCount).fill(-Infinity);
111
156
  resolvedChildren.forEach((child, index) => {
@@ -119,12 +164,14 @@ export function resolveFacetLayout({
119
164
  rowHeights[row] = Math.max(rowHeights[row], child.height);
120
165
  });
121
166
  const columnStarts = columnWidths.map((_, column) =>
122
- resolvedPadding.left + columnWidths.slice(0, column)
167
+ resolvedPadding.left + headers.outer.left + columnWidths.slice(0, column)
123
168
  .reduce((sum, width) => sum + width, 0) + resolvedGap * column
124
169
  );
125
170
  const rowStarts = rowHeights.map((_, row) =>
126
- resolvedTitleHeight + resolvedPadding.top + rowHeights.slice(0, row)
127
- .reduce((sum, height) => sum + height, 0) + resolvedGap * row
171
+ resolvedTitleHeight + resolvedPadding.top + headers.outer.top +
172
+ rowHeights.slice(0, row).reduce((sum, height, index) =>
173
+ sum + headers.cellRows.top[index] + height + headers.cellRows.bottom[index], 0) +
174
+ resolvedGap * row + headers.cellRows.top[row]
128
175
  );
129
176
  const placements = resolvedChildren.map((child, index) => {
130
177
  const column = coordinates === undefined
@@ -153,12 +200,18 @@ export function resolveFacetLayout({
153
200
  };
154
201
  });
155
202
  const gridWidth = resolvedPadding.left +
203
+ headers.outer.left +
156
204
  columnWidths.reduce((sum, width) => sum + width, 0) +
157
205
  resolvedGap * Math.max(0, resolvedColumns - 1) +
206
+ headers.outer.right +
158
207
  resolvedPadding.right;
159
208
  const gridHeight = resolvedTitleHeight + resolvedPadding.top +
209
+ headers.outer.top +
160
210
  rowHeights.reduce((sum, height) => sum + height, 0) +
211
+ headers.cellRows.top.reduce((sum, amount) => sum + amount, 0) +
212
+ headers.cellRows.bottom.reduce((sum, amount) => sum + amount, 0) +
161
213
  resolvedGap * Math.max(0, rowCount - 1) +
214
+ headers.outer.bottom +
162
215
  resolvedPadding.bottom;
163
216
  return cloneAndFreeze({
164
217
  columns: resolvedColumns,
@@ -167,6 +220,7 @@ export function resolveFacetLayout({
167
220
  align: resolvedAlign,
168
221
  padding: resolvedPadding,
169
222
  titleHeight: resolvedTitleHeight,
223
+ headerLayout: headers,
170
224
  gridWidth,
171
225
  width: gridWidth + (sideLegend ? legendLane : 0),
172
226
  height: gridHeight + (sideLegend ? 0 : legendLane),