ggaction 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +10 -3
  11. package/src/actions/basic.js +4 -2
  12. package/src/actions/boxPlots/components.js +14 -4
  13. package/src/actions/boxPlots/edit.js +31 -5
  14. package/src/actions/boxPlots/materialize.js +13 -4
  15. package/src/actions/boxPlots/options.js +48 -16
  16. package/src/actions/canvas/actions.js +3 -1
  17. package/src/actions/canvas/fitting.js +211 -0
  18. package/src/actions/canvas/index.js +2 -0
  19. package/src/actions/categoryOrder/index.js +18 -6
  20. package/src/actions/charts/area.js +51 -0
  21. package/src/actions/charts/bar.js +13 -6
  22. package/src/actions/charts/beeswarm.js +70 -0
  23. package/src/actions/charts/density.js +48 -0
  24. package/src/actions/charts/ecdf.js +186 -0
  25. package/src/actions/charts/endpoints.js +473 -0
  26. package/src/actions/charts/heatmap.js +4 -3
  27. package/src/actions/charts/histogram.js +5 -4
  28. package/src/actions/charts/horizon.js +73 -0
  29. package/src/actions/charts/index.js +37 -0
  30. package/src/actions/charts/interval-regression.js +172 -0
  31. package/src/actions/charts/line.js +8 -6
  32. package/src/actions/charts/parallel.js +8 -2
  33. package/src/actions/charts/pie.js +52 -0
  34. package/src/actions/charts/polar.js +134 -0
  35. package/src/actions/charts/radar.js +305 -0
  36. package/src/actions/charts/radial.js +50 -0
  37. package/src/actions/charts/raincloud.js +422 -0
  38. package/src/actions/charts/rug-strip.js +283 -0
  39. package/src/actions/charts/scatter.js +11 -3
  40. package/src/actions/charts/shared.js +69 -10
  41. package/src/actions/composition/actions.js +129 -3
  42. package/src/actions/coordinates/actions.js +1 -1
  43. package/src/actions/coordinates/edit.js +76 -0
  44. package/src/actions/data/bin.js +29 -0
  45. package/src/actions/data/bin2d.js +57 -185
  46. package/src/actions/data/complete.js +26 -0
  47. package/src/actions/data/computed.js +24 -0
  48. package/src/actions/data/create.js +2 -2
  49. package/src/actions/data/density.js +12 -20
  50. package/src/actions/data/derived.js +111 -15
  51. package/src/actions/data/ecdf.js +38 -0
  52. package/src/actions/data/edit.js +539 -0
  53. package/src/actions/data/filter.js +226 -30
  54. package/src/actions/data/fold.js +24 -0
  55. package/src/actions/data/impute.js +26 -0
  56. package/src/actions/data/index.js +38 -0
  57. package/src/actions/data/interval.js +2 -0
  58. package/src/actions/data/intervalEdit.js +86 -4
  59. package/src/actions/data/normalize.js +27 -0
  60. package/src/actions/data/regression.js +19 -21
  61. package/src/actions/data/revisionBindings.js +145 -0
  62. package/src/actions/data/shared.js +20 -4
  63. package/src/actions/data/stack.js +26 -0
  64. package/src/actions/data/summary.js +26 -0
  65. package/src/actions/data/timeUnit.js +4 -1
  66. package/src/actions/data/window.js +1 -1
  67. package/src/actions/encodings/appearance.js +32 -13
  68. package/src/actions/encodings/barWidth.js +8 -0
  69. package/src/actions/encodings/basic.js +2 -0
  70. package/src/actions/encodings/channels.js +415 -0
  71. package/src/actions/encodings/color/continuous.js +19 -59
  72. package/src/actions/encodings/color/index.js +30 -23
  73. package/src/actions/encodings/color/layout.js +3 -97
  74. package/src/actions/encodings/color/policy.js +5 -8
  75. package/src/actions/encodings/density/resolve.js +37 -5
  76. package/src/actions/encodings/density.js +124 -34
  77. package/src/actions/encodings/histogram.js +3 -2
  78. package/src/actions/encodings/horizon/resolve.js +8 -4
  79. package/src/actions/encodings/horizon.js +18 -8
  80. package/src/actions/encodings/index.js +6 -0
  81. package/src/actions/encodings/offset.js +30 -29
  82. package/src/actions/encodings/parallel.js +2 -8
  83. package/src/actions/encodings/position/apply.js +63 -11
  84. package/src/actions/encodings/position/index.js +52 -6
  85. package/src/actions/encodings/position/policies/arc.js +14 -0
  86. package/src/actions/encodings/position/policies/area.js +4 -42
  87. package/src/actions/encodings/position/policies/bar.js +57 -13
  88. package/src/actions/encodings/position/resolve.js +88 -26
  89. package/src/actions/encodings/ranged.js +123 -97
  90. package/src/actions/encodings/remove.js +44 -34
  91. package/src/actions/encodings/ruleAppearance.js +24 -32
  92. package/src/actions/encodings/seriesLayout.js +105 -0
  93. package/src/actions/encodings/shared.js +107 -37
  94. package/src/actions/encodings/stroke.js +226 -0
  95. package/src/actions/encodings/strokeDash.js +9 -6
  96. package/src/actions/encodings/temporal.js +7 -0
  97. package/src/actions/encodings/text.js +30 -15
  98. package/src/actions/errorBands/create.js +41 -8
  99. package/src/actions/errorBands/edit.js +360 -25
  100. package/src/actions/errorBars/create.js +56 -17
  101. package/src/actions/errorBars/edit.js +372 -21
  102. package/src/actions/facets/actions.js +619 -41
  103. package/src/actions/facets/derive.js +139 -25
  104. package/src/actions/gradientPlots/components.js +43 -13
  105. package/src/actions/gradientPlots/edit.js +14 -4
  106. package/src/actions/gradientPlots/materialize.js +8 -8
  107. package/src/actions/gradientPlots/options.js +10 -2
  108. package/src/actions/guides/applicability.js +28 -19
  109. package/src/actions/guides/axes/axes.js +4 -3
  110. package/src/actions/guides/axes/axis.js +18 -18
  111. package/src/actions/guides/axes/components.js +43 -0
  112. package/src/actions/guides/axes/edit.js +16 -51
  113. package/src/actions/guides/axes/labels.js +184 -28
  114. package/src/actions/guides/axes/lines.js +13 -7
  115. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  116. package/src/actions/guides/axes/parallel/policy.js +137 -0
  117. package/src/actions/guides/axes/parallel/resolve.js +50 -25
  118. package/src/actions/guides/axes/parallel.js +93 -217
  119. package/src/actions/guides/axes/policy.js +21 -67
  120. package/src/actions/guides/axes/tickGroups.js +15 -8
  121. package/src/actions/guides/axes/ticks.js +11 -6
  122. package/src/actions/guides/axes/titles.js +32 -11
  123. package/src/actions/guides/basic.js +2 -0
  124. package/src/actions/guides/facade.js +106 -0
  125. package/src/actions/guides/facadeAxes.js +200 -0
  126. package/src/actions/guides/facadeLegend.js +63 -0
  127. package/src/actions/guides/grids/grid.js +37 -33
  128. package/src/actions/guides/grids/resolve.js +17 -4
  129. package/src/actions/guides/guides.js +54 -31
  130. package/src/actions/guides/legends/blocks.js +426 -0
  131. package/src/actions/guides/legends/categorical/actions.js +309 -279
  132. package/src/actions/guides/legends/categorical/components.js +21 -17
  133. package/src/actions/guides/legends/categorical/index.js +37 -33
  134. package/src/actions/guides/legends/categorical/layout.js +123 -354
  135. package/src/actions/guides/legends/categorical/options.js +32 -16
  136. package/src/actions/guides/legends/categorical/recipes.js +114 -9
  137. package/src/actions/guides/legends/categorical/resolve.js +30 -15
  138. package/src/actions/guides/legends/categorical/symbols.js +87 -33
  139. package/src/actions/guides/legends/continuous/common.js +99 -26
  140. package/src/actions/guides/legends/continuous/gradient.js +120 -89
  141. package/src/actions/guides/legends/continuous/index.js +8 -2
  142. package/src/actions/guides/legends/continuous/interval.js +79 -95
  143. package/src/actions/guides/legends/continuous/opacity.js +113 -89
  144. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  145. package/src/actions/guides/legends/creation.js +40 -0
  146. package/src/actions/guides/legends/edit.js +321 -90
  147. package/src/actions/guides/legends/focused.js +2 -2
  148. package/src/actions/guides/legends/index.js +2 -0
  149. package/src/actions/guides/legends/lane.js +149 -102
  150. package/src/actions/guides/legends/lifecycle.js +143 -0
  151. package/src/actions/guides/legends/remove.js +43 -78
  152. package/src/actions/guides/legends/sampling.js +140 -0
  153. package/src/actions/guides/legends/size.js +213 -116
  154. package/src/actions/guides/legends/strokeWidth.js +121 -73
  155. package/src/actions/guides/legends/target.js +123 -0
  156. package/src/actions/guides/legends/transition.js +192 -0
  157. package/src/actions/guides/polar/axes/facade.js +81 -66
  158. package/src/actions/guides/polar/axes/labels.js +28 -8
  159. package/src/actions/guides/polar/axes/lines.js +12 -5
  160. package/src/actions/guides/polar/axes/shared.js +22 -0
  161. package/src/actions/guides/polar/axes/ticks.js +6 -5
  162. package/src/actions/guides/polar/axes/titles.js +6 -5
  163. package/src/actions/guides/polar/grids.js +8 -8
  164. package/src/actions/guides/polar/resolve.js +12 -8
  165. package/src/actions/guides/reuse.js +44 -0
  166. package/src/actions/guides/tickValues.js +19 -21
  167. package/src/actions/index.js +15 -2
  168. package/src/actions/intervals/resolve.js +35 -15
  169. package/src/actions/marks/arc/actions.js +78 -20
  170. package/src/actions/marks/area/actions.js +51 -12
  171. package/src/actions/marks/area/materialize.js +22 -3
  172. package/src/actions/marks/bar/create.js +11 -2
  173. package/src/actions/marks/bar/edit.js +20 -3
  174. package/src/actions/marks/bar/materialize.js +33 -9
  175. package/src/actions/marks/index.js +14 -0
  176. package/src/actions/marks/line/actions.js +71 -113
  177. package/src/actions/marks/line/materialize.js +57 -56
  178. package/src/actions/marks/point/create.js +8 -2
  179. package/src/actions/marks/point/edit.js +29 -21
  180. package/src/actions/marks/point/index.js +1 -0
  181. package/src/actions/marks/point/jitter.js +3 -0
  182. package/src/actions/marks/point/materialize.js +154 -16
  183. package/src/actions/marks/point/packing.js +68 -0
  184. package/src/actions/marks/rect/actions.js +33 -12
  185. package/src/actions/marks/references.js +658 -0
  186. package/src/actions/marks/remove.js +87 -4
  187. package/src/actions/marks/rule/actions.js +77 -9
  188. package/src/actions/marks/rule/appearance.js +26 -0
  189. package/src/actions/marks/shared.js +13 -13
  190. package/src/actions/marks/text/actions.js +296 -13
  191. package/src/actions/marks/text/index.js +4 -0
  192. package/src/actions/marks/text/layout.js +156 -8
  193. package/src/actions/marks/text/removal.js +173 -0
  194. package/src/actions/marks/text/selection.js +73 -0
  195. package/src/actions/marks/tick/actions.js +22 -5
  196. package/src/actions/primitives/createGraphics.js +3 -1
  197. package/src/actions/primitives/editGraphics.js +26 -0
  198. package/src/actions/primitives/semantic.js +3 -2
  199. package/src/actions/primitives/semanticAction.js +120 -10
  200. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  201. package/src/actions/primitives/semanticValidation/index.js +9 -1
  202. package/src/actions/primitives/semanticValidation/layer.js +23 -1
  203. package/src/actions/primitives/semanticValidation/scale.js +38 -8
  204. package/src/actions/regression/components.js +26 -11
  205. package/src/actions/regression/create.js +11 -3
  206. package/src/actions/regression/edit.js +40 -13
  207. package/src/actions/regression/resolve.js +1 -1
  208. package/src/actions/resources/index.js +7 -0
  209. package/src/actions/resources/remove.js +172 -0
  210. package/src/actions/scales/channels.js +212 -0
  211. package/src/actions/scales/consumers/common.js +21 -54
  212. package/src/actions/scales/consumers/families.js +28 -14
  213. package/src/actions/scales/consumers/index.js +42 -4
  214. package/src/actions/scales/consumers/seriesLayout.js +57 -12
  215. package/src/actions/scales/create.js +58 -26
  216. package/src/actions/scales/definitions.js +109 -39
  217. package/src/actions/scales/edit.js +101 -34
  218. package/src/actions/scales/editPolicy.js +95 -35
  219. package/src/actions/scales/index.js +6 -0
  220. package/src/actions/scales/materialize.js +14 -49
  221. package/src/actions/scales/offset.js +48 -0
  222. package/src/actions/scales/parallel.js +71 -0
  223. package/src/actions/scales/patch.js +1 -1
  224. package/src/actions/scales/preview.js +140 -0
  225. package/src/actions/scales/quantitativeColor.js +9 -5
  226. package/src/actions/selection/actions.js +154 -30
  227. package/src/actions/selection/index.js +3 -0
  228. package/src/actions/theme/actions.js +98 -0
  229. package/src/actions/theme/composition.js +238 -0
  230. package/src/actions/theme/index.js +16 -0
  231. package/src/actions/theme/reconcile.js +1130 -0
  232. package/src/actions/theme/state.js +160 -0
  233. package/src/actions/titles/actions.js +11 -10
  234. package/src/actions/titles/resolve.js +1 -40
  235. package/src/actions/violinPlots/create.js +90 -43
  236. package/src/actions/violinPlots/edit.js +192 -0
  237. package/src/actions/violinPlots/index.js +2 -0
  238. package/src/core/ChartProgram.js +7 -0
  239. package/src/core/action.js +32 -2
  240. package/src/core/compositionState.js +141 -8
  241. package/src/core/programState.js +20 -0
  242. package/src/core/resourceReferences.js +332 -0
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/grammar/aggregate.js +66 -23
  245. package/src/grammar/arcs.js +58 -1
  246. package/src/grammar/areaEndpoints.js +46 -0
  247. package/src/grammar/areaSeries.js +87 -41
  248. package/src/grammar/bars/aggregate.js +37 -13
  249. package/src/grammar/bars/geometry.js +44 -0
  250. package/src/grammar/bars/policy.js +9 -7
  251. package/src/grammar/bin.js +226 -0
  252. package/src/grammar/categoryOrder.js +34 -0
  253. package/src/grammar/complete.js +279 -0
  254. package/src/grammar/computed.js +351 -0
  255. package/src/grammar/coordinates.js +37 -0
  256. package/src/grammar/density.js +239 -37
  257. package/src/grammar/displayLabels.js +57 -0
  258. package/src/grammar/ecdf.js +198 -0
  259. package/src/grammar/facets/dependencies.js +133 -1
  260. package/src/grammar/facets/guides.js +3 -0
  261. package/src/grammar/facets/index.js +123 -52
  262. package/src/grammar/facets/scales.js +15 -18
  263. package/src/grammar/fold.js +123 -0
  264. package/src/grammar/horizon.js +6 -4
  265. package/src/grammar/impute.js +340 -0
  266. package/src/grammar/interval.js +37 -20
  267. package/src/grammar/lineSeries.js +20 -63
  268. package/src/grammar/markFilter.js +22 -4
  269. package/src/grammar/markLabelSelection.js +57 -0
  270. package/src/grammar/markLabels.js +110 -0
  271. package/src/grammar/normalize.js +366 -0
  272. package/src/grammar/parallelAxisTitles.js +16 -0
  273. package/src/grammar/pathSeries.js +109 -0
  274. package/src/grammar/pointPacking.js +183 -0
  275. package/src/grammar/pointShapes.js +13 -4
  276. package/src/grammar/polar.js +90 -8
  277. package/src/grammar/positionDatum.js +12 -0
  278. package/src/grammar/rects.js +9 -1
  279. package/src/grammar/regression/derive.js +4 -0
  280. package/src/grammar/regression/index.js +8 -2
  281. package/src/grammar/regression/models.js +22 -7
  282. package/src/grammar/regression/parameters.js +63 -13
  283. package/src/grammar/rotation.js +29 -0
  284. package/src/grammar/roundedRect.js +180 -0
  285. package/src/grammar/rules.js +8 -21
  286. package/src/grammar/scales/color.js +23 -1
  287. package/src/grammar/scales/colorConsumers.js +57 -0
  288. package/src/grammar/scales/continuous.js +2 -2
  289. package/src/grammar/scales/definition.js +42 -5
  290. package/src/grammar/scales/discretized.js +11 -5
  291. package/src/grammar/scales/fields.js +38 -8
  292. package/src/grammar/scales/index.js +1 -0
  293. package/src/grammar/scales/mapping.js +2 -0
  294. package/src/grammar/scales/ordinal.js +12 -3
  295. package/src/grammar/scales/policies.js +2 -2
  296. package/src/grammar/scales/radial.js +55 -0
  297. package/src/grammar/scales/size.js +456 -0
  298. package/src/grammar/scales/types.js +1 -0
  299. package/src/grammar/schemas/concreteGraphic.js +14 -2
  300. package/src/grammar/schemas/graphic.js +15 -0
  301. package/src/grammar/schemas/graphicBounds.js +179 -30
  302. package/src/grammar/schemas/semanticPath.js +23 -4
  303. package/src/grammar/stack.js +170 -0
  304. package/src/grammar/statisticalReference.js +103 -0
  305. package/src/grammar/statistics/confidenceInterval.js +101 -0
  306. package/src/grammar/strokeStyle.js +77 -0
  307. package/src/grammar/summary.js +245 -0
  308. package/src/grammar/text.js +22 -16
  309. package/src/grammar/timeUnit.js +234 -13
  310. package/src/grammar/transformTopology.js +10 -0
  311. package/src/grammar/transforms.js +418 -16
  312. package/src/grammar/valueFormat.js +112 -0
  313. package/src/grammar/weightedStatistics.js +374 -0
  314. package/src/grammar/window.js +232 -21
  315. package/src/layout/aspect.js +80 -0
  316. package/src/layout/facets.js +100 -12
  317. package/src/layout/guideCollisions.js +12 -0
  318. package/src/layout/labels.js +343 -0
  319. package/src/layout/legend.js +2 -10
  320. package/src/layout/legendItems.js +102 -0
  321. package/src/layout/legendLane.js +100 -11
  322. package/src/materialization/bars/aggregate.js +56 -147
  323. package/src/materialization/bars/histogram.js +104 -20
  324. package/src/materialization/bars/ranged.js +19 -3
  325. package/src/materialization/bars/resolve.js +7 -4
  326. package/src/materialization/categorySlotOffset.js +40 -0
  327. package/src/materialization/composition.js +3 -1
  328. package/src/materialization/compositionSnapshot.js +1 -1
  329. package/src/materialization/coordinateBounds.js +204 -0
  330. package/src/materialization/dependencies.js +93 -16
  331. package/src/materialization/encodings.js +1 -1
  332. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  333. package/src/materialization/facetGuides/placement.js +8 -2
  334. package/src/materialization/facetGuides/preparation.js +12 -7
  335. package/src/materialization/facetHeaders.js +346 -0
  336. package/src/materialization/facets.js +23 -66
  337. package/src/materialization/guides/layout.js +57 -0
  338. package/src/materialization/guides/resources.js +28 -2
  339. package/src/materialization/legends.js +11 -2
  340. package/src/materialization/marks/capabilities.js +20 -5
  341. package/src/materialization/marks/grouping.js +52 -0
  342. package/src/materialization/marks/index.js +68 -27
  343. package/src/materialization/marks/policies.js +4 -1
  344. package/src/materialization/rect.js +55 -42
  345. package/src/materialization/rectConfig.js +5 -1
  346. package/src/materialization/rowEncoding.js +13 -13
  347. package/src/materialization/scaleGuideDependencies.js +8 -5
  348. package/src/materialization/scales/map.js +6 -3
  349. package/src/materialization/scales/policies/arc.js +42 -0
  350. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  351. package/src/materialization/scales/policies/offset.js +50 -8
  352. package/src/materialization/scales/policies/series.js +5 -1
  353. package/src/materialization/scales/resolve.js +65 -17
  354. package/src/materialization/selection/compatibility.js +11 -0
  355. package/src/materialization/selection/items/arc.js +28 -4
  356. package/src/materialization/selection/items/bar.js +42 -71
  357. package/src/materialization/selection/items/common.js +27 -6
  358. package/src/materialization/selection/items/path.js +3 -13
  359. package/src/materialization/selection/items/rect.js +35 -8
  360. package/src/materialization/selection/path.js +1 -1
  361. package/src/materialization/selection/point.js +3 -1
  362. package/src/materialization/text.js +324 -14
  363. package/src/mcp/adapter.js +2 -2
  364. package/src/renderers/canvas/circle.js +2 -3
  365. package/src/renderers/canvas/line.js +2 -3
  366. package/src/renderers/canvas/native.js +4 -4
  367. package/src/renderers/canvas/path.js +2 -3
  368. package/src/renderers/canvas/rect.js +2 -3
  369. package/src/renderers/canvas/stroke.js +11 -0
  370. package/src/renderers/svg.js +19 -0
  371. package/src/selectors/datasets.js +44 -0
  372. package/src/selectors/layers.js +7 -2
  373. package/src/selectors/scales.js +6 -0
  374. package/src/theme/defaults.js +140 -0
  375. package/types/basic.d.ts +17 -2
  376. package/types/index.d.ts +211 -0
  377. package/types/program.d.ts +2182 -204
  378. package/src/materialization/bars/grouped.js +0 -144
@@ -1,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 {
@@ -336,10 +428,107 @@ function stableWindowOutput(operation, calculate) {
336
428
  }
337
429
  }
338
430
 
339
- function applyOperation(partition, operation, sortBy) {
431
+ function setWindowOutput(entry, field, value) {
432
+ Object.defineProperty(entry.row, field, {
433
+ value, enumerable: true, writable: true, configurable: true
434
+ });
435
+ }
436
+
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) {
340
529
  if (operation.op === "rowNumber") {
341
530
  partition.forEach((entry, index) => {
342
- entry.row[operation.as] = index + 1;
531
+ setWindowOutput(entry, operation.as, index + 1);
343
532
  });
344
533
  return;
345
534
  }
@@ -349,12 +538,14 @@ function applyOperation(partition, operation, sortBy) {
349
538
  partition.forEach((entry, index) => {
350
539
  if (
351
540
  index > 0 &&
352
- 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)
353
544
  ) {
354
545
  rank = index + 1;
355
546
  denseRank += 1;
356
547
  }
357
- entry.row[operation.as] = operation.op === "rank" ? rank : denseRank;
548
+ setWindowOutput(entry, operation.as, operation.op === "rank" ? rank : denseRank);
358
549
  });
359
550
  return;
360
551
  }
@@ -364,11 +555,16 @@ function applyOperation(partition, operation, sortBy) {
364
555
  stableFinitePrefixSums(values, label)
365
556
  );
366
557
  partition.forEach((entry, index) => {
367
- entry.row[operation.as] = totals[index];
558
+ setWindowOutput(entry, operation.as, totals[index]);
368
559
  });
369
560
  return;
370
561
  }
371
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);
372
568
  const calculate = operation.op === "movingMean"
373
569
  ? stableFiniteMean
374
570
  : stableFiniteSum;
@@ -379,18 +575,18 @@ function applyOperation(partition, operation, sortBy) {
379
575
  index + operation.frame.following
380
576
  );
381
577
  const values = operationValues(partition, operation, first, last);
382
- entry.row[operation.as] = stableWindowOutput(operation, label =>
383
- calculate(values, label)
384
- );
578
+ setWindowOutput(entry, operation.as, values.length < operation.minPeriods
579
+ ? null
580
+ : stableWindowOutput(operation, label => calculate(values, label)));
385
581
  });
386
582
  return;
387
583
  }
388
584
  const direction = operation.op === "lag" ? -1 : 1;
389
585
  partition.forEach((entry, index) => {
390
586
  const peer = partition[index + direction * operation.offset];
391
- entry.row[operation.as] = peer === undefined
587
+ setWindowOutput(entry, operation.as, peer === undefined
392
588
  ? operation.default
393
- : peer.row[operation.field];
589
+ : peer.row[operation.field]);
394
590
  });
395
591
  }
396
592
 
@@ -405,13 +601,28 @@ export function deriveWindowRows(rows, transform) {
405
601
  validateSourceFields(rows, transform);
406
602
  const entries = rows.map((row, index) => ({ index, row: { ...row } }));
407
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
+ );
408
607
  for (const partition of partitions) {
409
608
  validatePartitionSortValues(partition, transform);
410
- 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
+ }
411
622
  }
412
623
  for (const operation of transform.operations) {
413
624
  for (const partition of partitions) {
414
- applyOperation(partition, operation, transform.sortBy);
625
+ applyOperation(partition, operation, transform.sortBy, transform);
415
626
  }
416
627
  }
417
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,
@@ -23,12 +23,78 @@ function resolveColumns(columns, count) {
23
23
  return Math.min(value, count);
24
24
  }
25
25
 
26
+ function resolveGridCoordinates(children, columns) {
27
+ const requested = children.some(child => child.row !== undefined || child.column !== undefined);
28
+ if (!requested) return undefined;
29
+ if (children.some(child => !(
30
+ Number.isInteger(child.row) && child.row >= 0 &&
31
+ Number.isInteger(child.column) && child.column >= 0
32
+ ))) {
33
+ throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");
34
+ }
35
+ const keys = children.map(child => `${child.row}:${child.column}`);
36
+ if (new Set(keys).size !== keys.length) {
37
+ throw new Error("Facet grid children must occupy unique row and column coordinates.");
38
+ }
39
+ const requiredColumns = Math.max(...children.map(child => child.column)) + 1;
40
+ if (columns !== undefined && columns !== requiredColumns) {
41
+ throw new RangeError("Facet grid columns must match the declared column domain.");
42
+ }
43
+ return {
44
+ columns: requiredColumns,
45
+ rows: Math.max(...children.map(child => child.row)) + 1
46
+ };
47
+ }
48
+
26
49
  function alignedOffset(remaining, align) {
27
50
  if (align === "start") return 0;
28
51
  if (align === "end") return remaining;
29
52
  return remaining / 2;
30
53
  }
31
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
+
32
98
  export function resolveFacetLayout({
33
99
  children,
34
100
  columns,
@@ -40,13 +106,16 @@ export function resolveFacetLayout({
40
106
  sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
41
107
  sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
42
108
  sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
43
- sharedLegendPosition = "right"
109
+ sharedLegendPosition = "right",
110
+ headerLayout
44
111
  } = {}) {
45
112
  const values = children?.map(child => child?.value);
113
+ const coordinates = resolveGridCoordinates(children ?? [], columns);
46
114
  const resolvedChildren = normalizeCompositionChildren(children?.map(
47
- ({ value: _value, ...child }) => child
115
+ ({ value: _value, row: _row, column: _column, ...child }) => child
48
116
  ));
49
- const resolvedColumns = resolveColumns(columns, resolvedChildren.length);
117
+ const resolvedColumns = coordinates?.columns ??
118
+ resolveColumns(columns, resolvedChildren.length);
50
119
  const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
51
120
  const resolvedAlign = normalizeCompositionAlign(align);
52
121
  const resolvedPadding = normalizeCompositionPadding(padding);
@@ -79,26 +148,38 @@ export function resolveFacetLayout({
79
148
  const legendLane = sharedLegend
80
149
  ? legendGap + (sideLegend ? legendWidth : legendHeight)
81
150
  : 0;
82
- const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
151
+ const rowCount = coordinates?.rows ??
152
+ Math.ceil(resolvedChildren.length / resolvedColumns);
153
+ const headers = normalizeHeaderLayout(headerLayout, rowCount);
83
154
  const columnWidths = Array(resolvedColumns).fill(-Infinity);
84
155
  const rowHeights = Array(rowCount).fill(-Infinity);
85
156
  resolvedChildren.forEach((child, index) => {
86
- const column = index % resolvedColumns;
87
- const row = Math.floor(index / resolvedColumns);
157
+ const column = coordinates === undefined
158
+ ? index % resolvedColumns
159
+ : children[index].column;
160
+ const row = coordinates === undefined
161
+ ? Math.floor(index / resolvedColumns)
162
+ : children[index].row;
88
163
  columnWidths[column] = Math.max(columnWidths[column], child.width);
89
164
  rowHeights[row] = Math.max(rowHeights[row], child.height);
90
165
  });
91
166
  const columnStarts = columnWidths.map((_, column) =>
92
- resolvedPadding.left + columnWidths.slice(0, column)
167
+ resolvedPadding.left + headers.outer.left + columnWidths.slice(0, column)
93
168
  .reduce((sum, width) => sum + width, 0) + resolvedGap * column
94
169
  );
95
170
  const rowStarts = rowHeights.map((_, row) =>
96
- resolvedTitleHeight + resolvedPadding.top + rowHeights.slice(0, row)
97
- .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]
98
175
  );
99
176
  const placements = resolvedChildren.map((child, index) => {
100
- const column = index % resolvedColumns;
101
- const row = Math.floor(index / resolvedColumns);
177
+ const column = coordinates === undefined
178
+ ? index % resolvedColumns
179
+ : children[index].column;
180
+ const row = coordinates === undefined
181
+ ? Math.floor(index / resolvedColumns)
182
+ : children[index].row;
102
183
  return {
103
184
  id: child.id,
104
185
  value: values[index],
@@ -119,12 +200,18 @@ export function resolveFacetLayout({
119
200
  };
120
201
  });
121
202
  const gridWidth = resolvedPadding.left +
203
+ headers.outer.left +
122
204
  columnWidths.reduce((sum, width) => sum + width, 0) +
123
205
  resolvedGap * Math.max(0, resolvedColumns - 1) +
206
+ headers.outer.right +
124
207
  resolvedPadding.right;
125
208
  const gridHeight = resolvedTitleHeight + resolvedPadding.top +
209
+ headers.outer.top +
126
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) +
127
213
  resolvedGap * Math.max(0, rowCount - 1) +
214
+ headers.outer.bottom +
128
215
  resolvedPadding.bottom;
129
216
  return cloneAndFreeze({
130
217
  columns: resolvedColumns,
@@ -133,6 +220,7 @@ export function resolveFacetLayout({
133
220
  align: resolvedAlign,
134
221
  padding: resolvedPadding,
135
222
  titleHeight: resolvedTitleHeight,
223
+ headerLayout: headers,
136
224
  gridWidth,
137
225
  width: gridWidth + (sideLegend ? legendLane : 0),
138
226
  height: gridHeight + (sideLegend ? 0 : legendLane),
@@ -0,0 +1,12 @@
1
+ export function assertGuideCollisionBlocks(blocks) {
2
+ for (let index = 0; index < blocks.length; index += 1) {
3
+ const a = blocks[index];
4
+ for (const b of blocks.slice(index + 1)) {
5
+ if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
6
+ if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
7
+ a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
8
+ throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
9
+ }
10
+ }
11
+ }
12
+ }