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
@@ -0,0 +1,180 @@
1
+ import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ requestedStrokeDetails,
4
+ STROKE_STYLE_PROPERTIES
5
+ } from "./strokeStyle.js";
6
+
7
+ export const ROUNDED_RECT_K = 4 * (Math.sqrt(2) - 1) / 3;
8
+ export const RECT_STYLE_PROPERTIES = Object.freeze([
9
+ "cornerRadius",
10
+ ...STROKE_STYLE_PROPERTIES
11
+ ]);
12
+
13
+ function requireFinite(value, property) {
14
+ if (!Number.isFinite(value)) {
15
+ throw new TypeError(`Rounded rect ${property} must be a finite number.`);
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export function validateCornerRadius(value, label = "Rect style") {
21
+ if (!Number.isFinite(value)) {
22
+ throw new TypeError(`${label} cornerRadius must be a finite number.`);
23
+ }
24
+ if (value < 0) {
25
+ throw new RangeError(`${label} cornerRadius must not be negative.`);
26
+ }
27
+ return value;
28
+ }
29
+
30
+ export function requestedRectStyleDetails(request = {}, label = "Rect style") {
31
+ if (!isPlainObject(request)) {
32
+ throw new TypeError(`${label} details must be a plain object.`);
33
+ }
34
+ return freezeOwned({
35
+ ...requestedStrokeDetails(request, label),
36
+ ...(Object.hasOwn(request, "cornerRadius")
37
+ ? { cornerRadius: validateCornerRadius(request.cornerRadius, label) }
38
+ : {})
39
+ });
40
+ }
41
+
42
+ export function normalizeRectGeometry({ x, y, width, height }) {
43
+ x = requireFinite(x, "x");
44
+ y = requireFinite(y, "y");
45
+ width = requireFinite(width, "width");
46
+ height = requireFinite(height, "height");
47
+ if (width < 0) {
48
+ x += width;
49
+ width = -width;
50
+ }
51
+ if (height < 0) {
52
+ y += height;
53
+ height = -height;
54
+ }
55
+ return freezeOwned({ x, y, width, height });
56
+ }
57
+
58
+ export function resolveRoundedRectRadius(width, height, requestedRadius) {
59
+ requireFinite(width, "width");
60
+ requireFinite(height, "height");
61
+ validateCornerRadius(requestedRadius, "Rounded rect");
62
+ if (width < 0 || height < 0) {
63
+ throw new RangeError("Rounded rect width and height must not be negative.");
64
+ }
65
+ return Math.min(requestedRadius, width / 2, height / 2);
66
+ }
67
+
68
+ export function roundedRectCommands({ x, y, width, height, radius }) {
69
+ const geometry = normalizeRectGeometry({ x, y, width, height });
70
+ const r = resolveRoundedRectRadius(
71
+ geometry.width,
72
+ geometry.height,
73
+ radius
74
+ );
75
+ if (!(r > 0)) {
76
+ throw new RangeError("Rounded rect commands require a positive resolved radius.");
77
+ }
78
+ const right = geometry.x + geometry.width;
79
+ const bottom = geometry.y + geometry.height;
80
+ const c = ROUNDED_RECT_K * r;
81
+ return freezeOwned([
82
+ { op: "M", x: geometry.x + r, y: geometry.y },
83
+ { op: "L", x: right - r, y: geometry.y },
84
+ {
85
+ op: "C",
86
+ x1: right - r + c,
87
+ y1: geometry.y,
88
+ x2: right,
89
+ y2: geometry.y + r - c,
90
+ x: right,
91
+ y: geometry.y + r
92
+ },
93
+ { op: "L", x: right, y: bottom - r },
94
+ {
95
+ op: "C",
96
+ x1: right,
97
+ y1: bottom - r + c,
98
+ x2: right - r + c,
99
+ y2: bottom,
100
+ x: right - r,
101
+ y: bottom
102
+ },
103
+ { op: "L", x: geometry.x + r, y: bottom },
104
+ {
105
+ op: "C",
106
+ x1: geometry.x + r - c,
107
+ y1: bottom,
108
+ x2: geometry.x,
109
+ y2: bottom - r + c,
110
+ x: geometry.x,
111
+ y: bottom - r
112
+ },
113
+ { op: "L", x: geometry.x, y: geometry.y + r },
114
+ {
115
+ op: "C",
116
+ x1: geometry.x,
117
+ y1: geometry.y + r - c,
118
+ x2: geometry.x + r - c,
119
+ y2: geometry.y,
120
+ x: geometry.x + r,
121
+ y: geometry.y
122
+ },
123
+ { op: "Z" }
124
+ ].map(command => freezeOwned(command)));
125
+ }
126
+
127
+ export function materializeRectItem(properties, requestedRadius = 0) {
128
+ if (!isPlainObject(properties)) {
129
+ throw new TypeError("Rounded rect item properties must be a plain object.");
130
+ }
131
+ const geometry = normalizeRectGeometry(properties);
132
+ const radius = resolveRoundedRectRadius(
133
+ geometry.width,
134
+ geometry.height,
135
+ requestedRadius
136
+ );
137
+ const appearance = Object.fromEntries(Object.entries(properties).filter(
138
+ ([key]) => !["x", "y", "width", "height"].includes(key)
139
+ ));
140
+ return radius === 0
141
+ ? freezeOwned({
142
+ type: "rect",
143
+ properties: freezeOwned({ ...geometry, ...appearance })
144
+ })
145
+ : freezeOwned({
146
+ type: "path",
147
+ properties: freezeOwned({
148
+ commands: roundedRectCommands({ ...geometry, radius }),
149
+ ...appearance
150
+ })
151
+ });
152
+ }
153
+
154
+ export function readRectItemGeometry(item) {
155
+ if (!isPlainObject(item) || !isPlainObject(item.properties)) return undefined;
156
+ if (item.type === "rect") {
157
+ try {
158
+ return normalizeRectGeometry(item.properties);
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+ const commands = item.type === "path" ? item.properties.commands : undefined;
164
+ if (
165
+ !Array.isArray(commands) ||
166
+ commands.length !== 10 ||
167
+ commands.map(command => command?.op).join("") !== "MLCLCLCLCZ"
168
+ ) return undefined;
169
+ const geometry = {
170
+ x: commands[7].x,
171
+ y: commands[0].y,
172
+ width: commands[2].x - commands[7].x,
173
+ height: commands[4].y - commands[0].y
174
+ };
175
+ try {
176
+ return normalizeRectGeometry(geometry);
177
+ } catch {
178
+ return undefined;
179
+ }
180
+ }
@@ -1,11 +1,9 @@
1
+ import { normalizePositionDatum } from "./positionDatum.js";
1
2
  import { cloneAndFreeze } from "../core/immutable.js";
2
3
  import {
3
- isNominalValue,
4
- normalizeTemporalValue,
5
4
  readNominalField,
6
5
  readQuantitativeField,
7
- readTemporalField,
8
- validateSemanticFieldType
6
+ readTemporalField
9
7
  } from "./scales/index.js";
10
8
 
11
9
  const ENDPOINT_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
@@ -13,6 +11,7 @@ const VALUE_CHANNELS = Object.freeze([
13
11
  ...ENDPOINT_CHANNELS,
14
12
  "xOffset",
15
13
  "yOffset",
14
+ "stroke",
16
15
  "strokeDash",
17
16
  "strokeWidth",
18
17
  "opacity"
@@ -39,26 +38,13 @@ function readField(rows, encoding) {
39
38
  return readNominalField(rows, encoding.field);
40
39
  }
41
40
  if (encoding.fieldType === "temporal") {
42
- return readTemporalField(rows, encoding.field);
41
+ return readTemporalField(rows, encoding.field, encoding.temporalUnit);
43
42
  }
44
43
  return readQuantitativeField(rows, encoding.field);
45
44
  }
46
45
 
47
- export function normalizeRuleDatum(value, fieldType, channel) {
48
- validateSemanticFieldType(fieldType);
49
- if (fieldType === "quantitative") {
50
- if (!Number.isFinite(value)) {
51
- throw new TypeError(`Rule ${channel} datum must be a finite number.`);
52
- }
53
- return value;
54
- }
55
- if (fieldType === "temporal") {
56
- return normalizeTemporalValue(value, `${channel} datum`, 0);
57
- }
58
- if (!isNominalValue(value)) {
59
- throw new TypeError(`Rule ${channel} datum must be a nominal value.`);
60
- }
61
- return value;
46
+ export function normalizeRuleDatum(value, fieldType, channel, temporalUnit) {
47
+ return normalizePositionDatum(value, fieldType, channel, temporalUnit, "Rule");
62
48
  }
63
49
 
64
50
  export function deriveRuleValues(rows, layer) {
@@ -83,7 +69,8 @@ export function deriveRuleValues(rows, layer) {
83
69
  const datum = normalizeRuleDatum(
84
70
  encoding.datum,
85
71
  encoding.fieldType,
86
- channel
72
+ channel,
73
+ encoding.temporalUnit
87
74
  );
88
75
  values[channel] = Array.from({ length }, () => datum);
89
76
  }
@@ -325,6 +325,23 @@ export function interpolateColorStops(stops, position, interpolation = "rgb") {
325
325
  return interpolateColors(colors, position, method, true);
326
326
  }
327
327
 
328
+ export function validateSequentialMidpoint(value, type, domain = "auto") {
329
+ if (value === undefined || value === "auto") return undefined;
330
+ if (type !== "sequential") {
331
+ throw new Error(`Scale type "${type}" does not support midpoint.`);
332
+ }
333
+ if (!Number.isFinite(value)) {
334
+ throw new TypeError("Scale midpoint must be finite or auto.");
335
+ }
336
+ if (domain !== "auto" && (
337
+ !Array.isArray(domain) || domain.length !== 2 || !domain.every(Number.isFinite) ||
338
+ !(value > Math.min(...domain) && value < Math.max(...domain))
339
+ )) {
340
+ throw new RangeError("Scale midpoint must lie strictly inside its resolved domain.");
341
+ }
342
+ return value;
343
+ }
344
+
328
345
  export function mapSequentialColors(
329
346
  values,
330
347
  domain,
@@ -342,6 +359,7 @@ export function mapSequentialColors(
342
359
  if (typeof shouldClamp !== "boolean") {
343
360
  throw new TypeError("Sequential color clamp must be a boolean.");
344
361
  }
362
+ const midpoint = validateSequentialMidpoint(options.midpoint, "sequential", domain);
345
363
  const [start, end] = domain;
346
364
  const constant = start === end;
347
365
  const method = validateContinuousColorInterpolation(interpolation);
@@ -354,7 +372,11 @@ export function mapSequentialColors(
354
372
  if (hasUnknown) return options.unknown;
355
373
  throw new TypeError("Sequential color values must be finite numbers.");
356
374
  }
357
- const raw = constant ? 0.5 : inverseLerp(value, start, end);
375
+ const raw = midpoint === undefined
376
+ ? constant ? 0.5 : inverseLerp(value, start, end)
377
+ : (end > start ? value <= midpoint : value >= midpoint)
378
+ ? inverseLerp(value, start, midpoint) / 2
379
+ : 0.5 + inverseLerp(value, midpoint, end) / 2;
358
380
  return interpolateColors(colors, raw, method, shouldClamp);
359
381
  }));
360
382
  }
@@ -0,0 +1,57 @@
1
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../bars/policy.js";
2
+ import { validateAggregate, validateAggregateFieldType } from "../aggregate.js";
3
+ import { isDiscretizedColorScaleType } from "./types.js";
4
+
5
+ export function validateContinuousColorConsumer(layer, encoding, scale, {
6
+ inferAggregate = false,
7
+ channel = "color"
8
+ } = {}) {
9
+ const kind = layer.mark?.type;
10
+ if (channel === "stroke") {
11
+ if (!["point", "line", "area", "bar", "rect", "arc", "rule", "tick"].includes(kind)) {
12
+ throw new Error("Continuous stroke requires a supported graphical mark consumer.");
13
+ }
14
+ if (!['quantitative', 'temporal'].includes(encoding.fieldType) ||
15
+ (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
16
+ throw new Error(`Scale type "${scale.type}" has an incompatible stroke field type.`);
17
+ }
18
+ if (encoding.aggregate !== undefined) {
19
+ throw new Error("Continuous stroke does not support aggregate.");
20
+ }
21
+ if (Object.hasOwn(scale, "unknown") && kind !== "point") {
22
+ throw new Error("Continuous stroke scale unknown currently requires a row-owned point mark.");
23
+ }
24
+ return undefined;
25
+ }
26
+ if (!["point", "bar", "rect"].includes(kind)) {
27
+ throw new Error("Continuous color requires a Point, aggregate Bar, or Rect consumer.");
28
+ }
29
+ if (!["quantitative", "temporal"].includes(encoding.fieldType) ||
30
+ (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
31
+ throw new Error(`Scale type "${scale.type}" has an incompatible color field type.`);
32
+ }
33
+ if (Object.hasOwn(scale, "unknown") && kind !== "point") {
34
+ throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");
35
+ }
36
+ if (kind !== "bar") {
37
+ if (encoding.aggregate !== undefined) throw new Error(`${kind} continuous color does not support aggregate.`);
38
+ return undefined;
39
+ }
40
+ if (["nominal", "ordinal"].includes(layer.encoding?.color?.fieldType)) {
41
+ throw new Error("Continuous bar color cannot replace an existing nominal color layout.");
42
+ }
43
+ if (encoding.fieldType !== "quantitative") {
44
+ throw new Error("Aggregate bar color currently requires a quantitative field.");
45
+ }
46
+ if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
47
+ throw new Error("Continuous bar color requires a complete categorical aggregate bar.");
48
+ }
49
+ const measure = layer.encoding?.[resolveBarChannels(layer).measure];
50
+ const aggregate = encoding.aggregate ?? (inferAggregate && measure?.field === encoding.field ? measure.aggregate : undefined);
51
+ if (aggregate === undefined) {
52
+ throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");
53
+ }
54
+ validateAggregate(aggregate);
55
+ validateAggregateFieldType(aggregate, encoding.fieldType);
56
+ return aggregate;
57
+ }
@@ -94,10 +94,10 @@ export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
94
94
  return resolved;
95
95
  }
96
96
 
97
- export function resolveScaleRange(range, channel, bounds) {
97
+ export function resolveScaleRange(range, channel, bounds, polarFrame) {
98
98
  validatePositionChannel(channel);
99
99
  if (POLAR_POSITION_CHANNELS.includes(channel)) {
100
- return resolvePolarScaleRange(range, channel, bounds);
100
+ return resolvePolarScaleRange(range, channel, bounds, polarFrame);
101
101
  }
102
102
  const validated = validateScaleRange(range);
103
103
  if (validated !== "auto") return validated;
@@ -1,4 +1,5 @@
1
- import { validateContinuousColorInterpolation } from "./color.js";
1
+ import { validateMeasuredRadiusScale } from "./radial.js";
2
+ import { validateContinuousColorInterpolation, validateSequentialMidpoint } from "./color.js";
2
3
  import { isTransformedScaleType } from "./mapping.js";
3
4
  import {
4
5
  normalizeTransformParameters,
@@ -23,13 +24,35 @@ function retainedValue(previous, patch, defaults, property, typeChanged) {
23
24
  }
24
25
 
25
26
  function validateBandParameters(definition, previous, patch, defaults, typeChanged) {
27
+ const hasPadding = Object.hasOwn(patch, "padding");
28
+ if (
29
+ hasPadding &&
30
+ (Object.hasOwn(patch, "paddingInner") || Object.hasOwn(patch, "paddingOuter"))
31
+ ) {
32
+ throw new Error(
33
+ "Scale padding cannot be combined with paddingInner or paddingOuter."
34
+ );
35
+ }
36
+ if (
37
+ hasPadding &&
38
+ (!Number.isFinite(patch.padding) || patch.padding < 0 || patch.padding >= 1)
39
+ ) {
40
+ throw new RangeError(
41
+ "Scale padding must be from 0 (inclusive) to 1 (exclusive)."
42
+ );
43
+ }
44
+ const normalizedPatch = hasPadding
45
+ ? { ...patch, paddingInner: patch.padding, paddingOuter: patch.padding }
46
+ : patch;
26
47
  const paddingInner = retainedValue(
27
- previous, patch, defaults, "paddingInner", typeChanged
48
+ previous, normalizedPatch, defaults, "paddingInner", typeChanged
28
49
  ) ?? 0;
29
50
  const paddingOuter = retainedValue(
30
- previous, patch, defaults, "paddingOuter", typeChanged
51
+ previous, normalizedPatch, defaults, "paddingOuter", typeChanged
31
52
  ) ?? 0;
32
- const align = retainedValue(previous, patch, defaults, "align", typeChanged) ?? 0.5;
53
+ const align = retainedValue(
54
+ previous, normalizedPatch, defaults, "align", typeChanged
55
+ ) ?? 0.5;
33
56
  if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
34
57
  throw new RangeError(
35
58
  "Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
@@ -65,6 +88,7 @@ export function normalizeScaleDefinition({
65
88
  defaults = {},
66
89
  retainCoreOnTypeChange = false,
67
90
  retainCompatibleOnTypeChange = false,
91
+ allowOrdinalBandParameters = false,
68
92
  validateDomain,
69
93
  validateRange
70
94
  }) {
@@ -137,7 +161,12 @@ export function normalizeScaleDefinition({
137
161
  throw new Error(`Scale type "${type}" does not support interpolate.`);
138
162
  }
139
163
 
140
- if (type === "band") {
164
+ const midpoint = validateSequentialMidpoint(
165
+ retainedValue(previous, patch, defaults, "midpoint", typeChanged), type, definition.domain
166
+ );
167
+ if (midpoint !== undefined) definition.midpoint = midpoint;
168
+
169
+ if (type === "band" || (type === "ordinal" && allowOrdinalBandParameters)) {
141
170
  validateBandParameters(definition, previous, patch, defaults, typeChanged);
142
171
  } else if (type === "point") {
143
172
  validatePointParameters(definition, previous, patch, defaults, typeChanged);
@@ -149,5 +178,13 @@ export function normalizeScaleDefinition({
149
178
  }
150
179
  }
151
180
 
181
+ const radialMapping = Object.hasOwn(patch, "radialMapping") ? patch.radialMapping : previous.radialMapping;
182
+ if (radialMapping !== undefined) {
183
+ definition.radialMapping = radialMapping;
184
+ validateMeasuredRadiusScale({ ...definition,
185
+ ...(Object.hasOwn(patch, "unknown") || Object.hasOwn(previous, "unknown")
186
+ ? { unknown: patch.unknown ?? previous.unknown } : {})
187
+ }, { allowAuto: true });
188
+ }
152
189
  return definition;
153
190
  }
@@ -1,6 +1,7 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../../core/validation.js";
3
3
  import { interpolateNumber } from "../numeric.js";
4
+ import { formatValue, validateValueFormat } from "../valueFormat.js";
4
5
  import { resolveColorRange, validateColorRange } from "./appearance.js";
5
6
  import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
6
7
 
@@ -162,17 +163,22 @@ export function mapDiscretizedColors(values, scale) {
162
163
  }));
163
164
  }
164
165
 
165
- export function formatDiscretizedIntervals(thresholds) {
166
+ export function formatDiscretizedIntervals(thresholds, format = "auto") {
166
167
  nondecreasing(thresholds, "Discretized color thresholds");
168
+ const resolvedFormat = validateValueFormat(format, "Legend label format");
167
169
  let resolution = Infinity;
168
170
  for (let index = 1; index < thresholds.length; index += 1) {
169
171
  const gap = thresholds[index] - thresholds[index - 1];
170
172
  if (Number.isFinite(gap) && gap > 0) resolution = Math.min(resolution, gap);
171
173
  }
172
- let labels = thresholds.map(value =>
173
- Number.isInteger(value) ? String(value) : value.toFixed(1)
174
- );
175
- if (thresholds.some((value, index) => {
174
+ let labels = resolvedFormat === "auto"
175
+ ? thresholds.map(value => Number.isInteger(value) ? String(value) : value.toFixed(1))
176
+ : thresholds.map(value => formatValue(value, {
177
+ format: resolvedFormat,
178
+ valueType: "quantitative",
179
+ label: "Legend label format"
180
+ }));
181
+ if (resolvedFormat === "auto" && thresholds.some((value, index) => {
176
182
  const rounded = Number(labels[index]);
177
183
  return value !== 0 && rounded === 0 ||
178
184
  index > 0 && value !== thresholds[index - 1] &&
@@ -81,9 +81,37 @@ function validTimestamp(value) {
81
81
  return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
82
82
  }
83
83
 
84
- export function normalizeTemporalValue(value, field = "value", index = 0) {
84
+ export function validateTemporalUnit(unit) {
85
+ if (unit !== undefined && !["auto", "year", "timestamp"].includes(unit)) {
86
+ throw new Error(`Unsupported temporalUnit "${unit}"; use auto, year, or timestamp.`);
87
+ }
88
+ return unit;
89
+ }
90
+
91
+ export function resolveTemporalUnit(options, fieldType, previous) {
92
+ if (fieldType !== "temporal") {
93
+ if (Object.hasOwn(options, "temporalUnit")) {
94
+ throw new Error("temporalUnit requires a temporal fieldType.");
95
+ }
96
+ return undefined;
97
+ }
98
+ validateTemporalUnit(options.temporalUnit);
99
+ const sameBinding = ["field", "datum"].some(key =>
100
+ Object.hasOwn(options, key) && Object.hasOwn(previous ?? {}, key) &&
101
+ options[key] === previous[key]);
102
+ return options.temporalUnit ?? (sameBinding && previous?.fieldType === "temporal"
103
+ ? previous.temporalUnit : undefined);
104
+ }
105
+
106
+ export function normalizeTemporalValue(value, field = "value", index = 0, temporalUnit = "auto") {
107
+ validateTemporalUnit(temporalUnit);
85
108
  let timestamp;
86
- if (typeof value === "number") {
109
+ if (temporalUnit === "year") {
110
+ const year = typeof value === "string" && /^\d{4}$/.test(value) ? Number(value) : value;
111
+ if (Number.isInteger(year) && year >= 0 && year <= 9999) timestamp = utcDate(year);
112
+ } else if (temporalUnit === "timestamp") {
113
+ if (typeof value === "number") timestamp = value;
114
+ } else if (typeof value === "number") {
87
115
  timestamp = Number.isInteger(value) && value >= 1000 && value <= 9999
88
116
  ? utcDate(value)
89
117
  : value;
@@ -99,21 +127,22 @@ export function normalizeTemporalValue(value, field = "value", index = 0) {
99
127
  if (!validTimestamp(timestamp)) {
100
128
  throw new TypeError(
101
129
  `Field "${field}" must contain a temporal string or finite timestamp ` +
102
- `(including a valid date or four-digit year) at row ${index}.`
130
+ `(including a valid date or four-digit year) at row ${index}; temporalUnit is ${temporalUnit}.`
103
131
  );
104
132
  }
105
133
  return timestamp;
106
134
  }
107
135
 
108
- export function readTemporalField(rows, field) {
136
+ export function readTemporalField(rows, field, temporalUnit) {
109
137
  validateField(field);
138
+ validateTemporalUnit(temporalUnit);
110
139
  return cloneAndFreeze(rows.map((row, index) => {
111
140
  if (!Object.hasOwn(row, field)) {
112
141
  throw new TypeError(
113
142
  `Field "${field}" must contain a temporal string or finite timestamp at row ${index}.`
114
143
  );
115
144
  }
116
- return normalizeTemporalValue(row[field], field, index);
145
+ return normalizeTemporalValue(row[field], field, index, temporalUnit);
117
146
  }));
118
147
  }
119
148
 
@@ -129,10 +158,11 @@ export function readNominalField(rows, field) {
129
158
  }));
130
159
  }
131
160
 
132
- export function readScaleField(rows, field, fieldType, { allowUnknown = false } = {}) {
161
+ export function readScaleField(rows, field, fieldType, { allowUnknown = false, temporalUnit } = {}) {
133
162
  validateSemanticFieldType(fieldType);
163
+ validateTemporalUnit(temporalUnit);
134
164
  if (!allowUnknown) {
135
- if (fieldType === "temporal") return readTemporalField(rows, field);
165
+ if (fieldType === "temporal") return readTemporalField(rows, field, temporalUnit);
136
166
  if (["nominal", "ordinal"].includes(fieldType)) {
137
167
  return readNominalField(rows, field);
138
168
  }
@@ -149,7 +179,7 @@ export function readScaleField(rows, field, fieldType, { allowUnknown = false }
149
179
  return isNominalValue(value) ? value : undefined;
150
180
  }
151
181
  try {
152
- return normalizeTemporalValue(value, field, index);
182
+ return normalizeTemporalValue(value, field, index, temporalUnit);
153
183
  } catch {
154
184
  return undefined;
155
185
  }
@@ -7,6 +7,7 @@ export * from "./fields.js";
7
7
  export * from "./mapping.js";
8
8
  export * from "./ordinal.js";
9
9
  export * from "./policies.js";
10
+ export * from "./size.js";
10
11
  export * from "./transformed.js";
11
12
  export * from "./types.js";
12
13
  export * from "./validation.js";
@@ -1,3 +1,4 @@
1
+ import { mapMeasuredRadiusValues } from "./radial.js";
1
2
  import { mapLinearValues } from "./continuous.js";
2
3
  import { mapTransformedValues } from "./transformed.js";
3
4
  import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
@@ -9,6 +10,7 @@ export function isTransformedScaleType(type) {
9
10
  }
10
11
 
11
12
  export function mapContinuousScaleValues(values, scale) {
13
+ if (scale?.radialMapping !== undefined) return mapMeasuredRadiusValues(values, scale);
12
14
  if (isTransformedScaleType(scale?.type)) {
13
15
  validateScaleTypeForRole(scale.type, SCALE_ROLES.quantitativePosition);
14
16
  return mapTransformedValues(values, scale.domain, scale.range, {
@@ -196,6 +196,7 @@ export function resolveOrdinalOffsetScale({
196
196
  parentBandwidth,
197
197
  paddingInner = 0,
198
198
  paddingOuter = 0,
199
+ align = 0.5,
199
200
  channel = "xOffset"
200
201
  }) {
201
202
  const resolvedDomain = resolveOrdinalDomain(domain, values);
@@ -217,6 +218,9 @@ export function resolveOrdinalOffsetScale({
217
218
  "Offset scale paddingOuter must be a non-negative finite number."
218
219
  );
219
220
  }
221
+ if (!Number.isFinite(align) || align < 0 || align > 1) {
222
+ throw new RangeError("Offset scale align must be between 0 and 1.");
223
+ }
220
224
  validateDomainValues(resolvedDomain, values, undefined, "ordinal");
221
225
  const denominator = Math.max(
222
226
  1,
@@ -231,15 +235,18 @@ export function resolveOrdinalOffsetScale({
231
235
  if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
232
236
  throw new Error("Offset scale padding must leave a positive bandwidth.");
233
237
  }
238
+ const occupied = step * (resolvedDomain.length - paddingInner);
239
+ const start = resolvedRange[0] + (resolvedRange[1] - resolvedRange[0] - occupied) * align;
234
240
  return cloneAndFreeze({
235
241
  type: "ordinal",
236
242
  domain: resolvedDomain,
237
243
  range: resolvedRange,
238
244
  step,
239
- start: resolvedRange[0] + step * paddingOuter,
245
+ start,
240
246
  bandwidth,
241
247
  paddingInner,
242
- paddingOuter
248
+ paddingOuter,
249
+ align
243
250
  });
244
251
  }
245
252
 
@@ -296,7 +303,9 @@ export function mapOrdinalOffsetValues(values, scale) {
296
303
  return finitePosition(
297
304
  direct,
298
305
  scale,
299
- (scale.paddingOuter + index + (1 - scale.paddingInner) / 2) /
306
+ (scale.paddingOuter * 2 * scale.align +
307
+ index +
308
+ (1 - scale.paddingInner) / 2) /
300
309
  denominator
301
310
  );
302
311
  }));
@@ -9,9 +9,9 @@ export function validateScaleUnknown(channel, value) {
9
9
  }
10
10
  return value;
11
11
  }
12
- if (channel === "color") {
12
+ if (["color", "stroke"].includes(channel)) {
13
13
  if (typeof value !== "string" || value.length === 0) {
14
- throw new TypeError("Scale unknown for color must be a non-empty color string.");
14
+ throw new TypeError(`Scale unknown for ${channel} must be a non-empty color string.`);
15
15
  }
16
16
  return value;
17
17
  }