ggaction 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +10 -3
  11. package/src/actions/basic.js +4 -2
  12. package/src/actions/boxPlots/components.js +14 -4
  13. package/src/actions/boxPlots/edit.js +31 -5
  14. package/src/actions/boxPlots/materialize.js +13 -4
  15. package/src/actions/boxPlots/options.js +48 -16
  16. package/src/actions/canvas/actions.js +3 -1
  17. package/src/actions/canvas/fitting.js +211 -0
  18. package/src/actions/canvas/index.js +2 -0
  19. package/src/actions/categoryOrder/index.js +18 -6
  20. package/src/actions/charts/area.js +51 -0
  21. package/src/actions/charts/bar.js +13 -6
  22. package/src/actions/charts/beeswarm.js +70 -0
  23. package/src/actions/charts/density.js +48 -0
  24. package/src/actions/charts/ecdf.js +186 -0
  25. package/src/actions/charts/endpoints.js +473 -0
  26. package/src/actions/charts/heatmap.js +4 -3
  27. package/src/actions/charts/histogram.js +5 -4
  28. package/src/actions/charts/horizon.js +73 -0
  29. package/src/actions/charts/index.js +37 -0
  30. package/src/actions/charts/interval-regression.js +172 -0
  31. package/src/actions/charts/line.js +8 -6
  32. package/src/actions/charts/parallel.js +8 -2
  33. package/src/actions/charts/pie.js +52 -0
  34. package/src/actions/charts/polar.js +134 -0
  35. package/src/actions/charts/radar.js +305 -0
  36. package/src/actions/charts/radial.js +50 -0
  37. package/src/actions/charts/raincloud.js +422 -0
  38. package/src/actions/charts/rug-strip.js +283 -0
  39. package/src/actions/charts/scatter.js +11 -3
  40. package/src/actions/charts/shared.js +69 -10
  41. package/src/actions/composition/actions.js +129 -3
  42. package/src/actions/coordinates/actions.js +1 -1
  43. package/src/actions/coordinates/edit.js +76 -0
  44. package/src/actions/data/bin.js +29 -0
  45. package/src/actions/data/bin2d.js +57 -185
  46. package/src/actions/data/complete.js +26 -0
  47. package/src/actions/data/computed.js +24 -0
  48. package/src/actions/data/create.js +2 -2
  49. package/src/actions/data/density.js +12 -20
  50. package/src/actions/data/derived.js +111 -15
  51. package/src/actions/data/ecdf.js +38 -0
  52. package/src/actions/data/edit.js +539 -0
  53. package/src/actions/data/filter.js +226 -30
  54. package/src/actions/data/fold.js +24 -0
  55. package/src/actions/data/impute.js +26 -0
  56. package/src/actions/data/index.js +38 -0
  57. package/src/actions/data/interval.js +2 -0
  58. package/src/actions/data/intervalEdit.js +86 -4
  59. package/src/actions/data/normalize.js +27 -0
  60. package/src/actions/data/regression.js +19 -21
  61. package/src/actions/data/revisionBindings.js +145 -0
  62. package/src/actions/data/shared.js +20 -4
  63. package/src/actions/data/stack.js +26 -0
  64. package/src/actions/data/summary.js +26 -0
  65. package/src/actions/data/timeUnit.js +4 -1
  66. package/src/actions/data/window.js +1 -1
  67. package/src/actions/encodings/appearance.js +32 -13
  68. package/src/actions/encodings/barWidth.js +8 -0
  69. package/src/actions/encodings/basic.js +2 -0
  70. package/src/actions/encodings/channels.js +415 -0
  71. package/src/actions/encodings/color/continuous.js +19 -59
  72. package/src/actions/encodings/color/index.js +30 -23
  73. package/src/actions/encodings/color/layout.js +3 -97
  74. package/src/actions/encodings/color/policy.js +5 -8
  75. package/src/actions/encodings/density/resolve.js +37 -5
  76. package/src/actions/encodings/density.js +124 -34
  77. package/src/actions/encodings/histogram.js +3 -2
  78. package/src/actions/encodings/horizon/resolve.js +8 -4
  79. package/src/actions/encodings/horizon.js +18 -8
  80. package/src/actions/encodings/index.js +6 -0
  81. package/src/actions/encodings/offset.js +30 -29
  82. package/src/actions/encodings/parallel.js +2 -8
  83. package/src/actions/encodings/position/apply.js +63 -11
  84. package/src/actions/encodings/position/index.js +52 -6
  85. package/src/actions/encodings/position/policies/arc.js +14 -0
  86. package/src/actions/encodings/position/policies/area.js +4 -42
  87. package/src/actions/encodings/position/policies/bar.js +57 -13
  88. package/src/actions/encodings/position/resolve.js +88 -26
  89. package/src/actions/encodings/ranged.js +123 -97
  90. package/src/actions/encodings/remove.js +44 -34
  91. package/src/actions/encodings/ruleAppearance.js +24 -32
  92. package/src/actions/encodings/seriesLayout.js +105 -0
  93. package/src/actions/encodings/shared.js +107 -37
  94. package/src/actions/encodings/stroke.js +226 -0
  95. package/src/actions/encodings/strokeDash.js +9 -6
  96. package/src/actions/encodings/temporal.js +7 -0
  97. package/src/actions/encodings/text.js +30 -15
  98. package/src/actions/errorBands/create.js +41 -8
  99. package/src/actions/errorBands/edit.js +360 -25
  100. package/src/actions/errorBars/create.js +56 -17
  101. package/src/actions/errorBars/edit.js +372 -21
  102. package/src/actions/facets/actions.js +619 -41
  103. package/src/actions/facets/derive.js +139 -25
  104. package/src/actions/gradientPlots/components.js +43 -13
  105. package/src/actions/gradientPlots/edit.js +14 -4
  106. package/src/actions/gradientPlots/materialize.js +8 -8
  107. package/src/actions/gradientPlots/options.js +10 -2
  108. package/src/actions/guides/applicability.js +28 -19
  109. package/src/actions/guides/axes/axes.js +4 -3
  110. package/src/actions/guides/axes/axis.js +18 -18
  111. package/src/actions/guides/axes/components.js +43 -0
  112. package/src/actions/guides/axes/edit.js +16 -51
  113. package/src/actions/guides/axes/labels.js +184 -28
  114. package/src/actions/guides/axes/lines.js +13 -7
  115. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  116. package/src/actions/guides/axes/parallel/policy.js +137 -0
  117. package/src/actions/guides/axes/parallel/resolve.js +50 -25
  118. package/src/actions/guides/axes/parallel.js +93 -217
  119. package/src/actions/guides/axes/policy.js +21 -67
  120. package/src/actions/guides/axes/tickGroups.js +15 -8
  121. package/src/actions/guides/axes/ticks.js +11 -6
  122. package/src/actions/guides/axes/titles.js +32 -11
  123. package/src/actions/guides/basic.js +2 -0
  124. package/src/actions/guides/facade.js +106 -0
  125. package/src/actions/guides/facadeAxes.js +200 -0
  126. package/src/actions/guides/facadeLegend.js +63 -0
  127. package/src/actions/guides/grids/grid.js +37 -33
  128. package/src/actions/guides/grids/resolve.js +17 -4
  129. package/src/actions/guides/guides.js +54 -31
  130. package/src/actions/guides/legends/blocks.js +426 -0
  131. package/src/actions/guides/legends/categorical/actions.js +309 -279
  132. package/src/actions/guides/legends/categorical/components.js +21 -17
  133. package/src/actions/guides/legends/categorical/index.js +37 -33
  134. package/src/actions/guides/legends/categorical/layout.js +123 -354
  135. package/src/actions/guides/legends/categorical/options.js +32 -16
  136. package/src/actions/guides/legends/categorical/recipes.js +114 -9
  137. package/src/actions/guides/legends/categorical/resolve.js +30 -15
  138. package/src/actions/guides/legends/categorical/symbols.js +87 -33
  139. package/src/actions/guides/legends/continuous/common.js +99 -26
  140. package/src/actions/guides/legends/continuous/gradient.js +120 -89
  141. package/src/actions/guides/legends/continuous/index.js +8 -2
  142. package/src/actions/guides/legends/continuous/interval.js +79 -95
  143. package/src/actions/guides/legends/continuous/opacity.js +113 -89
  144. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  145. package/src/actions/guides/legends/creation.js +40 -0
  146. package/src/actions/guides/legends/edit.js +321 -90
  147. package/src/actions/guides/legends/focused.js +2 -2
  148. package/src/actions/guides/legends/index.js +2 -0
  149. package/src/actions/guides/legends/lane.js +149 -102
  150. package/src/actions/guides/legends/lifecycle.js +143 -0
  151. package/src/actions/guides/legends/remove.js +43 -78
  152. package/src/actions/guides/legends/sampling.js +140 -0
  153. package/src/actions/guides/legends/size.js +213 -116
  154. package/src/actions/guides/legends/strokeWidth.js +121 -73
  155. package/src/actions/guides/legends/target.js +123 -0
  156. package/src/actions/guides/legends/transition.js +192 -0
  157. package/src/actions/guides/polar/axes/facade.js +81 -66
  158. package/src/actions/guides/polar/axes/labels.js +28 -8
  159. package/src/actions/guides/polar/axes/lines.js +12 -5
  160. package/src/actions/guides/polar/axes/shared.js +22 -0
  161. package/src/actions/guides/polar/axes/ticks.js +6 -5
  162. package/src/actions/guides/polar/axes/titles.js +6 -5
  163. package/src/actions/guides/polar/grids.js +8 -8
  164. package/src/actions/guides/polar/resolve.js +12 -8
  165. package/src/actions/guides/reuse.js +44 -0
  166. package/src/actions/guides/tickValues.js +19 -21
  167. package/src/actions/index.js +15 -2
  168. package/src/actions/intervals/resolve.js +35 -15
  169. package/src/actions/marks/arc/actions.js +78 -20
  170. package/src/actions/marks/area/actions.js +51 -12
  171. package/src/actions/marks/area/materialize.js +22 -3
  172. package/src/actions/marks/bar/create.js +11 -2
  173. package/src/actions/marks/bar/edit.js +20 -3
  174. package/src/actions/marks/bar/materialize.js +33 -9
  175. package/src/actions/marks/index.js +14 -0
  176. package/src/actions/marks/line/actions.js +71 -113
  177. package/src/actions/marks/line/materialize.js +57 -56
  178. package/src/actions/marks/point/create.js +8 -2
  179. package/src/actions/marks/point/edit.js +29 -21
  180. package/src/actions/marks/point/index.js +1 -0
  181. package/src/actions/marks/point/jitter.js +3 -0
  182. package/src/actions/marks/point/materialize.js +154 -16
  183. package/src/actions/marks/point/packing.js +68 -0
  184. package/src/actions/marks/rect/actions.js +33 -12
  185. package/src/actions/marks/references.js +658 -0
  186. package/src/actions/marks/remove.js +87 -4
  187. package/src/actions/marks/rule/actions.js +77 -9
  188. package/src/actions/marks/rule/appearance.js +26 -0
  189. package/src/actions/marks/shared.js +13 -13
  190. package/src/actions/marks/text/actions.js +296 -13
  191. package/src/actions/marks/text/index.js +4 -0
  192. package/src/actions/marks/text/layout.js +156 -8
  193. package/src/actions/marks/text/removal.js +173 -0
  194. package/src/actions/marks/text/selection.js +73 -0
  195. package/src/actions/marks/tick/actions.js +22 -5
  196. package/src/actions/primitives/createGraphics.js +3 -1
  197. package/src/actions/primitives/editGraphics.js +26 -0
  198. package/src/actions/primitives/semantic.js +3 -2
  199. package/src/actions/primitives/semanticAction.js +120 -10
  200. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  201. package/src/actions/primitives/semanticValidation/index.js +9 -1
  202. package/src/actions/primitives/semanticValidation/layer.js +23 -1
  203. package/src/actions/primitives/semanticValidation/scale.js +38 -8
  204. package/src/actions/regression/components.js +26 -11
  205. package/src/actions/regression/create.js +11 -3
  206. package/src/actions/regression/edit.js +40 -13
  207. package/src/actions/regression/resolve.js +1 -1
  208. package/src/actions/resources/index.js +7 -0
  209. package/src/actions/resources/remove.js +172 -0
  210. package/src/actions/scales/channels.js +212 -0
  211. package/src/actions/scales/consumers/common.js +21 -54
  212. package/src/actions/scales/consumers/families.js +28 -14
  213. package/src/actions/scales/consumers/index.js +42 -4
  214. package/src/actions/scales/consumers/seriesLayout.js +57 -12
  215. package/src/actions/scales/create.js +58 -26
  216. package/src/actions/scales/definitions.js +109 -39
  217. package/src/actions/scales/edit.js +101 -34
  218. package/src/actions/scales/editPolicy.js +95 -35
  219. package/src/actions/scales/index.js +6 -0
  220. package/src/actions/scales/materialize.js +14 -49
  221. package/src/actions/scales/offset.js +48 -0
  222. package/src/actions/scales/parallel.js +71 -0
  223. package/src/actions/scales/patch.js +1 -1
  224. package/src/actions/scales/preview.js +140 -0
  225. package/src/actions/scales/quantitativeColor.js +9 -5
  226. package/src/actions/selection/actions.js +154 -30
  227. package/src/actions/selection/index.js +3 -0
  228. package/src/actions/theme/actions.js +98 -0
  229. package/src/actions/theme/composition.js +238 -0
  230. package/src/actions/theme/index.js +16 -0
  231. package/src/actions/theme/reconcile.js +1130 -0
  232. package/src/actions/theme/state.js +160 -0
  233. package/src/actions/titles/actions.js +11 -10
  234. package/src/actions/titles/resolve.js +1 -40
  235. package/src/actions/violinPlots/create.js +90 -43
  236. package/src/actions/violinPlots/edit.js +192 -0
  237. package/src/actions/violinPlots/index.js +2 -0
  238. package/src/core/ChartProgram.js +7 -0
  239. package/src/core/action.js +32 -2
  240. package/src/core/compositionState.js +141 -8
  241. package/src/core/programState.js +20 -0
  242. package/src/core/resourceReferences.js +332 -0
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/grammar/aggregate.js +66 -23
  245. package/src/grammar/arcs.js +58 -1
  246. package/src/grammar/areaEndpoints.js +46 -0
  247. package/src/grammar/areaSeries.js +87 -41
  248. package/src/grammar/bars/aggregate.js +37 -13
  249. package/src/grammar/bars/geometry.js +44 -0
  250. package/src/grammar/bars/policy.js +9 -7
  251. package/src/grammar/bin.js +226 -0
  252. package/src/grammar/categoryOrder.js +34 -0
  253. package/src/grammar/complete.js +279 -0
  254. package/src/grammar/computed.js +351 -0
  255. package/src/grammar/coordinates.js +37 -0
  256. package/src/grammar/density.js +239 -37
  257. package/src/grammar/displayLabels.js +57 -0
  258. package/src/grammar/ecdf.js +198 -0
  259. package/src/grammar/facets/dependencies.js +133 -1
  260. package/src/grammar/facets/guides.js +3 -0
  261. package/src/grammar/facets/index.js +123 -52
  262. package/src/grammar/facets/scales.js +15 -18
  263. package/src/grammar/fold.js +123 -0
  264. package/src/grammar/horizon.js +6 -4
  265. package/src/grammar/impute.js +340 -0
  266. package/src/grammar/interval.js +37 -20
  267. package/src/grammar/lineSeries.js +20 -63
  268. package/src/grammar/markFilter.js +22 -4
  269. package/src/grammar/markLabelSelection.js +57 -0
  270. package/src/grammar/markLabels.js +110 -0
  271. package/src/grammar/normalize.js +366 -0
  272. package/src/grammar/parallelAxisTitles.js +16 -0
  273. package/src/grammar/pathSeries.js +109 -0
  274. package/src/grammar/pointPacking.js +183 -0
  275. package/src/grammar/pointShapes.js +13 -4
  276. package/src/grammar/polar.js +90 -8
  277. package/src/grammar/positionDatum.js +12 -0
  278. package/src/grammar/rects.js +9 -1
  279. package/src/grammar/regression/derive.js +4 -0
  280. package/src/grammar/regression/index.js +8 -2
  281. package/src/grammar/regression/models.js +22 -7
  282. package/src/grammar/regression/parameters.js +63 -13
  283. package/src/grammar/rotation.js +29 -0
  284. package/src/grammar/roundedRect.js +180 -0
  285. package/src/grammar/rules.js +8 -21
  286. package/src/grammar/scales/color.js +23 -1
  287. package/src/grammar/scales/colorConsumers.js +57 -0
  288. package/src/grammar/scales/continuous.js +2 -2
  289. package/src/grammar/scales/definition.js +42 -5
  290. package/src/grammar/scales/discretized.js +11 -5
  291. package/src/grammar/scales/fields.js +38 -8
  292. package/src/grammar/scales/index.js +1 -0
  293. package/src/grammar/scales/mapping.js +2 -0
  294. package/src/grammar/scales/ordinal.js +12 -3
  295. package/src/grammar/scales/policies.js +2 -2
  296. package/src/grammar/scales/radial.js +55 -0
  297. package/src/grammar/scales/size.js +456 -0
  298. package/src/grammar/scales/types.js +1 -0
  299. package/src/grammar/schemas/concreteGraphic.js +14 -2
  300. package/src/grammar/schemas/graphic.js +15 -0
  301. package/src/grammar/schemas/graphicBounds.js +179 -30
  302. package/src/grammar/schemas/semanticPath.js +23 -4
  303. package/src/grammar/stack.js +170 -0
  304. package/src/grammar/statisticalReference.js +103 -0
  305. package/src/grammar/statistics/confidenceInterval.js +101 -0
  306. package/src/grammar/strokeStyle.js +77 -0
  307. package/src/grammar/summary.js +245 -0
  308. package/src/grammar/text.js +22 -16
  309. package/src/grammar/timeUnit.js +234 -13
  310. package/src/grammar/transformTopology.js +10 -0
  311. package/src/grammar/transforms.js +418 -16
  312. package/src/grammar/valueFormat.js +112 -0
  313. package/src/grammar/weightedStatistics.js +374 -0
  314. package/src/grammar/window.js +232 -21
  315. package/src/layout/aspect.js +80 -0
  316. package/src/layout/facets.js +100 -12
  317. package/src/layout/guideCollisions.js +12 -0
  318. package/src/layout/labels.js +343 -0
  319. package/src/layout/legend.js +2 -10
  320. package/src/layout/legendItems.js +102 -0
  321. package/src/layout/legendLane.js +100 -11
  322. package/src/materialization/bars/aggregate.js +56 -147
  323. package/src/materialization/bars/histogram.js +104 -20
  324. package/src/materialization/bars/ranged.js +19 -3
  325. package/src/materialization/bars/resolve.js +7 -4
  326. package/src/materialization/categorySlotOffset.js +40 -0
  327. package/src/materialization/composition.js +3 -1
  328. package/src/materialization/compositionSnapshot.js +1 -1
  329. package/src/materialization/coordinateBounds.js +204 -0
  330. package/src/materialization/dependencies.js +93 -16
  331. package/src/materialization/encodings.js +1 -1
  332. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  333. package/src/materialization/facetGuides/placement.js +8 -2
  334. package/src/materialization/facetGuides/preparation.js +12 -7
  335. package/src/materialization/facetHeaders.js +346 -0
  336. package/src/materialization/facets.js +23 -66
  337. package/src/materialization/guides/layout.js +57 -0
  338. package/src/materialization/guides/resources.js +28 -2
  339. package/src/materialization/legends.js +11 -2
  340. package/src/materialization/marks/capabilities.js +20 -5
  341. package/src/materialization/marks/grouping.js +52 -0
  342. package/src/materialization/marks/index.js +68 -27
  343. package/src/materialization/marks/policies.js +4 -1
  344. package/src/materialization/rect.js +55 -42
  345. package/src/materialization/rectConfig.js +5 -1
  346. package/src/materialization/rowEncoding.js +13 -13
  347. package/src/materialization/scaleGuideDependencies.js +8 -5
  348. package/src/materialization/scales/map.js +6 -3
  349. package/src/materialization/scales/policies/arc.js +42 -0
  350. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  351. package/src/materialization/scales/policies/offset.js +50 -8
  352. package/src/materialization/scales/policies/series.js +5 -1
  353. package/src/materialization/scales/resolve.js +65 -17
  354. package/src/materialization/selection/compatibility.js +11 -0
  355. package/src/materialization/selection/items/arc.js +28 -4
  356. package/src/materialization/selection/items/bar.js +42 -71
  357. package/src/materialization/selection/items/common.js +27 -6
  358. package/src/materialization/selection/items/path.js +3 -13
  359. package/src/materialization/selection/items/rect.js +35 -8
  360. package/src/materialization/selection/path.js +1 -1
  361. package/src/materialization/selection/point.js +3 -1
  362. package/src/materialization/text.js +324 -14
  363. package/src/mcp/adapter.js +2 -2
  364. package/src/renderers/canvas/circle.js +2 -3
  365. package/src/renderers/canvas/line.js +2 -3
  366. package/src/renderers/canvas/native.js +4 -4
  367. package/src/renderers/canvas/path.js +2 -3
  368. package/src/renderers/canvas/rect.js +2 -3
  369. package/src/renderers/canvas/stroke.js +11 -0
  370. package/src/renderers/svg.js +19 -0
  371. package/src/selectors/datasets.js +44 -0
  372. package/src/selectors/layers.js +7 -2
  373. package/src/selectors/scales.js +6 -0
  374. package/src/theme/defaults.js +140 -0
  375. package/types/basic.d.ts +17 -2
  376. package/types/index.d.ts +211 -0
  377. package/types/program.d.ts +2182 -204
  378. package/src/materialization/bars/grouped.js +0 -144
@@ -0,0 +1,346 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ formatVisibleText,
4
+ measureTextWidth,
5
+ resolveTextBounds,
6
+ textBoundsIntersect
7
+ } from "../core/textMetrics.js";
8
+ import { resolveDisplayLabel } from "../grammar/displayLabels.js";
9
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
10
+
11
+ const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
12
+ const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
13
+
14
+ export function createDefaultFacetHeaders() {
15
+ return {
16
+ mode: "legacy",
17
+ common: {
18
+ fontSize: 12,
19
+ fontFamily: DEFAULT_FONT_FAMILY,
20
+ fontWeight: 600,
21
+ color: DEFAULT_COLORS.strongText,
22
+ offset: 10,
23
+ align: "center"
24
+ },
25
+ row: { side: "left" },
26
+ column: { side: "top" }
27
+ };
28
+ }
29
+
30
+ export function normalizeFacetHeadersConfig(value) {
31
+ if (!isPlainObject(value)) {
32
+ throw new TypeError("Facet headers config must be a plain object.");
33
+ }
34
+ if (value.common === undefined && value.mode === undefined) {
35
+ return {
36
+ ...createDefaultFacetHeaders(),
37
+ common: {
38
+ ...createDefaultFacetHeaders().common,
39
+ ...value
40
+ }
41
+ };
42
+ }
43
+ if (!["legacy", "roles"].includes(value.mode) ||
44
+ !isPlainObject(value.common) || !isPlainObject(value.row) ||
45
+ !isPlainObject(value.column)) {
46
+ throw new Error("Facet headers config must contain canonical mode and role objects.");
47
+ }
48
+ return {
49
+ mode: value.mode,
50
+ common: { ...value.common },
51
+ row: { ...value.row },
52
+ column: { ...value.column }
53
+ };
54
+ }
55
+
56
+ export function resolveFacetHeaderConfig(headers, role) {
57
+ const normalized = normalizeFacetHeadersConfig(headers);
58
+ return role === "all"
59
+ ? { ...normalized.common }
60
+ : { ...normalized.common, ...normalized[role] };
61
+ }
62
+
63
+ function mappedText(value, config) {
64
+ return resolveDisplayLabel(value, config.labelMap, formatVisibleText);
65
+ }
66
+
67
+ function gridCellById(spec) {
68
+ return new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
69
+ }
70
+
71
+ function legacyDescriptors(program, headers) {
72
+ const spec = program.compositionSpec;
73
+ const config = resolveFacetHeaderConfig(headers, "all");
74
+ const cells = gridCellById(spec);
75
+ return spec.children.map((id, index) => {
76
+ const cell = cells.get(id);
77
+ const text = cell === undefined
78
+ ? mappedText(spec.facet.values[index], config)
79
+ : `${mappedText(cell.rowValue, config)} · ${mappedText(cell.columnValue, config)}`;
80
+ return { topology: "legacy", role: "column", id, text, config };
81
+ });
82
+ }
83
+
84
+ function uniqueGridGroups(spec, role) {
85
+ const coordinate = role === "column" ? "column" : "row";
86
+ const valueKey = role === "column" ? "columnValue" : "rowValue";
87
+ const groups = new Map();
88
+ for (const cell of spec.facet.grid.cells) {
89
+ const existing = groups.get(cell[coordinate]);
90
+ if (existing === undefined) {
91
+ groups.set(cell[coordinate], {
92
+ coordinate: cell[coordinate],
93
+ value: cell[valueKey],
94
+ ids: [cell.id]
95
+ });
96
+ } else {
97
+ if (existing.value !== cell[valueKey]) {
98
+ throw new Error(`Facet ${role} header values conflict at coordinate ${cell[coordinate]}.`);
99
+ }
100
+ existing.ids.push(cell.id);
101
+ }
102
+ }
103
+ return [...groups.values()].sort((left, right) =>
104
+ left.coordinate - right.coordinate
105
+ );
106
+ }
107
+
108
+ function roleDescriptors(program, headers) {
109
+ const spec = program.compositionSpec;
110
+ const column = resolveFacetHeaderConfig(headers, "column");
111
+ if (spec.facet.grid === undefined) {
112
+ return spec.children.map((id, index) => ({
113
+ topology: "cell-column",
114
+ role: "column",
115
+ id,
116
+ ids: [id],
117
+ row: Math.floor(index / spec.columns),
118
+ text: mappedText(spec.facet.values[index], column),
119
+ config: column
120
+ }));
121
+ }
122
+ const row = resolveFacetHeaderConfig(headers, "row");
123
+ const columns = uniqueGridGroups(spec, "column").map(group => ({
124
+ topology: "grid-column",
125
+ role: "column",
126
+ ids: group.ids,
127
+ coordinate: group.coordinate,
128
+ text: mappedText(group.value, column),
129
+ config: column
130
+ }));
131
+ const rows = uniqueGridGroups(spec, "row").map(group => ({
132
+ topology: "grid-row",
133
+ role: "row",
134
+ ids: group.ids,
135
+ coordinate: group.coordinate,
136
+ text: mappedText(group.value, row),
137
+ config: row
138
+ }));
139
+ return [...columns, ...rows];
140
+ }
141
+
142
+ function laneThickness(descriptors, role) {
143
+ const visible = descriptors.filter(item => item.role === role && item.text !== "");
144
+ if (visible.length === 0) return 0;
145
+ if (role === "column") {
146
+ return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
147
+ }
148
+ return Math.max(...visible.map(item => measureTextWidth(item.text, item.config) +
149
+ item.config.offset));
150
+ }
151
+
152
+ export function prepareFacetHeaders(program, headersConfig) {
153
+ const headers = normalizeFacetHeadersConfig(headersConfig);
154
+ const descriptors = headers.mode === "legacy"
155
+ ? legacyDescriptors(program, headers)
156
+ : roleDescriptors(program, headers);
157
+ const rows = program.compositionSpec.facet.grid === undefined
158
+ ? Math.ceil(program.compositionSpec.children.length / program.compositionSpec.columns)
159
+ : Math.max(...program.compositionSpec.facet.grid.cells.map(cell => cell.row)) + 1;
160
+ const headerLayout = {
161
+ outer: { ...ZERO_SIDES },
162
+ cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
163
+ };
164
+ if (headers.mode === "roles") {
165
+ const column = resolveFacetHeaderConfig(headers, "column");
166
+ const columnThickness = laneThickness(descriptors, "column");
167
+ if (program.compositionSpec.facet.grid === undefined) {
168
+ for (const descriptor of descriptors) {
169
+ if (descriptor.text !== "") {
170
+ headerLayout.cellRows[column.side][descriptor.row] = Math.max(
171
+ headerLayout.cellRows[column.side][descriptor.row],
172
+ descriptor.config.fontSize + descriptor.config.offset
173
+ );
174
+ }
175
+ }
176
+ } else {
177
+ headerLayout.outer[column.side] = columnThickness;
178
+ const row = resolveFacetHeaderConfig(headers, "row");
179
+ headerLayout.outer[row.side] = laneThickness(descriptors, "row");
180
+ }
181
+ }
182
+ return { headers, descriptors, headerLayout };
183
+ }
184
+
185
+ function horizontalAnchor(bounds, align) {
186
+ if (align === "start") return { x: bounds.left, textAlign: "left" };
187
+ if (align === "end") return { x: bounds.right, textAlign: "right" };
188
+ return { x: (bounds.left + bounds.right) / 2, textAlign: "center" };
189
+ }
190
+
191
+ function verticalAnchor(bounds, align) {
192
+ if (align === "start") return { y: bounds.top, textBaseline: "top" };
193
+ if (align === "end") return { y: bounds.bottom, textBaseline: "bottom" };
194
+ return { y: (bounds.top + bounds.bottom) / 2, textBaseline: "middle" };
195
+ }
196
+
197
+ function union(items) {
198
+ return {
199
+ left: Math.min(...items.map(item => item.left)),
200
+ right: Math.max(...items.map(item => item.right)),
201
+ top: Math.min(...items.map(item => item.top)),
202
+ bottom: Math.max(...items.map(item => item.bottom))
203
+ };
204
+ }
205
+
206
+ function placedPlot(cell, plot) {
207
+ return {
208
+ left: cell.x + plot.x,
209
+ right: cell.x + plot.x + plot.width,
210
+ top: cell.y + plot.y,
211
+ bottom: cell.y + plot.y + plot.height
212
+ };
213
+ }
214
+
215
+ function placedCell(cell) {
216
+ return {
217
+ left: cell.x,
218
+ right: cell.x + cell.width,
219
+ top: cell.y,
220
+ bottom: cell.y + cell.height
221
+ };
222
+ }
223
+
224
+ function roleItem(descriptor, cells, plots) {
225
+ const selectedCells = descriptor.ids.map(id => cells.get(id));
226
+ const missingCell = descriptor.ids.find((id, index) =>
227
+ selectedCells[index] === undefined
228
+ );
229
+ const missingPlot = descriptor.ids.find(id => plots.get(id) === undefined);
230
+ if (missingCell !== undefined || missingPlot !== undefined) {
231
+ throw new Error(
232
+ `Facet header requires cell and plot bounds for "${missingCell ?? missingPlot}".`
233
+ );
234
+ }
235
+ const selectedPlots = descriptor.ids.map((id, index) =>
236
+ placedPlot(selectedCells[index], plots.get(id))
237
+ );
238
+ const plot = union(selectedPlots);
239
+ const snapshots = union(selectedCells.map(placedCell));
240
+ const config = descriptor.config;
241
+ let anchor;
242
+ if (descriptor.role === "column") {
243
+ anchor = horizontalAnchor(plot, config.align);
244
+ const top = config.side === "top";
245
+ anchor.y = top
246
+ ? snapshots.top - config.offset - config.fontSize / 2
247
+ : snapshots.bottom + config.offset + config.fontSize / 2;
248
+ anchor.textBaseline = "middle";
249
+ } else {
250
+ anchor = verticalAnchor(plot, config.align);
251
+ const left = config.side === "left";
252
+ anchor.x = left
253
+ ? snapshots.left - config.offset
254
+ : snapshots.right + config.offset;
255
+ anchor.textAlign = left ? "right" : "left";
256
+ }
257
+ return { descriptor, snapshots, properties: anchor };
258
+ }
259
+
260
+ function legacyItem(descriptor, cell, plot) {
261
+ if (cell === undefined || plot === undefined) {
262
+ throw new Error(`Facet header requires cell and plot bounds for "${descriptor.id}".`);
263
+ }
264
+ return {
265
+ descriptor,
266
+ snapshots: placedCell(cell),
267
+ properties: {
268
+ ...horizontalAnchor(placedPlot(cell, plot), descriptor.config.align),
269
+ y: cell.y + descriptor.config.offset,
270
+ textBaseline: "middle"
271
+ }
272
+ };
273
+ }
274
+
275
+ function styledItem(item) {
276
+ const { config } = item.descriptor;
277
+ return {
278
+ type: "text",
279
+ properties: {
280
+ ...item.properties,
281
+ text: item.descriptor.text,
282
+ fill: config.color,
283
+ fontSize: config.fontSize,
284
+ fontFamily: config.fontFamily,
285
+ fontWeight: config.fontWeight
286
+ }
287
+ };
288
+ }
289
+
290
+ function assertRoleHeadersFit(records, items, layout) {
291
+ const bounds = items.map((item, index) => records[index].descriptor.text === ""
292
+ ? undefined
293
+ : resolveTextBounds(item.properties));
294
+ for (const [index, current] of bounds.entries()) {
295
+ if (current === undefined) continue;
296
+ if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
297
+ current.top < -1e-9 || current.bottom > layout.height + 1e-9 ||
298
+ bounds.some((other, otherIndex) => otherIndex < index &&
299
+ other !== undefined && textBoundsIntersect(other, current)) ||
300
+ layout.children.some(cell => textBoundsIntersect(current, placedCell(cell)))) {
301
+ throw new Error(
302
+ "Facet role headers require sufficient non-overlapping reserved space."
303
+ );
304
+ }
305
+ }
306
+ }
307
+
308
+ function assertLegacyHeadersFit(items, layout, plotById) {
309
+ const previous = [];
310
+ for (let index = 0; index < items.length; index += 1) {
311
+ if (items[index].properties.text === "") continue;
312
+ const bounds = resolveTextBounds(items[index].properties);
313
+ const cell = layout.children[index];
314
+ const plot = plotById.get(cell.id);
315
+ if (bounds.left < 0 || bounds.right > layout.width ||
316
+ bounds.top < 0 || bounds.bottom > layout.height ||
317
+ previous.some(item => textBoundsIntersect(item, bounds)) ||
318
+ textBoundsIntersect(bounds, placedPlot(cell, plot))) {
319
+ throw new Error(
320
+ "Facet headers require sufficient non-overlapping space above every child plot."
321
+ );
322
+ }
323
+ previous.push(bounds);
324
+ }
325
+ }
326
+
327
+ export function materializeFacetHeaders(program, layout, plots, prepared) {
328
+ const cells = new Map(layout.children.map(cell => [cell.id, cell]));
329
+ const plotById = new Map(plots.map(plot => [plot.id, plot]));
330
+ const records = prepared.descriptors.map(descriptor => {
331
+ if (descriptor.topology === "legacy") {
332
+ return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
333
+ }
334
+ return roleItem(descriptor, cells, plotById);
335
+ });
336
+ const items = records.map(styledItem);
337
+ if (prepared.headers.mode === "legacy") {
338
+ assertLegacyHeadersFit(items, layout, plotById);
339
+ } else {
340
+ assertRoleHeadersFit(records, items, layout);
341
+ }
342
+ const id = `${program.compositionSpec.id}-headers`;
343
+ return program
344
+ .createGraphics({ id, type: "collection", parent: "canvas" })
345
+ .editGraphics({ target: id, property: "items", value: items });
346
+ }
@@ -1,9 +1,4 @@
1
1
  import { resolveGraphicBounds } from "../layout/canvas.js";
2
- import {
3
- formatVisibleText,
4
- resolveTextBounds,
5
- textBoundsIntersect
6
- } from "../core/textMetrics.js";
7
2
  import { resolvePlacedPlotBounds } from "../layout/composition.js";
8
3
  import {
9
4
  alignedTextAnchor,
@@ -17,6 +12,10 @@ import {
17
12
  clearCompositionChildren,
18
13
  compositionChildDescriptor
19
14
  } from "./composition.js";
15
+ import {
16
+ materializeFacetHeaders,
17
+ prepareFacetHeaders
18
+ } from "./facetHeaders.js";
20
19
 
21
20
  const ZERO_MARGIN = { top: 0, right: 0, bottom: 0, left: 0 };
22
21
 
@@ -50,61 +49,6 @@ function titleLayout(program) {
50
49
  return { config, block, top, height: Math.ceil(top + block.height) };
51
50
  }
52
51
 
53
- function collection(program, id, items) {
54
- return program
55
- .createGraphics({ id, type: "collection", parent: "canvas" })
56
- .editGraphics({ target: id, property: "items", value: items });
57
- }
58
-
59
- function assertFacetHeadersFit(items, layout, plotById) {
60
- const previous = [];
61
- for (let index = 0; index < items.length; index += 1) {
62
- const bounds = resolveTextBounds(items[index].properties);
63
- const cell = layout.children[index];
64
- const plot = plotById.get(cell.id);
65
- if (bounds.left < 0 || bounds.right > layout.width ||
66
- bounds.top < 0 || bounds.bottom > layout.height ||
67
- previous.some(item => textBoundsIntersect(item, bounds)) ||
68
- textBoundsIntersect(bounds, {
69
- left: cell.x + plot.x,
70
- right: cell.x + plot.x + plot.width,
71
- top: cell.y + plot.y,
72
- bottom: cell.y + plot.y + plot.height
73
- })) {
74
- throw new Error(
75
- "Facet headers require sufficient non-overlapping space above every child plot."
76
- );
77
- }
78
- previous.push(bounds);
79
- }
80
- }
81
-
82
- function materializeHeaders(program, layout, plots, config) {
83
- const plotById = new Map(plots.map(plot => [plot.id, plot]));
84
- const items = layout.children.map(cell => {
85
- const plot = plotById.get(cell.id);
86
- if (plot === undefined) {
87
- throw new Error(`Facet header requires plot bounds for "${cell.id}".`);
88
- }
89
- return {
90
- type: "text",
91
- properties: {
92
- x: cell.x + plot.x + plot.width / 2,
93
- y: cell.y + config.offset,
94
- text: formatVisibleText(cell.value),
95
- fill: config.color,
96
- fontSize: config.fontSize,
97
- fontFamily: config.fontFamily,
98
- fontWeight: config.fontWeight,
99
- textAlign: "center",
100
- textBaseline: "middle"
101
- }
102
- };
103
- });
104
- assertFacetHeadersFit(items, layout, plotById);
105
- return collection(program, `${program.compositionSpec.id}-headers`, items);
106
- }
107
-
108
52
  function materializeTitleComponent(program, id, lines, centers, style, plot, top) {
109
53
  if (lines.length === 0) return program;
110
54
  const x = alignedTextAnchor(plot.x, plot.width, program.titleConfig.align);
@@ -162,16 +106,28 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
162
106
  }
163
107
  const title = titleLayout(program);
164
108
  const spec = program.compositionSpec;
109
+ const preparedHeaders = prepareFacetHeaders(
110
+ program,
111
+ facetConfig(program).headers
112
+ );
113
+ const gridCells = new Map(
114
+ (spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
115
+ );
165
116
  const layout = resolveFacetLayout({
166
117
  children: spec.children.map((id, index) => ({
167
118
  ...compositionChildDescriptor(id, program.children[id]),
168
- value: spec.facet.values[index]
119
+ value: spec.facet.values[index],
120
+ ...(gridCells.has(id) ? {
121
+ row: gridCells.get(id).row,
122
+ column: gridCells.get(id).column
123
+ } : {})
169
124
  })),
170
125
  columns: spec.columns,
171
126
  gap: spec.gap,
172
127
  align: spec.align,
173
128
  padding: spec.padding,
174
129
  titleHeight: title.height,
130
+ headerLayout: preparedHeaders.headerLayout,
175
131
  sharedLegend: spec.facet.guides.legend === "shared",
176
132
  ...(preparedLegend === undefined ? {} : {
177
133
  sharedLegendGap: preparedLegend.reservation.gap,
@@ -188,16 +144,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
188
144
  placements: layout.children,
189
145
  plots
190
146
  });
191
- return { layout, title, plot, plots };
147
+ return { layout, title, plot, plots, preparedHeaders };
192
148
  }
193
149
 
194
150
  export function materializeFacetGraphics(program) {
195
151
  const preparedLegend = prepareSharedFacetLegend(program);
196
- const { layout, title, plot, plots } = resolveFacetProgramLayout(
152
+ const { layout, title, plot, plots, preparedHeaders } = resolveFacetProgramLayout(
197
153
  program,
198
154
  preparedLegend
199
155
  );
200
- const config = facetConfig(program);
156
+ const background = program.graphicSpec.objects.canvas?.properties
157
+ ?.background ?? "white";
201
158
  let next = clearCompositionChildren(program);
202
159
  if (next.graphicSpec.objects.canvas === undefined) {
203
160
  next = next.createGraphics({ id: "canvas", type: "canvas" });
@@ -205,7 +162,7 @@ export function materializeFacetGraphics(program) {
205
162
  for (const [property, value] of Object.entries({
206
163
  width: layout.width,
207
164
  height: layout.height,
208
- background: "white"
165
+ background
209
166
  })) {
210
167
  next = next.editGraphics({ target: "canvas", property, value });
211
168
  }
@@ -218,7 +175,7 @@ export function materializeFacetGraphics(program) {
218
175
  });
219
176
  next = attachSnapshotObject(next, snapshot, snapshot.order[0], "canvas");
220
177
  }
221
- next = materializeHeaders(next, layout, plots, config.headers);
178
+ next = materializeFacetHeaders(next, layout, plots, preparedHeaders);
222
179
  next = next.composeFacetGuides({ layout, plot });
223
180
  if (title.height > 0) next = materializeTitle(next, plot, title);
224
181
  return next._withCanvasConfig({
@@ -0,0 +1,57 @@
1
+ import { assertGuideCollisionBlocks } from "../../layout/guideCollisions.js";
2
+ import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
3
+ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
4
+
5
+ // Only domain-owned guides participate. Extension primitives may deliberately
6
+ // overlap, and a combined categorical/size legend is one occupied group.
7
+ export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
8
+ const blocks = [];
9
+ const append = (id, kind, position, ids) => {
10
+ if (position === undefined) return;
11
+ const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
12
+ const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
13
+ if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
14
+ };
15
+ append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
16
+ for (const channel of ["x", "y"]) {
17
+ for (const id of axisGraphicIds(channel)) {
18
+ const component = id.slice(`${channel}Axis`.length).toLowerCase();
19
+ append(`${channel}-axis ${component}`, "axis", guideConfigs.axis?.[channel]?.[component]?.position, [id]);
20
+ }
21
+ }
22
+ const policies = legendResourcePolicies();
23
+ const configs = guideConfigs.legend ?? {};
24
+ const categorical = policies.find(policy => policy.family === "categorical" && configs[policy.kind] !== undefined);
25
+ const combined = categorical !== undefined && configs.size !== undefined &&
26
+ configs[categorical.kind].target === configs.size.target;
27
+ for (const policy of policies) {
28
+ const config = configs[policy.kind];
29
+ if (config === undefined || combined && policy.kind === "size") continue;
30
+ const ids = combined && policy === categorical
31
+ ? [...policy.graphicIds, ...policies.find(item => item.kind === "size").graphicIds]
32
+ : policy.graphicIds;
33
+ append(`${policy.kind}${combined && policy === categorical ? "+size" : ""} legend`, "legend", config.position, ids);
34
+ }
35
+ return blocks;
36
+ }
37
+
38
+
39
+ // Defer cross-guide checks while sibling materializers still carry old
40
+ // coordinates. The scope is transient and never survives a returned program.
41
+ export function withGuideLayoutTransaction(program, apply) {
42
+ if (program.context.deferGuideLayoutValidation === true) return apply(program);
43
+ const result = apply(program._withContext({ deferGuideLayoutValidation: true }));
44
+ const { deferGuideLayoutValidation: _deferred, ...context } = result.context;
45
+ if (Object.hasOwn(program.context, "deferGuideLayoutValidation")) {
46
+ context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
47
+ }
48
+ const next = result._clone({ context });
49
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
50
+ return next;
51
+ }
52
+
53
+ export function withGuideLayoutValidation(implementation) {
54
+ return function (args) {
55
+ return withGuideLayoutTransaction(this, program => implementation.call(program, args));
56
+ };
57
+ }
@@ -22,12 +22,20 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
22
22
  component => `colorLegend${component}`
23
23
  ))
24
24
  }),
25
+ stroke: Object.freeze({
26
+ semanticKind: "stroke",
27
+ family: "categorical",
28
+ rematerializeOp: undefined,
29
+ graphicIds: Object.freeze(CATEGORICAL_COMPONENTS.map(
30
+ component => `strokeLegend${component}`
31
+ ))
32
+ }),
25
33
  size: Object.freeze({
26
34
  semanticKind: "size",
27
35
  family: "size",
28
36
  rematerializeOp: "rematerializeSizeLegend",
29
37
  graphicIds: Object.freeze([
30
- "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
38
+ "sizeLegendBackground", "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
31
39
  ])
32
40
  }),
33
41
  gradient: Object.freeze({
@@ -48,6 +56,24 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
48
56
  "colorLegendTitle"
49
57
  ])
50
58
  }),
59
+ strokeGradient: Object.freeze({
60
+ semanticKind: "stroke",
61
+ family: "continuous",
62
+ rematerializeOp: "rematerializeStrokeGradientLegend",
63
+ graphicIds: Object.freeze([
64
+ "strokeGradientBackground", "strokeGradientStrips", "strokeGradientTicks",
65
+ "strokeGradientLabels", "strokeGradientTitle"
66
+ ])
67
+ }),
68
+ strokeInterval: Object.freeze({
69
+ semanticKind: "stroke",
70
+ family: "interval",
71
+ rematerializeOp: "rematerializeStrokeIntervalLegend",
72
+ graphicIds: Object.freeze([
73
+ "strokeIntervalBackground", "strokeIntervalSymbols",
74
+ "strokeIntervalLabels", "strokeIntervalTitle"
75
+ ])
76
+ }),
51
77
  opacity: Object.freeze({
52
78
  semanticKind: "opacity",
53
79
  family: "continuous",
@@ -62,7 +88,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
62
88
  family: "strokeWidth",
63
89
  rematerializeOp: "rematerializeStrokeWidthLegend",
64
90
  graphicIds: Object.freeze([
65
- "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
91
+ "strokeWidthLegendBackground", "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
66
92
  "strokeWidthLegendTitle"
67
93
  ])
68
94
  })
@@ -1,10 +1,16 @@
1
+ import { findLayer } from "../selectors/layers.js";
1
2
  import { LEGEND_CONFIG_KINDS } from "../core/vocabulary.js";
2
3
 
4
+ export function isOpacityLegendLayer(layer) {
5
+ return ["point", "line"].includes(layer.mark?.type) &&
6
+ layer.encoding?.opacity?.scale !== undefined;
7
+ }
8
+
3
9
  export function hasMaterializedLegend(program) {
4
10
  return LEGEND_CONFIG_KINDS.some(kind => {
5
11
  const config = program.guideConfigs?.legend?.[kind];
6
12
  if (config === undefined) return false;
7
- if (kind === "series" || kind === "color") {
13
+ if (["series", "color", "stroke"].includes(kind)) {
8
14
  return program.semanticSpec.guides.legend?.[kind] !== undefined;
9
15
  }
10
16
  return true;
@@ -15,6 +21,9 @@ export function materializedLegendUsesScale(program, id) {
15
21
  return LEGEND_CONFIG_KINDS.some(kind => {
16
22
  const config = program.guideConfigs?.legend?.[kind];
17
23
  if (config === undefined) return false;
18
- return config.scale === id || config.scales?.includes(id);
24
+ const order = program.semanticSpec.guides.legend?.[kind]?.order;
25
+ const linkedScale = order?.channel === undefined ? undefined
26
+ : findLayer(program, config.target)?.encoding?.[order.channel]?.scale;
27
+ return config.scale === id || config.scales?.includes(id) || linkedScale === id;
19
28
  });
20
29
  }
@@ -145,9 +145,9 @@ export function canMaterializeBar(program, layer) {
145
145
  ) {
146
146
  const offsetChannel = resolveBarOffsetChannel(layer);
147
147
  return (
148
- layer.encoding?.color?.field !== undefined &&
149
- layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
150
- layer.encoding[offsetChannel].scale !== undefined
148
+ (layer.encoding?.group !== undefined || (layer.encoding?.color?.field !== undefined &&
149
+ layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field)) &&
150
+ layer.encoding[offsetChannel]?.scale !== undefined
151
151
  );
152
152
  }
153
153
  return true;
@@ -163,14 +163,29 @@ export function canMaterializeRule(program, layer) {
163
163
  );
164
164
  }
165
165
 
166
+ const TEXT_SOURCE_CAPABILITIES = Object.freeze({
167
+ point: canMaterializePoint,
168
+ line: canMaterializeLine,
169
+ bar: canMaterializeBar,
170
+ rule: canMaterializeRule,
171
+ rect: (program, layer) => canMaterializeRect(program, layer) ||
172
+ program.markConfigs[layer.id]?.gradientPlot?.materialized === true,
173
+ arc: canMaterializeArc
174
+ });
175
+
176
+ export function isTextSource(layer) {
177
+ return layer?.data !== undefined &&
178
+ Object.hasOwn(TEXT_SOURCE_CAPABILITIES, layer.mark?.type);
179
+ }
180
+
166
181
  export function canMaterializeText(program, layer) {
167
182
  if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
168
183
  return false;
169
184
  }
170
185
  if (layer.source !== undefined) {
171
186
  const source = findLayer(program, layer.source);
172
- return source !== undefined &&
173
- ["point", "bar", "rule", "rect", "arc"].includes(source.mark?.type) &&
187
+ return isTextSource(source) &&
188
+ TEXT_SOURCE_CAPABILITIES[source.mark.type](program, source) &&
174
189
  Array.isArray(program.graphicSpec.objects[source.id]?.items);
175
190
  }
176
191
  return hasCartesianPositionScales(layer);