ggaction 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -1,6 +1,5 @@
1
1
  import { readSeriesIdentity } from "../../grammar/pathSeries.js";
2
2
  import {
3
- countHistogramBins,
4
3
  findHistogramBinIndex,
5
4
  resolveHistogramBins
6
5
  } from "../../grammar/histogram.js";
@@ -8,14 +7,55 @@ import {
8
7
  mapContinuousScaleValues,
9
8
  mapOrdinalValues,
10
9
  readNominalField,
11
- readQuantitativeField
10
+ readQuantitativeField,
11
+ readScaleField
12
12
  } from "../../grammar/scales/index.js";
13
+ import { mapScaleConsumerValues } from "../scales/map.js";
13
14
  import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
14
15
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
15
16
  import {
16
17
  DEFAULT_BAR_FILL,
17
18
  resolveBarAppearance
18
19
  } from "./resolve.js";
20
+ import {
21
+ readStatisticalWeights,
22
+ sumEntryWeights,
23
+ summarizeStatisticalWeights,
24
+ validateWeightedNumericFields
25
+ } from "../../grammar/weightedStatistics.js";
26
+
27
+ export function resolveHistogramInput(dataset, xEncoding) {
28
+ const allValues = readQuantitativeField(dataset.values, xEncoding.field);
29
+ if (xEncoding.weight === undefined) {
30
+ return {
31
+ rows: dataset.values,
32
+ values: allValues,
33
+ entries: dataset.values.map((row, index) => ({ row, index })),
34
+ mass(entries) {
35
+ return entries.length;
36
+ }
37
+ };
38
+ }
39
+ const weights = readStatisticalWeights(
40
+ dataset.values,
41
+ xEncoding.weight,
42
+ "Histogram"
43
+ );
44
+ validateWeightedNumericFields(weights.entries, [xEncoding.field], "Histogram");
45
+ const summary = summarizeStatisticalWeights(
46
+ weights.entries,
47
+ weights.definition.kind,
48
+ "Histogram data"
49
+ );
50
+ return {
51
+ rows: summary.positive.map(entry => entry.row),
52
+ values: summary.positive.map(entry => entry.row[xEncoding.field]),
53
+ entries: summary.positive,
54
+ mass(entries) {
55
+ return sumEntryWeights(entries, summary, "Histogram bin mass");
56
+ }
57
+ };
58
+ }
19
59
 
20
60
  export function deriveHistogramSegments({
21
61
  dataset,
@@ -24,7 +64,8 @@ export function deriveHistogramSegments({
24
64
  xScale,
25
65
  resolvedScales
26
66
  }) {
27
- const xValues = readQuantitativeField(dataset.values, xEncoding.field);
67
+ const input = resolveHistogramInput(dataset, xEncoding);
68
+ const { rows, values: xValues, entries } = input;
28
69
  const bins = resolveHistogramBins({
29
70
  values: xValues,
30
71
  bin: xEncoding.bin,
@@ -36,13 +77,13 @@ export function deriveHistogramSegments({
36
77
  const layout = resolveBarColorLayout(layer);
37
78
 
38
79
  if (layer.encoding?.group !== undefined) {
39
- const identity = readSeriesIdentity(dataset.values, layer);
80
+ const identity = readSeriesIdentity(rows, layer);
40
81
  const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
41
82
  const index = new Map(identity.domain.map((value, i) => [value, i]));
42
83
  xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
43
- if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(dataset.values[i]); });
44
- return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(rows => rows.length), layout).map(segment => {
45
- const members = cells[segment.index];
84
+ if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(entries[i]); });
85
+ return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(input.mass), layout).map(segment => {
86
+ const members = cells[segment.index].map(entry => entry.row);
46
87
  let color, colorValue;
47
88
  if (colorEncoding !== undefined) {
48
89
  const values = readNominalField(members, colorEncoding.field);
@@ -59,8 +100,13 @@ export function deriveHistogramSegments({
59
100
  }
60
101
 
61
102
  if (colorEncoding?.scale === undefined) {
62
- return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
63
- layoutSeriesPartition([count], layout).map(segment => ({
103
+ const cellRows = bins.boundaries.slice(0, -1).map(() => []);
104
+ xValues.forEach((value, index) => {
105
+ const bin = findHistogramBinIndex(value, bins.boundaries);
106
+ if (bin !== -1) cellRows[bin].push(entries[index]);
107
+ });
108
+ return cellRows.flatMap((cell, bin) =>
109
+ layoutSeriesPartition([input.mass(cell)], layout).map(segment => ({
64
110
  bin,
65
111
  start: bins.boundaries[bin],
66
112
  end: bins.boundaries[bin + 1],
@@ -68,9 +114,7 @@ export function deriveHistogramSegments({
68
114
  categoryCount: 1,
69
115
  stackStart: segment.start,
70
116
  stackEnd: segment.end,
71
- members: dataset.values.filter((_, index) =>
72
- findHistogramBinIndex(xValues[index], bins.boundaries) === bin
73
- )
117
+ members: cell.map(entry => entry.row)
74
118
  }))
75
119
  );
76
120
  }
@@ -81,24 +125,24 @@ export function deriveHistogramSegments({
81
125
  `Bar mark "${layer.id}" requires a resolved color scale.`
82
126
  );
83
127
  }
84
- const colorValues = readNominalField(dataset.values, colorEncoding.field);
128
+ const colorValues = readNominalField(rows, colorEncoding.field);
85
129
  mapOrdinalValues(colorValues, colorScale.domain, colorScale.range);
86
130
  const categoryIndex = new Map(
87
131
  colorScale.domain.map((value, index) => [value, index])
88
132
  );
89
- const counts = bins.boundaries.slice(0, -1).map(() =>
90
- colorScale.domain.map(() => 0)
133
+ const cells = bins.boundaries.slice(0, -1).map(() =>
134
+ colorScale.domain.map(() => [])
91
135
  );
92
136
 
93
137
  for (let index = 0; index < xValues.length; index += 1) {
94
138
  const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
95
139
  const category = categoryIndex.get(colorValues[index]);
96
- if (bin !== -1 && category !== undefined) counts[bin][category] += 1;
140
+ if (bin !== -1 && category !== undefined) cells[bin][category].push(entries[index]);
97
141
  }
98
142
 
99
143
  const segments = [];
100
- for (let bin = 0; bin < counts.length; bin += 1) {
101
- const partition = layoutSeriesPartition(counts[bin], layout);
144
+ for (let bin = 0; bin < cells.length; bin += 1) {
145
+ const partition = layoutSeriesPartition(cells[bin].map(input.mass), layout);
102
146
  for (const segment of partition) {
103
147
  const category = segment.index;
104
148
  segments.push({
@@ -106,13 +150,10 @@ export function deriveHistogramSegments({
106
150
  start: bins.boundaries[bin],
107
151
  end: bins.boundaries[bin + 1],
108
152
  category,
109
- categoryCount: counts[bin].length,
153
+ categoryCount: cells[bin].length,
110
154
  stackStart: segment.start,
111
155
  stackEnd: segment.end,
112
- members: dataset.values.filter((_, index) =>
113
- findHistogramBinIndex(xValues[index], bins.boundaries) === bin &&
114
- categoryIndex.get(colorValues[index]) === category
115
- ),
156
+ members: cells[bin][category].map(entry => entry.row),
116
157
  color: mapOrdinalValues(
117
158
  [colorScale.domain[category]],
118
159
  colorScale.domain,
@@ -135,6 +176,24 @@ export function deriveHistogramRectangles(required, resolved) {
135
176
  const layout = resolveBarColorLayout(required.layer);
136
177
  const config = resolved.markConfigs[required.layer.id] ?? {};
137
178
  const appearance = config.barAppearance ?? {};
179
+ const stroke = required.layer.encoding?.stroke;
180
+ const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
181
+ const values = readScaleField(
182
+ segment.members,
183
+ stroke.field,
184
+ stroke.fieldType,
185
+ { temporalUnit: stroke.temporalUnit }
186
+ );
187
+ if (new Set(values).size !== 1) {
188
+ throw new Error("Histogram stroke requires one value within each bin/series cell.");
189
+ }
190
+ return values[0];
191
+ });
192
+ const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
193
+ strokeValues,
194
+ resolved.resolvedScales[stroke.scale],
195
+ "stroke"
196
+ );
138
197
 
139
198
  return segments.map((segment, index) => {
140
199
  const [x1, x2] = mapContinuousScaleValues(
@@ -164,7 +223,8 @@ export function deriveHistogramRectangles(required, resolved) {
164
223
  config.fill ??
165
224
  existing[index]?.properties.fill ??
166
225
  DEFAULT_BAR_FILL,
167
- ...resolveBarAppearance(config, existing[index]?.properties)
226
+ ...resolveBarAppearance(config, existing[index]?.properties),
227
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
168
228
  };
169
229
  });
170
230
  }
@@ -1,4 +1,10 @@
1
- import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
1
+ import {
2
+ mapContinuousScaleValues,
3
+ mapOrdinalPositionValues,
4
+ mapOrdinalValues,
5
+ readScaleField
6
+ } from "../../grammar/scales/index.js";
7
+ import { mapScaleConsumerValues } from "../scales/map.js";
2
8
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
9
  import {
4
10
  DEFAULT_BAR_FILL,
@@ -36,6 +42,14 @@ export function deriveRangedRectangles(required, program, width) {
36
42
  const appearanceConfig = color === undefined
37
43
  ? config
38
44
  : { ...config, stroke: undefined };
45
+ const stroke = layer.encoding?.stroke;
46
+ const strokes = stroke === undefined ? undefined : mapScaleConsumerValues(
47
+ readScaleField(dataset.values, stroke.field, stroke.fieldType, {
48
+ temporalUnit: stroke.temporalUnit
49
+ }),
50
+ program.resolvedScales[stroke.scale],
51
+ "stroke"
52
+ );
39
53
  return dataset.values.map((_, index) => vertical ? {
40
54
  x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
41
55
  height: Math.abs(second[index] - first[index]), fill: fills[index],
@@ -44,7 +58,8 @@ export function deriveRangedRectangles(required, program, width) {
44
58
  undefined,
45
59
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
46
60
  1
47
- )
61
+ ),
62
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
48
63
  } : {
49
64
  x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
50
65
  width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
@@ -53,6 +68,7 @@ export function deriveRangedRectangles(required, program, width) {
53
68
  undefined,
54
69
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
55
70
  1
56
- )
71
+ ),
72
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
57
73
  });
58
74
  }
@@ -3,6 +3,7 @@ import { findDataset } from "../../selectors/datasets.js";
3
3
  import { findLayer } from "../../selectors/layers.js";
4
4
  import { findSemanticScale } from "../../selectors/scales.js";
5
5
  import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
6
+ import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
6
7
 
7
8
  export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
8
9
  export const DEFAULT_BAR_STROKE = "white";
@@ -25,7 +26,8 @@ export function resolveBarAppearance(
25
26
  ? 0
26
27
  : appearance.strokeWidth ?? config.strokeWidth ??
27
28
  existing?.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
28
- ...(opacity === undefined ? {} : { opacity })
29
+ ...(opacity === undefined ? {} : { opacity }),
30
+ ...requestedStrokeDetails(appearance, "Bar mark")
29
31
  };
30
32
  }
31
33
 
@@ -40,9 +42,10 @@ export function requireCompleteBar(program, id) {
40
42
  throw new Error(`Bar mark "${id}" requires an existing dataset.`);
41
43
  }
42
44
  const graphic = program.graphicSpec.objects[id];
43
- const rectCollection = graphic?.type === "collection" &&
44
- graphic.items?.every(child => child.type === "rect");
45
- if (graphic?.type !== "rect" && !rectCollection) {
45
+ const roundedCollection = graphic?.type === "collection" &&
46
+ Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
47
+ graphic.items?.every(child => ["rect", "path"].includes(child.type));
48
+ if (graphic?.type !== "rect" && !roundedCollection) {
46
49
  throw new Error(`Bar mark "${id}" requires rect graphics.`);
47
50
  }
48
51
 
@@ -121,6 +121,8 @@ export function clearCompositionChildren(program) {
121
121
  export function materializeCompositionGraphics(program) {
122
122
  program._assertCompositionProgram("materializeCompositionGraphics");
123
123
  const layout = resolveCompositionProgramLayout(program);
124
+ const background = program.graphicSpec.objects.canvas?.properties
125
+ ?.background ?? "white";
124
126
  let next = clearCompositionChildren(program);
125
127
  if (next.graphicSpec.objects.canvas === undefined) {
126
128
  next = next.createGraphics({ id: "canvas", type: "canvas" });
@@ -128,7 +130,7 @@ export function materializeCompositionGraphics(program) {
128
130
  for (const [property, value] of Object.entries({
129
131
  width: layout.width,
130
132
  height: layout.height,
131
- background: "white"
133
+ background
132
134
  })) {
133
135
  next = next.editGraphics({ target: "canvas", property, value });
134
136
  }
@@ -0,0 +1,204 @@
1
+ import { isSourceOwnedText } from "../grammar/text.js";
2
+ import { resolvePolarFrame } from "../grammar/polar.js";
3
+ import { resolveEffectiveBounds } from "../layout/aspect.js";
4
+ import { resolveGraphicBounds } from "../layout/canvas.js";
5
+ import { findCoordinate, requireCoordinate } from "../selectors/coordinates.js";
6
+ import { findSemanticScale } from "../selectors/scales.js";
7
+
8
+ const DATA_CHANNELS = Object.freeze(["x", "y"]);
9
+ const SECONDARY_CHANNELS = Object.freeze({ x2: "x", y2: "y" });
10
+
11
+ function samePair(left, right) {
12
+ return left.x === right.x && left.y === right.y;
13
+ }
14
+
15
+ export function resolveDataAspectScalePair(program, target) {
16
+ const coordinate = typeof target === "string"
17
+ ? requireCoordinate(program, target)
18
+ : target;
19
+ if (coordinate.type !== "cartesian") {
20
+ throw new Error("Coordinate data aspect requires a Cartesian coordinate.");
21
+ }
22
+ const layers = program.semanticSpec.layers.filter(layer =>
23
+ layer.coordinate === coordinate.id &&
24
+ !isSourceOwnedText(layer) &&
25
+ DATA_CHANNELS.some(channel => layer.encoding?.[channel] !== undefined)
26
+ );
27
+ if (layers.length === 0) {
28
+ throw new Error(
29
+ `Coordinate "${coordinate.id}" data aspect requires active x and y consumers.`
30
+ );
31
+ }
32
+ const pairs = layers.map(layer => {
33
+ for (const channel of DATA_CHANNELS) {
34
+ const encoding = layer.encoding?.[channel];
35
+ if (
36
+ encoding?.scale === undefined ||
37
+ encoding.fieldType !== "quantitative"
38
+ ) {
39
+ throw new Error(
40
+ `Coordinate "${coordinate.id}" data aspect requires quantitative x and y scale consumers.`
41
+ );
42
+ }
43
+ }
44
+ for (const [secondary, primary] of Object.entries(SECONDARY_CHANNELS)) {
45
+ const encoding = layer.encoding?.[secondary];
46
+ if (encoding !== undefined && (
47
+ encoding.fieldType !== "quantitative" ||
48
+ encoding.scale !== layer.encoding[primary].scale
49
+ )) {
50
+ throw new Error(
51
+ `Coordinate "${coordinate.id}" data aspect requires ${secondary} to share its quantitative ${primary} scale.`
52
+ );
53
+ }
54
+ }
55
+ return { x: layer.encoding.x.scale, y: layer.encoding.y.scale };
56
+ });
57
+ if (pairs.some(pair => !samePair(pair, pairs[0]))) {
58
+ throw new Error(
59
+ `Coordinate "${coordinate.id}" data aspect requires one consistent x/y scale pair.`
60
+ );
61
+ }
62
+ const scales = {
63
+ x: findSemanticScale(program, pairs[0].x),
64
+ y: findSemanticScale(program, pairs[0].y)
65
+ };
66
+ for (const channel of DATA_CHANNELS) {
67
+ if (scales[channel]?.type !== "linear") {
68
+ throw new Error(
69
+ `Coordinate data aspect requires a linear ${channel} scale.`
70
+ );
71
+ }
72
+ }
73
+ return Object.freeze({
74
+ x: pairs[0].x,
75
+ y: pairs[0].y
76
+ });
77
+ }
78
+
79
+ export function findDataAspectCoordinatesForScale(program, scaleId) {
80
+ return program.semanticSpec.coordinates.filter(coordinate => {
81
+ if (coordinate.aspect?.mode !== "data") return false;
82
+ const pair = resolveDataAspectScalePair(program, coordinate);
83
+ return pair.x === scaleId || pair.y === scaleId;
84
+ });
85
+ }
86
+
87
+ function sameNumericPair(left, right) {
88
+ return Array.isArray(left) && left.length === 2 &&
89
+ left.every((value, index) => Number.isFinite(value) &&
90
+ Math.abs(value - right[index]) <= 1e-9);
91
+ }
92
+
93
+ function validateExplicitDataRanges(program, pair, bounds) {
94
+ const expected = {
95
+ x: [bounds.x, bounds.x + bounds.width],
96
+ y: [bounds.y + bounds.height, bounds.y]
97
+ };
98
+ for (const channel of DATA_CHANNELS) {
99
+ const scale = findSemanticScale(program, pair[channel]);
100
+ if (scale.range === "auto") continue;
101
+ const directed = scale.reverse === true
102
+ ? [...expected[channel]].reverse()
103
+ : expected[channel];
104
+ if (!sameNumericPair(scale.range, directed)) {
105
+ throw new Error(
106
+ `Coordinate data aspect conflicts with explicit ${channel} scale range "${scale.id}".`
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ export function resolveCoordinateBounds(program, target, {
113
+ resolvedScales = program.resolvedScales
114
+ } = {}) {
115
+ const allocated = resolveGraphicBounds(program);
116
+ if (target === undefined) return allocated;
117
+ const coordinate = requireCoordinate(program, target);
118
+ const aspect = coordinate.aspect;
119
+ if (aspect === undefined || aspect === "auto") return allocated;
120
+ if (aspect.mode !== "data") {
121
+ return resolveEffectiveBounds(allocated, aspect);
122
+ }
123
+ const pair = resolveDataAspectScalePair(program, coordinate);
124
+ const x = resolvedScales[pair.x];
125
+ const y = resolvedScales[pair.y];
126
+ if (x?.type !== "linear" || y?.type !== "linear") {
127
+ throw new Error(
128
+ `Coordinate "${coordinate.id}" data aspect requires resolved linear x and y scales.`
129
+ );
130
+ }
131
+ const bounds = resolveEffectiveBounds(allocated, aspect, {
132
+ x: x.domain,
133
+ y: y.domain
134
+ });
135
+ validateExplicitDataRanges(program, pair, bounds);
136
+ return bounds;
137
+ }
138
+
139
+ export function resolveCoordinatePolarFrame(program, target, options) {
140
+ const coordinate = requireCoordinate(program, target);
141
+ if (coordinate.type !== "polar") {
142
+ throw new Error(`Coordinate "${coordinate.id}" is not Polar.`);
143
+ }
144
+ return resolvePolarFrame(
145
+ resolveCoordinateBounds(program, coordinate.id, options),
146
+ coordinate.polarFrame
147
+ );
148
+ }
149
+
150
+ function consumerCoordinateIds(consumers) {
151
+ return [...new Set(consumers.map(consumer => consumer.layer.coordinate))];
152
+ }
153
+
154
+ function sameBounds(left, right) {
155
+ return ["x", "y", "width", "height"].every(
156
+ property => Math.abs(left[property] - right[property]) <= 1e-9
157
+ );
158
+ }
159
+
160
+ export function resolveScaleConsumerBounds(program, consumers, options) {
161
+ const coordinateIds = consumerCoordinateIds(consumers);
162
+ if (coordinateIds.length === 0) return resolveGraphicBounds(program);
163
+ const allocated = resolveGraphicBounds(program);
164
+ const bounds = coordinateIds.map(id =>
165
+ id === undefined || findCoordinate(program, id) === undefined
166
+ ? allocated
167
+ : resolveCoordinateBounds(program, id, options)
168
+ );
169
+ if (bounds.slice(1).some(candidate => !sameBounds(candidate, bounds[0]))) {
170
+ throw new Error(
171
+ "A position scale cannot be shared across coordinates with different effective bounds."
172
+ );
173
+ }
174
+ return bounds[0];
175
+ }
176
+
177
+ export function resolveGuideCoordinateBounds(program, {
178
+ coordinate,
179
+ scale,
180
+ channel,
181
+ resolvedScales = program.resolvedScales
182
+ }) {
183
+ let target = coordinate;
184
+ if (target === undefined && scale !== undefined) {
185
+ const candidates = new Set();
186
+ for (const layer of program.semanticSpec.layers) {
187
+ if (isSourceOwnedText(layer)) continue;
188
+ if (layer.encoding?.[channel]?.scale === scale) {
189
+ candidates.add(layer.coordinate);
190
+ }
191
+ }
192
+ if (candidates.has(undefined)) {
193
+ throw new Error("Guide scale consumers require stored coordinates.");
194
+ }
195
+ if (candidates.size > 1) {
196
+ throw new Error("Guide bounds found no unique compatible coordinate.");
197
+ }
198
+ target = candidates.values().next().value;
199
+ }
200
+ if (target !== undefined && findCoordinate(program, target) === undefined) {
201
+ throw new Error(`Unknown coordinate "${target}".`);
202
+ }
203
+ return resolveCoordinateBounds(program, target, { resolvedScales });
204
+ }
@@ -19,6 +19,7 @@ import {
19
19
  planScaleGuideRematerialization
20
20
  } from "./scaleGuideDependencies.js";
21
21
  import { planLayoutRematerialization } from "./layout.js";
22
+ import { requireCoordinate } from "../selectors/coordinates.js";
22
23
 
23
24
  export function planCanvasRematerialization(program) {
24
25
  const marks = [];
@@ -61,6 +62,41 @@ export function planCanvasRematerialization(program) {
61
62
  return buildMaterializationPlan({ scales, marks, guides, layout });
62
63
  }
63
64
 
65
+ export function planCoordinateRematerialization(program, target) {
66
+ requireCoordinate(program, target);
67
+ const layers = program.semanticSpec.layers.filter(
68
+ layer => layer.coordinate === target
69
+ );
70
+ const scaleIds = [...new Set(layers.flatMap(layer => [
71
+ ...["x", "y", "x2", "y2", "theta", "radius"].map(
72
+ channel => layer.encoding?.[channel]?.scale
73
+ ),
74
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(
75
+ dimension => dimension.scale
76
+ )
77
+ ]).filter(id => id !== undefined))];
78
+ const directMarks = layers
79
+ .map(layer => getMarkMaterializationStep(program, layer))
80
+ .filter(step => step !== undefined);
81
+ const marks = [
82
+ ...directMarks,
83
+ ...directMarks.flatMap(step =>
84
+ getSourceDependentMarkSteps(program, step.args.id)
85
+ )
86
+ ];
87
+ return buildMaterializationPlan({
88
+ scales: scaleIds.map(id => ({
89
+ op: "rematerializeScale",
90
+ args: { id, guides: false, marks: false }
91
+ })),
92
+ marks: reusePlannedMarkScales(program, marks, scaleIds),
93
+ guides: scaleIds.flatMap(id =>
94
+ planScaleGuideRematerialization(program, id)
95
+ ),
96
+ layout: planLayoutRematerialization(program)
97
+ });
98
+ }
99
+
64
100
  export function planLayerDataRematerialization(program, id) {
65
101
  const layer = requireLayer(program, id);
66
102
  const scaleIds = [...new Set(getLayerScaleIds(layer))];
@@ -29,6 +29,9 @@ function collection(program, id, items) {
29
29
  }
30
30
 
31
31
  export function resolveLegacyCategoricalLegend(program) {
32
+ if (program.semanticSpec.layers.some(layer =>
33
+ layer.encoding?.stroke?.scale !== undefined
34
+ )) return undefined;
32
35
  const encodings = program.semanticSpec.layers.flatMap(layer =>
33
36
  layer.encoding?.color === undefined
34
37
  ? []
@@ -46,10 +46,16 @@ function legendTranslation(prepared, plot, layout) {
46
46
  y: layout.legend.y - prepared.bounds.top
47
47
  };
48
48
  }
49
- const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
49
+ const gradientKind = prepared.kinds?.find(kind =>
50
+ ["gradient", "strokeGradient"].includes(kind)
51
+ );
52
+ const prefix = gradientKind === "strokeGradient"
53
+ ? "strokeGradient"
54
+ : "colorGradient";
55
+ const strips = prepared.source.graphicSpec.objects[`${prefix}Strips`];
50
56
  if (strips?.items?.[0] !== undefined) {
51
57
  const properties = strips.items[0].properties;
52
- const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
58
+ const length = prepared.source.guideConfigs.legend[gradientKind].gradient.length;
53
59
  return {
54
60
  x: layout.legend.x - (position === "left"
55
61
  ? prepared.bounds.left
@@ -86,7 +92,7 @@ function attachParentLegend(program, prepared, plot, layout) {
86
92
  }
87
93
 
88
94
  function assertSharedLegendFits(program, id, layout, plot) {
89
- const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id);
95
+ const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
90
96
  if (bounds === undefined) {
91
97
  throw new Error("Facet shared legend requires measurable concrete bounds.");
92
98
  }
@@ -35,7 +35,7 @@ function legendCandidates(program) {
35
35
  const layer = color[0];
36
36
  return {
37
37
  target: layer.id,
38
- channels: ["color", "shape", "strokeDash"].filter(
38
+ channels: ["color", "stroke", "shape", "strokeDash"].filter(
39
39
  channel => layer.encoding?.[channel]?.scale !== undefined
40
40
  )
41
41
  };
@@ -45,7 +45,7 @@ function legendCandidates(program) {
45
45
  "Shared facet legend requires one unambiguous color legend target."
46
46
  );
47
47
  }
48
- for (const channel of ["shape", "strokeDash", "size", "opacity"]) {
48
+ for (const channel of ["stroke", "shape", "strokeDash", "size", "opacity"]) {
49
49
  const candidates = layers.filter(
50
50
  layer => layer.encoding?.[channel]?.scale !== undefined
51
51
  );
@@ -105,9 +105,14 @@ function prepareAutoLegendSource(child) {
105
105
  }
106
106
  const scale = findSemanticScale(
107
107
  source,
108
- findLayer(source, request.target)?.encoding?.color?.scale
108
+ findLayer(source, request.target)?.encoding?.[
109
+ request.channels.includes("color") ? "color" : "stroke"
110
+ ]?.scale
109
111
  );
110
112
  const gradient = scale?.type === "sequential";
113
+ const gradientPrefix = request.channels.includes("color")
114
+ ? "colorGradient"
115
+ : "strokeGradient";
111
116
  source = source.createLegend({
112
117
  target: request.target,
113
118
  channels: request.channels,
@@ -122,7 +127,7 @@ function prepareAutoLegendSource(child) {
122
127
  const resolved = source.resolvedScales[scale.id];
123
128
  source = source
124
129
  .editGraphics({
125
- target: "colorGradientLabels",
130
+ target: `${gradientPrefix}Labels`,
126
131
  property: "text",
127
132
  value: formatDistinctNumericSamples(
128
133
  continuousValues(resolved.domain, 5),
@@ -130,9 +135,9 @@ function prepareAutoLegendSource(child) {
130
135
  )
131
136
  })
132
137
  .editGraphics({
133
- target: "colorGradientTitle",
138
+ target: `${gradientPrefix}Title`,
134
139
  property: "y",
135
- value: source.graphicSpec.objects.colorGradientTitle.properties.y + 8
140
+ value: source.graphicSpec.objects[`${gradientPrefix}Title`].properties.y + 8
136
141
  });
137
142
  }
138
143
  return source;
@@ -192,11 +197,11 @@ export function prepareSharedFacetLegend(program) {
192
197
  if (roots.length === 0) {
193
198
  throw new Error("Shared facet legend has no concrete guide graphics.");
194
199
  }
195
- const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
200
+ const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots, source.materializationConfigs.textMetrics);
196
201
  if (bounds === undefined) {
197
202
  throw new Error("Shared facet legend has no measurable concrete bounds.");
198
203
  }
199
- const gradient = kinds.includes("gradient");
204
+ const gradient = kinds.some(kind => ["gradient", "strokeGradient"].includes(kind));
200
205
  const placement = promotedLegendPlacement(source, kinds);
201
206
  const width = Math.ceil(bounds.right - bounds.left);
202
207
  return {