ggaction 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -19,6 +19,350 @@ const MAX_EXHAUSTIVE_OFFSET_STEPS = 28;
19
19
  const MAX_SEARCH_DISPLACEMENT = 1_000_000;
20
20
  const DISTANT_RING_COUNT = 16;
21
21
  const DISTANT_ANGLE_COUNT = 32;
22
+ const PLACEMENT_OPTIONS = Object.freeze([
23
+ "anchor", "gap", "overflow", "leader"
24
+ ]);
25
+ const PLACEMENT_LEADER_OPTIONS = Object.freeze(["stroke", "strokeWidth"]);
26
+ const SEMANTIC_ANCHORS = new Set([
27
+ "center", "insideStart", "insideEnd", "outsideStart", "outsideEnd"
28
+ ]);
29
+ const OVERFLOW_POLICIES = new Set(["hide", "outside", "allow"]);
30
+
31
+ export const DEFAULT_MARK_LABEL_PLACEMENT = cloneAndFreeze({
32
+ gap: 4,
33
+ overflow: "hide",
34
+ leader: false
35
+ });
36
+
37
+ export function markLabelPlacementLeaderId(target) {
38
+ return `${target}-placement-leaders`;
39
+ }
40
+
41
+ function normalizePlacementLeader(value) {
42
+ if (value === undefined || value === false) return false;
43
+ validateOptionObject(value, PLACEMENT_LEADER_OPTIONS, "mark label placement leader");
44
+ const leader = { stroke: "#94a3b8", strokeWidth: 1, ...value };
45
+ if (typeof leader.stroke !== "string" || leader.stroke.length === 0) {
46
+ throw new TypeError("Mark label placement leader stroke must be a non-empty string.");
47
+ }
48
+ validateNonNegativeFinite(
49
+ leader.strokeWidth,
50
+ "Mark label placement leader strokeWidth"
51
+ );
52
+ return leader;
53
+ }
54
+
55
+ export function normalizeMarkLabelPlacement(value) {
56
+ validateOptionObject(value, PLACEMENT_OPTIONS, "mark label placement", {
57
+ allowEmpty: false,
58
+ emptyError: TypeError
59
+ });
60
+ if (!SEMANTIC_ANCHORS.has(value.anchor)) {
61
+ throw new Error(`Unsupported mark label anchor "${value.anchor}".`);
62
+ }
63
+ const gap = value.gap ?? DEFAULT_MARK_LABEL_PLACEMENT.gap;
64
+ const overflowPolicy = value.overflow ?? DEFAULT_MARK_LABEL_PLACEMENT.overflow;
65
+ validateNonNegativeFinite(gap, "Mark label placement gap");
66
+ if (!OVERFLOW_POLICIES.has(overflowPolicy)) {
67
+ throw new Error(`Unsupported mark label overflow "${overflowPolicy}".`);
68
+ }
69
+ return cloneAndFreeze({
70
+ anchor: value.anchor,
71
+ gap,
72
+ overflow: overflowPolicy,
73
+ leader: normalizePlacementLeader(value.leader)
74
+ });
75
+ }
76
+
77
+ function finitePoint(value, label) {
78
+ if (
79
+ !isPlainObject(value) ||
80
+ !Number.isFinite(value.x) ||
81
+ !Number.isFinite(value.y)
82
+ ) {
83
+ throw new TypeError(`${label} requires finite x and y.`);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ function unitVector(value, label) {
89
+ finitePoint(value, label);
90
+ const length = Math.hypot(value.x, value.y);
91
+ if (!Number.isFinite(length) || length <= 0) {
92
+ throw new RangeError(`${label} requires a non-zero direction.`);
93
+ }
94
+ return { x: value.x / length, y: value.y / length };
95
+ }
96
+
97
+ function validateIntervalBounds(bounds) {
98
+ if (
99
+ !isPlainObject(bounds) ||
100
+ ![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
101
+ bounds.right < bounds.left ||
102
+ bounds.bottom < bounds.top
103
+ ) {
104
+ throw new RangeError("Interval label placement requires ordered finite bounds.");
105
+ }
106
+ }
107
+
108
+ function validatePlacementGeometry(geometry, anchor) {
109
+ if (!isPlainObject(geometry)) {
110
+ throw new TypeError("Mark label placement requires source geometry.");
111
+ }
112
+ if (geometry.kind === "interval") {
113
+ finitePoint(geometry.center, "Interval label center");
114
+ for (const role of ["start", "end"]) {
115
+ finitePoint(geometry[role]?.point, `Interval label ${role}`);
116
+ unitVector(geometry[role]?.outward, `Interval label ${role}`);
117
+ }
118
+ validateIntervalBounds(geometry.bounds);
119
+ return;
120
+ }
121
+ if (geometry.kind === "arc") {
122
+ if (![geometry.centerX, geometry.centerY, geometry.startTheta,
123
+ geometry.endTheta, geometry.innerRadius, geometry.outerRadius]
124
+ .every(Number.isFinite)) {
125
+ throw new TypeError("Arc label placement requires finite sector geometry.");
126
+ }
127
+ if (
128
+ geometry.innerRadius < 0 ||
129
+ geometry.outerRadius <= geometry.innerRadius ||
130
+ geometry.startTheta === geometry.endTheta ||
131
+ Math.abs(geometry.endTheta - geometry.startTheta) > 360
132
+ ) {
133
+ throw new RangeError("Arc label placement requires an ordered annular sector.");
134
+ }
135
+ return;
136
+ }
137
+ if (geometry.kind === "point") {
138
+ finitePoint(geometry.center, "Point label center");
139
+ if (anchor === "outsideEnd") {
140
+ finitePoint(geometry.end?.point, "Polar Point label boundary");
141
+ unitVector(geometry.end?.outward, "Polar Point label direction");
142
+ }
143
+ return;
144
+ }
145
+ throw new Error(`Unsupported mark label placement geometry "${geometry.kind}".`);
146
+ }
147
+
148
+ export function assertMarkLabelPlacementSupport({ markType, coordinateType, intervalAxis }, placement) {
149
+ const anchor = placement.anchor;
150
+ if (markType === "bar" || markType === "arc") return placement;
151
+ if (markType === "rect") {
152
+ if (anchor === "center" || intervalAxis !== undefined) return placement;
153
+ throw new Error("Rect semantic label placement requires one explicit interval axis.");
154
+ }
155
+ if (markType === "point") {
156
+ if (anchor === "center" || (coordinateType === "polar" && anchor === "outsideEnd")) {
157
+ return placement;
158
+ }
159
+ throw new Error(
160
+ coordinateType === "polar"
161
+ ? `Polar Point does not support mark label anchor "${anchor}".`
162
+ : `Point does not support mark label anchor "${anchor}".`
163
+ );
164
+ }
165
+ if (markType === "line") {
166
+ throw new Error("Line labels retain the endpoint API and do not accept semantic placement objects.");
167
+ }
168
+ if (markType === "rule") {
169
+ throw new Error("Rule labels do not support semantic placement objects.");
170
+ }
171
+ throw new Error(`Mark type "${markType}" does not support semantic label placement.`);
172
+ }
173
+
174
+ function localTextBounds(text) {
175
+ return resolveTextBounds({ ...text, x: 0, y: 0 }, text.profile);
176
+ }
177
+
178
+ function projectionRange(bounds, direction) {
179
+ const values = [
180
+ { x: bounds.left, y: bounds.top },
181
+ { x: bounds.right, y: bounds.top },
182
+ { x: bounds.right, y: bounds.bottom },
183
+ { x: bounds.left, y: bounds.bottom }
184
+ ].map(point => point.x * direction.x + point.y * direction.y);
185
+ return { minimum: Math.min(...values), maximum: Math.max(...values) };
186
+ }
187
+
188
+ function boundaryCandidate(boundary, direction, gap, text) {
189
+ const resolvedDirection = unitVector(direction, "Mark label placement direction");
190
+ const support = projectionRange(localTextBounds(text), resolvedDirection);
191
+ const distance = gap - support.minimum;
192
+ return {
193
+ x: boundary.x + resolvedDirection.x * distance + (text.dx ?? 0),
194
+ y: boundary.y + resolvedDirection.y * distance + (text.dy ?? 0),
195
+ sourceX: boundary.x,
196
+ sourceY: boundary.y
197
+ };
198
+ }
199
+
200
+ function centerCandidate(center, text) {
201
+ return {
202
+ x: center.x + (text.dx ?? 0),
203
+ y: center.y + (text.dy ?? 0),
204
+ sourceX: center.x,
205
+ sourceY: center.y
206
+ };
207
+ }
208
+
209
+ function candidateBounds(candidate, text) {
210
+ return resolveTextBounds({ ...text, x: candidate.x, y: candidate.y }, text.profile);
211
+ }
212
+
213
+ function intervalContains(bounds, container) {
214
+ const epsilon = 1e-9;
215
+ return bounds.left >= container.left - epsilon &&
216
+ bounds.right <= container.right + epsilon &&
217
+ bounds.top >= container.top - epsilon &&
218
+ bounds.bottom <= container.bottom + epsilon;
219
+ }
220
+
221
+ function distanceToRectangle(centerX, centerY, bounds) {
222
+ const x = Math.max(bounds.left, Math.min(centerX, bounds.right));
223
+ const y = Math.max(bounds.top, Math.min(centerY, bounds.bottom));
224
+ return Math.hypot(x - centerX, y - centerY);
225
+ }
226
+
227
+ function thetaForPoint(point, geometry) {
228
+ return Math.atan2(point.x - geometry.centerX, geometry.centerY - point.y) *
229
+ 180 / Math.PI;
230
+ }
231
+
232
+ function angleWithin(theta, start, end) {
233
+ const sweep = end - start;
234
+ if (Math.abs(sweep) >= 360 - 1e-9) return true;
235
+ const direction = Math.sign(sweep);
236
+ const progress = ((direction * (theta - start)) % 360 + 360) % 360;
237
+ return progress <= Math.abs(sweep) + 1e-9;
238
+ }
239
+
240
+ function arcContains(bounds, geometry) {
241
+ const points = [
242
+ { x: bounds.left, y: bounds.top },
243
+ { x: bounds.right, y: bounds.top },
244
+ { x: bounds.right, y: bounds.bottom },
245
+ { x: bounds.left, y: bounds.bottom },
246
+ { x: (bounds.left + bounds.right) / 2, y: bounds.top },
247
+ { x: bounds.right, y: (bounds.top + bounds.bottom) / 2 },
248
+ { x: (bounds.left + bounds.right) / 2, y: bounds.bottom },
249
+ { x: bounds.left, y: (bounds.top + bounds.bottom) / 2 }
250
+ ];
251
+ const minimumRadius = distanceToRectangle(
252
+ geometry.centerX,
253
+ geometry.centerY,
254
+ bounds
255
+ );
256
+ const maximumRadius = Math.max(...points.slice(0, 4).map(point =>
257
+ Math.hypot(point.x - geometry.centerX, point.y - geometry.centerY)
258
+ ));
259
+ const epsilon = 1e-9;
260
+ return minimumRadius >= geometry.innerRadius - epsilon &&
261
+ maximumRadius <= geometry.outerRadius + epsilon &&
262
+ points.every(point => angleWithin(
263
+ thetaForPoint(point, geometry),
264
+ geometry.startTheta,
265
+ geometry.endTheta
266
+ ));
267
+ }
268
+
269
+ function arcRole(geometry, role) {
270
+ const theta = geometry.startTheta +
271
+ (geometry.endTheta - geometry.startTheta) / 2;
272
+ const radians = theta * Math.PI / 180;
273
+ const radial = { x: Math.sin(radians), y: -Math.cos(radians) };
274
+ const radius = role === "start" ? geometry.innerRadius : geometry.outerRadius;
275
+ return {
276
+ point: {
277
+ x: geometry.centerX + radial.x * radius,
278
+ y: geometry.centerY + radial.y * radius
279
+ },
280
+ outward: role === "start"
281
+ ? { x: -radial.x, y: -radial.y }
282
+ : radial
283
+ };
284
+ }
285
+
286
+ function geometryRole(geometry, role) {
287
+ return geometry.kind === "arc" ? arcRole(geometry, role) : geometry[role];
288
+ }
289
+
290
+ function geometryCenter(geometry) {
291
+ if (geometry.kind !== "arc") return geometry.center;
292
+ const role = arcRole(geometry, "end");
293
+ const radius = (geometry.innerRadius + geometry.outerRadius) / 2;
294
+ return {
295
+ x: geometry.centerX + role.outward.x * radius,
296
+ y: geometry.centerY + role.outward.y * radius
297
+ };
298
+ }
299
+
300
+ function candidateForAnchor(anchor, geometry, placement, text) {
301
+ if (anchor === "center") return centerCandidate(geometryCenter(geometry), text);
302
+ const roleName = anchor.endsWith("Start") ? "start" : "end";
303
+ const role = geometryRole(geometry, roleName);
304
+ const outside = anchor.startsWith("outside");
305
+ const direction = outside
306
+ ? role.outward
307
+ : { x: -role.outward.x, y: -role.outward.y };
308
+ return boundaryCandidate(role.point, direction, placement.gap, text);
309
+ }
310
+
311
+ function arcOutsideStartCrossesCenter(candidate, geometry, text) {
312
+ if (geometry.kind !== "arc") return false;
313
+ const radial = arcRole(geometry, "end").outward;
314
+ const projections = projectionRange(candidateBounds(candidate, text), radial);
315
+ const centerProjection = geometry.centerX * radial.x + geometry.centerY * radial.y;
316
+ return projections.minimum < centerProjection - 1e-9;
317
+ }
318
+
319
+ function fitsSource(candidate, geometry, text) {
320
+ if (geometry.kind === "point") return true;
321
+ const bounds = candidateBounds(candidate, text);
322
+ return geometry.kind === "arc"
323
+ ? arcContains(bounds, geometry)
324
+ : intervalContains(bounds, geometry.bounds);
325
+ }
326
+
327
+ function outsideFallback(anchor) {
328
+ return anchor === "insideStart" ? "outsideStart" : "outsideEnd";
329
+ }
330
+
331
+ export function resolveMarkLabelPlacement({ placement, geometry, text } = {}, profile) {
332
+ text = { ...text, profile };
333
+ const normalized = normalizeMarkLabelPlacement(placement);
334
+ validatePlacementGeometry(geometry, normalized.anchor);
335
+ let anchor = normalized.anchor;
336
+ let candidate = candidateForAnchor(anchor, geometry, normalized, text);
337
+ const requiresFit = anchor === "center" || anchor.startsWith("inside");
338
+ if (requiresFit && !fitsSource(candidate, geometry, text)) {
339
+ if (normalized.overflow === "hide") {
340
+ return cloneAndFreeze({ visible: false, anchor, fallback: false });
341
+ }
342
+ if (normalized.overflow === "outside") {
343
+ anchor = outsideFallback(anchor);
344
+ candidate = candidateForAnchor(anchor, geometry, normalized, text);
345
+ }
346
+ }
347
+ if (
348
+ anchor === "outsideStart" &&
349
+ normalized.overflow !== "allow" &&
350
+ arcOutsideStartCrossesCenter(candidate, geometry, text)
351
+ ) {
352
+ return cloneAndFreeze({
353
+ visible: false,
354
+ anchor,
355
+ fallback: anchor !== normalized.anchor
356
+ });
357
+ }
358
+ return cloneAndFreeze({
359
+ visible: true,
360
+ anchor,
361
+ fallback: anchor !== normalized.anchor,
362
+ ...candidate,
363
+ bounds: candidateBounds(candidate, text)
364
+ });
365
+ }
22
366
 
23
367
  export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
24
368
  axis: "both",
@@ -158,7 +502,7 @@ export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
158
502
  return cloneAndFreeze(candidates);
159
503
  }
160
504
 
161
- function textBounds(item, offset = { x: 0, y: 0 }) {
505
+ function textBounds(item, offset = { x: 0, y: 0 }, profile) {
162
506
  return resolveTextBounds({
163
507
  x: item.x + offset.x,
164
508
  y: item.y + offset.y,
@@ -169,7 +513,7 @@ function textBounds(item, offset = { x: 0, y: 0 }) {
169
513
  textAlign: item.textAlign,
170
514
  textBaseline: item.textBaseline,
171
515
  rotation: item.rotation
172
- });
516
+ }, profile);
173
517
  }
174
518
 
175
519
  function candidateScore(bounds, boundary, placed, candidate, order) {
@@ -209,7 +553,7 @@ function overlapPairs(items) {
209
553
  return pairs;
210
554
  }
211
555
 
212
- export function resolveLabelLayout({ items, bounds, ...options } = {}) {
556
+ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile) {
213
557
  if (!Array.isArray(items)) {
214
558
  throw new TypeError("Label layout requires an item array.");
215
559
  }
@@ -224,14 +568,14 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
224
568
  );
225
569
  const base = items.map(item => ({
226
570
  id: item.id,
227
- collisionBounds: expanded(textBounds(item), policy.padding)
571
+ collisionBounds: expanded(textBounds(item, undefined, profile), policy.padding)
228
572
  }));
229
573
  const placed = [];
230
574
  const resolved = [];
231
575
  for (const item of items) {
232
576
  let best;
233
577
  for (const [order, candidate] of candidates.entries()) {
234
- const boundsAtCandidate = textBounds(item, candidate);
578
+ const boundsAtCandidate = textBounds(item, candidate, profile);
235
579
  const collisionBounds = expanded(boundsAtCandidate, policy.padding);
236
580
  const score = candidateScore(
237
581
  collisionBounds,
@@ -276,7 +620,7 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
276
620
  });
277
621
  }
278
622
 
279
- export function assertPolarTextLayout({ canvas, items, label }) {
623
+ export function assertPolarTextLayout({ canvas, items, label }, profile) {
280
624
  const dimensions = canvas?.properties;
281
625
  if (
282
626
  ![dimensions?.width, dimensions?.height].every(Number.isFinite) ||
@@ -284,7 +628,7 @@ export function assertPolarTextLayout({ canvas, items, label }) {
284
628
  ) {
285
629
  throw new Error("Polar text layout requires finite Canvas dimensions.");
286
630
  }
287
- const bounds = items.map(resolveTextBounds);
631
+ const bounds = items.map(item => resolveTextBounds(item, profile));
288
632
  const outside = bounds.some(item => !textBoundsFitCanvas(item, dimensions));
289
633
  let overlap = false;
290
634
  for (let first = 0; first < bounds.length && !overlap; first += 1) {
@@ -3,15 +3,15 @@ import {
3
3
  measureTextWidth
4
4
  } from "../core/textMetrics.js";
5
5
 
6
- export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
7
- return measureTextWidth(formatVisibleText(value), style);
6
+ export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile) {
7
+ return measureTextWidth(formatVisibleText(value), style, profile);
8
8
  }
9
9
 
10
- export function resolveLegendGrid(config, width, count, symbolHeight) {
10
+ export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
11
11
  const labels = config.domain.map(formatVisibleText);
12
12
  const itemWidths = labels.map(
13
13
  label => width + config.labels.offset +
14
- measureLegendTextWidth(label, config.labels)
14
+ measureLegendTextWidth(label, config.labels, profile)
15
15
  );
16
16
  const columnCount = Math.min(config.columns ?? count, count);
17
17
  const rowCount = Math.ceil(count / columnCount);
@@ -3,7 +3,7 @@ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
3
3
 
4
4
  // Measure item content before choosing its edge. Family owners supply the
5
5
  // sample dimensions and formatted labels; this module does not inspect scales.
6
- export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
6
+ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, profile) {
7
7
  const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
8
8
  ? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
9
9
  const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
@@ -19,17 +19,17 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
19
19
  ...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
20
20
  const side = ["left", "right"].includes(config.position);
21
21
  const titleVisible = config.titleVisible !== false;
22
- const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
22
+ const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 0;
23
23
  const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
24
24
  const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
25
- text => measureTextWidth(text, config.labels)
25
+ text => measureTextWidth(text, config.labels, profile)
26
26
  ));
27
27
  let symbolX;
28
28
  let labelX;
29
29
  let itemY;
30
30
  let title;
31
31
  if (config.layout === "legacy-bottom") {
32
- const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
32
+ const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
33
33
  const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
34
34
  const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
35
35
  let cursor = x;
@@ -54,7 +54,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
54
54
  itemY = labels.map((_, index) => firstY + index * pitch);
55
55
  title = { x, y: plot.y + 20, align: "left" };
56
56
  } else {
57
- const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
57
+ const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
58
58
  const inline = config.titlePosition === "left" && titleVisible;
59
59
  const prefix = inline ? titleWidth + 20 : 0;
60
60
  const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
@@ -79,7 +79,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
79
79
  }
80
80
  const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
81
81
  x, y, text, ...style, textAlign: align, textBaseline: "middle"
82
- });
82
+ }, profile);
83
83
  const bounds = [
84
84
  ...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
85
85
  ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
@@ -5,12 +5,12 @@ export {
5
5
  resolveTextBounds
6
6
  } from "../core/textMetrics.js";
7
7
 
8
- function wrapCodePoints(text, maxWidth, style) {
8
+ function wrapCodePoints(text, maxWidth, style, profile) {
9
9
  const lines = [];
10
10
  let line = "";
11
11
  for (const codePoint of [...text]) {
12
12
  const candidate = line + codePoint;
13
- if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
13
+ if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
14
14
  lines.push(line);
15
15
  line = codePoint;
16
16
  } else {
@@ -21,22 +21,22 @@ function wrapCodePoints(text, maxWidth, style) {
21
21
  return lines;
22
22
  }
23
23
 
24
- function wrapWords(text, maxWidth, style) {
24
+ function wrapWords(text, maxWidth, style, profile) {
25
25
  const lines = [];
26
26
  let line = "";
27
27
  for (const word of text.trim().split(/\s+/u)) {
28
- if (measureTextWidth(word, style) > maxWidth) {
28
+ if (measureTextWidth(word, style, profile) > maxWidth) {
29
29
  if (line !== "") {
30
30
  lines.push(line);
31
31
  line = "";
32
32
  }
33
- const fragments = wrapCodePoints(word, maxWidth, style);
33
+ const fragments = wrapCodePoints(word, maxWidth, style, profile);
34
34
  lines.push(...fragments.slice(0, -1));
35
35
  line = fragments.at(-1);
36
36
  continue;
37
37
  }
38
38
  const candidate = line === "" ? word : `${line} ${word}`;
39
- if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
39
+ if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
40
40
  lines.push(line);
41
41
  line = word;
42
42
  } else {
@@ -50,7 +50,8 @@ function wrapWords(text, maxWidth, style) {
50
50
  export function wrapText(text, {
51
51
  maxWidth,
52
52
  mode = "word",
53
- style
53
+ style,
54
+ profile
54
55
  } = {}) {
55
56
  if (maxWidth === undefined) return [text];
56
57
  if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
@@ -60,6 +61,6 @@ export function wrapText(text, {
60
61
  throw new Error(`Unsupported text wrap mode "${mode}".`);
61
62
  }
62
63
  return mode === "word"
63
- ? wrapWords(text, maxWidth, style)
64
- : wrapCodePoints(text, maxWidth, style);
64
+ ? wrapWords(text, maxWidth, style, profile)
65
+ : wrapCodePoints(text, maxWidth, style, profile);
65
66
  }
@@ -4,16 +4,16 @@ function lineCenters(lines, style, lineHeight, start) {
4
4
  return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
5
5
  }
6
6
 
7
- export function buildTitleReadingBlock({ text, subtitle }, config) {
7
+ export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
8
8
  const titleLines = wrapText(text, {
9
9
  maxWidth: config.maxWidth,
10
10
  mode: config.wrap,
11
- style: config.titleStyle
11
+ style: config.titleStyle, profile
12
12
  });
13
13
  const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
14
14
  maxWidth: config.maxWidth,
15
15
  mode: config.wrap,
16
- style: config.subtitleStyle
16
+ style: config.subtitleStyle, profile
17
17
  });
18
18
  const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
19
19
  const subtitleLineHeight = config.lineHeight ??
@@ -36,8 +36,8 @@ export function buildTitleReadingBlock({ text, subtitle }, config) {
36
36
  ? titleBottom
37
37
  : subtitleCenters.at(-1) + config.subtitleStyle.fontSize / 2;
38
38
  const widths = [
39
- ...titleLines.map(line => measureTextWidth(line, config.titleStyle)),
40
- ...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle))
39
+ ...titleLines.map(line => measureTextWidth(line, config.titleStyle, profile)),
40
+ ...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle, profile))
41
41
  ];
42
42
  return {
43
43
  titleLines,
@@ -61,7 +61,7 @@ export function alignedTextAnchor(start, length, align) {
61
61
  return start + length;
62
62
  }
63
63
 
64
- function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
64
+ function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile) {
65
65
  return resolveTextBounds({
66
66
  x,
67
67
  y,
@@ -70,7 +70,7 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
70
70
  textAlign: align,
71
71
  textBaseline: "middle",
72
72
  rotation
73
- });
73
+ }, profile);
74
74
  }
75
75
 
76
76
  function unionBounds(bounds) {
@@ -82,7 +82,7 @@ function unionBounds(bounds) {
82
82
  }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
83
83
  }
84
84
 
85
- export function resolveTitleComponentBounds(component, style) {
85
+ export function resolveTitleComponentBounds(component, style, profile) {
86
86
  return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
87
87
  x: Array.isArray(component.x) ? component.x[index] : component.x,
88
88
  y: Array.isArray(component.y) ? component.y[index] : component.y,
@@ -90,7 +90,7 @@ export function resolveTitleComponentBounds(component, style) {
90
90
  style,
91
91
  align: component.textAlign,
92
92
  rotation: component.rotation
93
- })));
93
+ }, profile)));
94
94
  }
95
95
 
96
96
  export function unionTitleBounds(bounds) {
@@ -1,6 +1,10 @@
1
1
  import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
2
2
  import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
3
- import { mapContinuousScaleValues, mapOrdinalPositionValues } from "../../grammar/scales/index.js";
3
+ import {
4
+ mapContinuousScaleValues,
5
+ mapOrdinalPositionValues,
6
+ readScaleField
7
+ } from "../../grammar/scales/index.js";
4
8
  import { mapScaleConsumerValues } from "../scales/map.js";
5
9
  import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
6
10
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
@@ -19,6 +23,24 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
19
23
  const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
20
24
  grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
21
25
  const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
26
+ const stroke = layer.encoding?.stroke;
27
+ const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
28
+ const values = readScaleField(
29
+ segment.cell.sourceIndices.map(index => dataset.values[index]),
30
+ stroke.field,
31
+ stroke.fieldType,
32
+ { temporalUnit: stroke.temporalUnit }
33
+ );
34
+ if (new Set(values).size !== 1) {
35
+ throw new Error("Bar stroke requires one value within each aggregate cell.");
36
+ }
37
+ return values[0];
38
+ });
39
+ const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
40
+ strokeValues,
41
+ resolved.resolvedScales[stroke.scale],
42
+ "stroke"
43
+ );
22
44
  const config = resolved.markConfigs[layer.id] ?? {};
23
45
  const existing = resolved.graphicSpec.objects[layer.id].items;
24
46
  const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
@@ -42,6 +64,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
42
64
  return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
43
65
  width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
44
66
  fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
45
- ...resolveBarAppearance(config, existing[index]?.properties) };
67
+ ...resolveBarAppearance(config, existing[index]?.properties),
68
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }) };
46
69
  });
47
70
  }