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,6 +1,137 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { findTransformPolicy } from "../transforms.js";
4
+ import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
5
+ import { resolveRectMode } from "../rects.js";
6
+ import { resolveRuleMode } from "../rules.js";
7
+
8
+ const CARTESIAN_MARKS = new Set([
9
+ "point", "line", "area", "bar", "rule", "tick", "rect"
10
+ ]);
11
+ const FACET_BAR_GRAINS = new Set([
12
+ BAR_GRAINS.histogram,
13
+ BAR_GRAINS.aggregate,
14
+ BAR_GRAINS.ranged
15
+ ]);
16
+ const ORDINARY_SCALE_CHANNELS = Object.freeze([
17
+ ["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
18
+ ["theta", "theta"], ["r", "radius"], ["color", "color"],
19
+ ["stroke", "stroke"], ["size", "size"], ["shape", "shape"],
20
+ ["opacity", "opacity"], ["strokeDash", "strokeDash"]
21
+ ]);
22
+
23
+ function coordinateType(semanticSpec, layer) {
24
+ if (layer.coordinate === undefined) return undefined;
25
+ return semanticSpec.coordinates?.find(value => value.id === layer.coordinate)?.type;
26
+ }
27
+
28
+ function classifyPrimaryLayer(semanticSpec, layer) {
29
+ const type = layer.mark?.type;
30
+ const parallel = layer.encoding?.parallel;
31
+ if (parallel !== undefined || coordinateType(semanticSpec, layer) === "parallel") {
32
+ const complete = type === "line" && Array.isArray(parallel?.dimensions) &&
33
+ parallel.dimensions.length >= 2 &&
34
+ parallel.dimensions.every(dimension =>
35
+ typeof dimension?.scale === "string" && dimension.scale.length > 0
36
+ );
37
+ if (!complete) {
38
+ throw new Error(
39
+ `Facet layer "${layer.id}" must be a complete materializable Parallel mark.`
40
+ );
41
+ }
42
+ return "parallel";
43
+ }
44
+ const polar = layer.encoding?.theta !== undefined ||
45
+ layer.encoding?.radius !== undefined || coordinateType(semanticSpec, layer) === "polar";
46
+ if (polar) {
47
+ const theta = layer.encoding?.theta?.scale !== undefined;
48
+ const radius = layer.encoding?.radius?.scale !== undefined;
49
+ const complete = (type === "point" || type === "line")
50
+ ? theta && radius
51
+ : type === "arc" && theta;
52
+ if (!complete) {
53
+ throw new Error(
54
+ `Facet layer "${layer.id}" must be a complete materializable Polar mark.`
55
+ );
56
+ }
57
+ return "polar";
58
+ }
59
+ if (!CARTESIAN_MARKS.has(type)) {
60
+ throw new Error(
61
+ `facet does not support mark "${layer.id}" of type ${type ?? "incomplete"}.`
62
+ );
63
+ }
64
+ if (type === "bar" && !FACET_BAR_GRAINS.has(resolveBarGrain(layer))) {
65
+ throw new Error(
66
+ `facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
67
+ );
68
+ }
69
+ const complete = (
70
+ layer.encoding?.x?.scale !== undefined &&
71
+ layer.encoding?.y?.scale !== undefined
72
+ ) || (type === "rule" && resolveRuleMode(layer) !== undefined) ||
73
+ (type === "rect" && resolveRectMode(layer) !== undefined);
74
+ if (!complete) {
75
+ throw new Error(
76
+ `Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
77
+ );
78
+ }
79
+ return "cartesian";
80
+ }
81
+
82
+ export function collectFacetScaleBindings(semanticSpec) {
83
+ if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers)) {
84
+ throw new TypeError("Facet scale bindings require a semantic spec with layers.");
85
+ }
86
+ const bindings = [];
87
+ for (const layer of semanticSpec.layers) {
88
+ for (const [policyKey, semanticChannel] of ORDINARY_SCALE_CHANNELS) {
89
+ const scaleId = layer.encoding?.[semanticChannel]?.scale;
90
+ if (scaleId === undefined) continue;
91
+ bindings.push({ layerId: layer.id, scaleId, policyKey, semanticChannel });
92
+ }
93
+ for (const [dimensionIndex, dimension] of
94
+ (layer.encoding?.parallel?.dimensions ?? []).entries()) {
95
+ if (dimension?.scale === undefined) continue;
96
+ bindings.push({
97
+ layerId: layer.id,
98
+ scaleId: dimension.scale,
99
+ policyKey: "parallelDimensions",
100
+ semanticChannel: "parallel",
101
+ dimensionField: dimension.field,
102
+ dimensionIndex
103
+ });
104
+ }
105
+ }
106
+ return cloneAndFreeze(bindings);
107
+ }
108
+
109
+ export function resolveFacetFamily(semanticSpec) {
110
+ if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers) ||
111
+ semanticSpec.layers.length === 0) {
112
+ throw new Error("facet requires at least one materializable layer.");
113
+ }
114
+ const primary = semanticSpec.layers.filter(layer =>
115
+ !(layer.mark?.type === "text" && layer.source !== undefined)
116
+ );
117
+ if (primary.length === 0) {
118
+ throw new Error("facet requires at least one primary materializable layer.");
119
+ }
120
+ const families = primary.map(layer => classifyPrimaryLayer(semanticSpec, layer));
121
+ if (new Set(families).size !== 1) {
122
+ throw new Error("facet requires every primary layer to use one coordinate family.");
123
+ }
124
+ const family = families[0];
125
+ return cloneAndFreeze({
126
+ family,
127
+ primaryLayers: primary.map(layer => layer.id),
128
+ dependentLayers: semanticSpec.layers
129
+ .filter(layer => !primary.includes(layer))
130
+ .map(layer => layer.id),
131
+ scaleBindings: collectFacetScaleBindings(semanticSpec),
132
+ coordinates: [...new Set(primary.map(layer => layer.coordinate).filter(Boolean))]
133
+ });
134
+ }
4
135
 
5
136
  function requireCollection(value, label) {
6
137
  if (!Array.isArray(value) || value.length === 0) {
@@ -172,6 +303,7 @@ export function planFacetDependencies(semanticSpec, options = {}) {
172
303
  field,
173
304
  anchor,
174
305
  replay,
175
- layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data }))
306
+ layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data })),
307
+ ...(options.family === undefined ? {} : options.family)
176
308
  });
177
309
  }
@@ -3,8 +3,11 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
3
3
  const LEGEND_FAMILIES = Object.freeze({
4
4
  series: "categorical",
5
5
  color: "categorical",
6
+ stroke: "categorical",
6
7
  gradient: "gradient",
7
8
  interval: "discretized",
9
+ strokeGradient: "gradient",
10
+ strokeInterval: "discretized",
8
11
  size: "size",
9
12
  opacity: "opacity"
10
13
  });
@@ -4,61 +4,17 @@ import {
4
4
  validateGeneratedItemLimit,
5
5
  validateWorkLimit
6
6
  } from "../../core/validation.js";
7
- import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
8
- import { planFacetDependencies } from "./dependencies.js";
7
+ import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
9
8
  import { readNominalField } from "../scales/index.js";
10
-
11
- const SUPPORTED_MARKS = new Set([
12
- "point", "line", "area", "bar", "rule", "tick", "rect"
13
- ]);
14
- const SUPPORTED_BAR_GRAINS = new Set([
15
- BAR_GRAINS.histogram,
16
- BAR_GRAINS.aggregate,
17
- BAR_GRAINS.ranged
18
- ]);
19
9
  const MAX_FACET_CHILDREN = 100;
20
10
 
21
- function requireFacetField(field) {
11
+ function requireFacetField(field, label = "facet") {
22
12
  if (typeof field !== "string" || field.length === 0) {
23
- throw new TypeError("facet requires a non-empty field.");
13
+ throw new TypeError(`${label} requires a non-empty field.`);
24
14
  }
25
15
  return field;
26
16
  }
27
17
 
28
- function requireSupportedLayer(layer) {
29
- if (!SUPPORTED_MARKS.has(layer.mark?.type)) {
30
- throw new Error(
31
- `facet does not support mark "${layer.id}" of type ${layer.mark?.type ?? "incomplete"}.`
32
- );
33
- }
34
- if (
35
- layer.mark.type === "bar" &&
36
- !SUPPORTED_BAR_GRAINS.has(resolveBarGrain(layer))
37
- ) {
38
- throw new Error(
39
- `facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
40
- );
41
- }
42
- if (
43
- layer.encoding?.x?.scale === undefined ||
44
- layer.encoding?.y?.scale === undefined
45
- ) {
46
- throw new Error(
47
- `Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
48
- );
49
- }
50
- if (typeof layer.data !== "string" || layer.data.length === 0) {
51
- throw new Error(`Facet layer "${layer.id}" requires a dataset.`);
52
- }
53
- }
54
-
55
- function requireSupportedLayers(semanticSpec) {
56
- if (!Array.isArray(semanticSpec.layers) || semanticSpec.layers.length === 0) {
57
- throw new Error("facet requires at least one materializable layer.");
58
- }
59
- for (const layer of semanticSpec.layers) requireSupportedLayer(layer);
60
- }
61
-
62
18
  function requirePartitionDataset(semanticSpec, id) {
63
19
  const dataset = semanticSpec.datasets?.find(candidate => candidate.id === id);
64
20
  if (dataset === undefined) {
@@ -74,25 +30,52 @@ function uniqueInOrder(values) {
74
30
  return [...new Set(values)];
75
31
  }
76
32
 
77
- function resolveValues(observed, requested) {
33
+ function resolveValues(observed, requested, label = "facet values") {
78
34
  if (requested === undefined) return observed;
79
35
  if (!Array.isArray(requested) || requested.length === 0) {
80
- throw new TypeError("facet values must be a non-empty array when provided.");
36
+ throw new TypeError(`${label} must be a non-empty array when provided.`);
81
37
  }
82
38
  const normalized = readNominalField(
83
39
  requested.map(value => ({ value })),
84
40
  "value"
85
41
  );
86
42
  if (new Set(normalized).size !== normalized.length) {
87
- throw new Error("facet values must be unique.");
43
+ throw new Error(`${label} must be unique.`);
88
44
  }
89
45
  const missing = normalized.find(value => !observed.includes(value));
90
46
  if (missing !== undefined) {
91
- throw new Error(`facet value ${JSON.stringify(missing)} is not present in the source field.`);
47
+ throw new Error(
48
+ `${label} value ${JSON.stringify(missing)} is not present in the source field.`
49
+ );
92
50
  }
93
51
  return normalized;
94
52
  }
95
53
 
54
+ function requireGridRole(value, label) {
55
+ if (!isPlainObject(value)) {
56
+ throw new TypeError(`facetGrid ${label} must be a plain object.`);
57
+ }
58
+ const unknown = Object.keys(value).find(key => !["field", "values"].includes(key));
59
+ if (unknown !== undefined) {
60
+ throw new Error(`Unknown facetGrid ${label} option "${unknown}".`);
61
+ }
62
+ return {
63
+ field: requireFacetField(value.field, `facetGrid ${label}`),
64
+ ...(value.values === undefined ? {} : { values: value.values })
65
+ };
66
+ }
67
+
68
+ function pairKey(row, column) {
69
+ return JSON.stringify([
70
+ [typeof row, row],
71
+ [typeof column, column]
72
+ ]);
73
+ }
74
+
75
+ function displayValue(value) {
76
+ return value === "" ? "(empty)" : String(value);
77
+ }
78
+
96
79
  export function resolveFacetDefinition(semanticSpec, options = {}) {
97
80
  if (!isPlainObject(semanticSpec)) {
98
81
  throw new TypeError("resolveFacetDefinition requires a semantic spec.");
@@ -102,9 +85,10 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
102
85
  }
103
86
  const field = requireFacetField(options.field);
104
87
  const id = validateUserId(options.id ?? "facet", "Facet id");
105
- requireSupportedLayers(semanticSpec);
88
+ const family = resolveFacetFamily(semanticSpec);
106
89
  const dependencies = planFacetDependencies(semanticSpec, {
107
90
  field,
91
+ family,
108
92
  ...(options.data === undefined ? {} : { data: options.data })
109
93
  });
110
94
  const data = dependencies.anchor;
@@ -125,6 +109,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
125
109
  );
126
110
  return cloneAndFreeze({
127
111
  id,
112
+ family: family.family,
128
113
  data,
129
114
  field,
130
115
  values,
@@ -136,3 +121,89 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
136
121
  }))
137
122
  });
138
123
  }
124
+
125
+ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
126
+ if (!isPlainObject(semanticSpec)) {
127
+ throw new TypeError("resolveFacetGridDefinition requires a semantic spec.");
128
+ }
129
+ if (!isPlainObject(options)) {
130
+ throw new TypeError("facetGrid options must be a plain object.");
131
+ }
132
+ const rows = requireGridRole(options.rows, "rows");
133
+ const columns = requireGridRole(options.columns, "columns");
134
+ if (rows.field === columns.field) {
135
+ throw new Error("facetGrid rows.field and columns.field must be different.");
136
+ }
137
+ const combinations = options.combinations ?? "observed";
138
+ if (!["observed", "full"].includes(combinations)) {
139
+ throw new Error('facetGrid combinations must be "observed" or "full".');
140
+ }
141
+ const id = validateUserId(options.id ?? "facetGrid", "Facet grid id");
142
+ const family = resolveFacetFamily(semanticSpec);
143
+ const dependencies = planFacetDependencies(semanticSpec, {
144
+ field: rows.field,
145
+ family,
146
+ ...(options.data === undefined ? {} : { data: options.data })
147
+ });
148
+ const data = dependencies.anchor;
149
+ const dataset = requirePartitionDataset(semanticSpec, data);
150
+ const observedRows = readNominalField(dataset.values, rows.field);
151
+ const observedColumns = readNominalField(dataset.values, columns.field);
152
+ const rowValues = resolveValues(
153
+ uniqueInOrder(observedRows), rows.values, "facetGrid rows.values"
154
+ );
155
+ const columnValues = resolveValues(
156
+ uniqueInOrder(observedColumns), columns.values, "facetGrid columns.values"
157
+ );
158
+ const observedPairs = new Set(dataset.values.map((_, index) =>
159
+ pairKey(observedRows[index], observedColumns[index])
160
+ ));
161
+ const candidates = rowValues.flatMap((rowValue, row) =>
162
+ columnValues.map((columnValue, column) => ({
163
+ row,
164
+ column,
165
+ rowValue,
166
+ columnValue,
167
+ empty: !observedPairs.has(pairKey(rowValue, columnValue))
168
+ }))
169
+ );
170
+ const selected = combinations === "full"
171
+ ? candidates
172
+ : candidates.filter(cell => !cell.empty);
173
+ if (selected.length === 0) {
174
+ throw new Error("facetGrid has no observed row and column combinations.");
175
+ }
176
+ validateGeneratedItemLimit(
177
+ selected.length,
178
+ "Facet grid child count",
179
+ MAX_FACET_CHILDREN
180
+ );
181
+ validateWorkLimit(
182
+ dataset.values.length * selected.length,
183
+ "Facet grid partition work"
184
+ );
185
+ return cloneAndFreeze({
186
+ id,
187
+ family: family.family,
188
+ data,
189
+ dependencies,
190
+ grid: {
191
+ rows: { field: rows.field, values: rowValues },
192
+ columns: { field: columns.field, values: columnValues },
193
+ combinations
194
+ },
195
+ cells: selected.map(cell => {
196
+ const cellId = `${id}-row-${cell.row + 1}-column-${cell.column + 1}`;
197
+ return {
198
+ ...cell,
199
+ id: cellId,
200
+ data: `${cellId}-data`,
201
+ value: `${displayValue(cell.rowValue)} · ${displayValue(cell.columnValue)}`,
202
+ filters: [
203
+ { field: rows.field, value: cell.rowValue },
204
+ { field: columns.field, value: cell.columnValue }
205
+ ]
206
+ };
207
+ })
208
+ });
209
+ }
@@ -3,6 +3,7 @@ import {
3
3
  FACET_SCALE_CHANNELS,
4
4
  FACET_SCALE_RESOLUTIONS
5
5
  } from "../../core/vocabulary.js";
6
+ import { collectFacetScaleBindings } from "./dependencies.js";
6
7
  import { resolveHistogramBins } from "../histogram.js";
7
8
  import {
8
9
  hasOrdinalDomain,
@@ -63,30 +64,25 @@ function normalizeRequestedPolicies(requested) {
63
64
 
64
65
  function usedScaleChannels(semanticSpec, scales) {
65
66
  const used = new Map();
66
- for (const layer of semanticSpec.layers) {
67
- for (const channel of FACET_SCALE_CHANNELS) {
68
- const id = layer.encoding?.[channel]?.scale;
69
- if (id === undefined) continue;
70
- if (!scales.has(id)) {
71
- throw new Error(
72
- `Facet layer "${layer.id}" references missing scale "${id}".`
73
- );
74
- }
75
- if (!used.has(id)) used.set(id, new Set());
76
- used.get(id).add(channel);
67
+ const bindings = collectFacetScaleBindings(semanticSpec);
68
+ for (const binding of bindings) {
69
+ if (!scales.has(binding.scaleId)) {
70
+ throw new Error(
71
+ `Facet layer "${binding.layerId}" references missing scale "${binding.scaleId}".`
72
+ );
77
73
  }
74
+ if (!used.has(binding.scaleId)) used.set(binding.scaleId, new Set());
75
+ used.get(binding.scaleId).add(binding.policyKey);
78
76
  }
79
- return used;
77
+ return { used, bindings };
80
78
  }
81
79
 
82
80
  export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
83
81
  const scales = requireScaleDefinitions(semanticSpec);
84
82
  const channels = normalizeRequestedPolicies(requested);
85
- const used = usedScaleChannels(semanticSpec, scales);
83
+ const { used, bindings } = usedScaleChannels(semanticSpec, scales);
86
84
  for (const channel of Object.keys(requested)) {
87
- const applicable = semanticSpec.layers.some(
88
- layer => layer.encoding?.[channel]?.scale !== undefined
89
- );
85
+ const applicable = bindings.some(binding => binding.policyKey === channel);
90
86
  if (!applicable) {
91
87
  throw new Error(
92
88
  `Facet scale channel "${channel}" is not used by an affected layer.`
@@ -107,7 +103,7 @@ export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
107
103
  channels: attached
108
104
  };
109
105
  }
110
- return cloneAndFreeze({ channels, scales: scalePolicies });
106
+ return cloneAndFreeze({ channels, scales: scalePolicies, bindings });
111
107
  }
112
108
 
113
109
  function requireChildDomains(resolvedByChild, scaleId) {
@@ -210,7 +206,8 @@ export function resolveFacetScaleDomains(
210
206
  }
211
207
  return cloneAndFreeze({
212
208
  channels: normalized.channels,
213
- scales: resolved
209
+ scales: resolved,
210
+ bindings: normalized.bindings
214
211
  });
215
212
  }
216
213
 
@@ -0,0 +1,123 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
4
+ const AS_KEYS = Object.freeze(["key", "value"]);
5
+ const MAX_FIELDS = 64;
6
+ const MAX_OUTPUT_ROWS = 10_000;
7
+
8
+ function rejectUnknownKeys(value, supported, label) {
9
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
10
+ if (unknown !== undefined) {
11
+ throw new Error(`Unknown ${label} property "${unknown}".`);
12
+ }
13
+ }
14
+
15
+ function requireField(value, label) {
16
+ if (typeof value !== "string" || value.length === 0) {
17
+ throw new TypeError(`${label} must be a non-empty string.`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function normalizeAs(value) {
23
+ const requested = value ?? {};
24
+ if (!isPlainObject(requested)) return requested;
25
+ return {
26
+ ...requested,
27
+ key: requested.key ?? "key",
28
+ value: requested.value ?? "value"
29
+ };
30
+ }
31
+
32
+ export function normalizeFoldTransform({ fields, as } = {}) {
33
+ const transform = {
34
+ type: "fold",
35
+ fields: Array.isArray(fields) ? [...fields] : fields,
36
+ as: normalizeAs(as)
37
+ };
38
+ validateFoldTransform(transform);
39
+ return cloneAndFreeze(transform);
40
+ }
41
+
42
+ export function validateFoldTransform(transform) {
43
+ if (!isPlainObject(transform)) {
44
+ throw new TypeError("Fold transform must be a plain object.");
45
+ }
46
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "fold transform");
47
+ if (transform.type !== "fold") {
48
+ throw new Error(`Unsupported fold transform "${transform.type}".`);
49
+ }
50
+ if (!Array.isArray(transform.fields) || transform.fields.length === 0) {
51
+ throw new TypeError("Fold fields must be a non-empty array.");
52
+ }
53
+ if (transform.fields.length > MAX_FIELDS) {
54
+ throw new RangeError(`Fold fields cannot contain more than ${MAX_FIELDS} entries.`);
55
+ }
56
+ transform.fields.forEach((field, index) => {
57
+ requireField(field, `Fold fields[${index}]`);
58
+ });
59
+ if (new Set(transform.fields).size !== transform.fields.length) {
60
+ throw new Error("Fold fields must be unique.");
61
+ }
62
+ if (!isPlainObject(transform.as)) {
63
+ throw new TypeError("Fold as must be a plain object.");
64
+ }
65
+ rejectUnknownKeys(transform.as, AS_KEYS, "fold as");
66
+ const key = requireField(transform.as.key, "Fold as.key");
67
+ const value = requireField(transform.as.value, "Fold as.value");
68
+ if (key === value) {
69
+ throw new Error("Fold output fields must be unique.");
70
+ }
71
+ return transform;
72
+ }
73
+
74
+ function valueType(value, field, row) {
75
+ if (value === null || value === undefined) {
76
+ throw new TypeError(`Fold field "${field}" is missing at row ${row}.`);
77
+ }
78
+ if (typeof value === "number") {
79
+ if (!Number.isFinite(value)) {
80
+ throw new TypeError(`Fold field "${field}" must contain finite numbers.`);
81
+ }
82
+ return "number";
83
+ }
84
+ if (typeof value === "string" || typeof value === "boolean") {
85
+ return typeof value;
86
+ }
87
+ throw new TypeError(
88
+ `Fold field "${field}" must contain finite numbers, strings, or booleans.`
89
+ );
90
+ }
91
+
92
+ export function deriveFoldRows(rows, transform) {
93
+ validateFoldTransform(transform);
94
+ if (rows.length * transform.fields.length > MAX_OUTPUT_ROWS) {
95
+ throw new RangeError(`Fold output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
96
+ }
97
+ const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
98
+ for (const output of [transform.as.key, transform.as.value]) {
99
+ if (sourceFields.has(output)) {
100
+ throw new Error(`Fold output field "${output}" already exists.`);
101
+ }
102
+ }
103
+ let expectedType;
104
+ const output = [];
105
+ rows.forEach((row, rowIndex) => {
106
+ for (const field of transform.fields) {
107
+ if (!Object.hasOwn(row, field)) {
108
+ throw new Error(`Fold source does not contain field "${field}" at row ${rowIndex}.`);
109
+ }
110
+ const type = valueType(row[field], field, rowIndex);
111
+ if (expectedType === undefined) expectedType = type;
112
+ else if (type !== expectedType) {
113
+ throw new TypeError("Fold fields must contain one common primitive type.");
114
+ }
115
+ output.push({
116
+ ...row,
117
+ [transform.as.key]: field,
118
+ [transform.as.value]: row[field]
119
+ });
120
+ }
121
+ });
122
+ return output;
123
+ }
@@ -8,14 +8,15 @@ import { interpolateNumber, inverseLerp } from "./numeric.js";
8
8
  import { requireFiniteResult } from "./numeric.js";
9
9
  import {
10
10
  isNominalValue,
11
- normalizeTemporalValue
11
+ normalizeTemporalValue,
12
+ resolveTemporalUnit
12
13
  } from "./scales/index.js";
13
14
 
14
15
  export const HORIZON_RESOLUTIONS = Object.freeze(["shared", "independent"]);
15
16
  export const HORIZON_MISSING_POLICIES = Object.freeze(["break", "error"]);
16
17
  export const HORIZON_OVERFLOW_POLICIES = Object.freeze(["clip", "error"]);
17
18
 
18
- const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType"]);
19
+ const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType", "temporalUnit"]);
19
20
  const OUTPUT_PROPERTIES = Object.freeze([
20
21
  "x", "lower", "upper", "group", "color", "sign", "band", "segment"
21
22
  ]);
@@ -41,7 +42,8 @@ function validateEncoding(value, role) {
41
42
  `Horizon ${role} fieldType must be quantitative${supportsTemporal ? " or temporal" : ""}.`
42
43
  );
43
44
  }
44
- return { field, fieldType };
45
+ const temporalUnit = resolveTemporalUnit(value, fieldType);
46
+ return { field, fieldType, ...(temporalUnit === undefined ? {} : { temporalUnit }) };
45
47
  }
46
48
 
47
49
  function validateOutputFields(value) {
@@ -183,7 +185,7 @@ export function validateHorizonTransform(transform) {
183
185
 
184
186
  function orderedX(value, encoding, rowIndex) {
185
187
  if (encoding.fieldType === "temporal") {
186
- return normalizeTemporalValue(value, encoding.field, rowIndex);
188
+ return normalizeTemporalValue(value, encoding.field, rowIndex, encoding.temporalUnit);
187
189
  }
188
190
  if (!Number.isFinite(value)) {
189
191
  throw new TypeError(