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
@@ -0,0 +1,347 @@
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, profile) {
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, profile) +
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", program.materializationConfigs.textMetrics);
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", program.materializationConfigs.textMetrics);
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, profile) {
291
+ const bounds = items.map((item, index) => records[index].descriptor.text === ""
292
+ ? undefined
293
+ : resolveTextBounds(item.properties, profile));
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, profile) {
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, profile);
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 profile = program.materializationConfigs.textMetrics;
329
+ const cells = new Map(layout.children.map(cell => [cell.id, cell]));
330
+ const plotById = new Map(plots.map(plot => [plot.id, plot]));
331
+ const records = prepared.descriptors.map(descriptor => {
332
+ if (descriptor.topology === "legacy") {
333
+ return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
334
+ }
335
+ return roleItem(descriptor, cells, plotById);
336
+ });
337
+ const items = records.map(styledItem);
338
+ if (prepared.headers.mode === "legacy") {
339
+ assertLegacyHeadersFit(items, layout, plotById, profile);
340
+ } else {
341
+ assertRoleHeadersFit(records, items, layout, profile);
342
+ }
343
+ const id = `${program.compositionSpec.id}-headers`;
344
+ return program
345
+ .createGraphics({ id, type: "collection", parent: "canvas" })
346
+ .editGraphics({ target: id, property: "items", value: items });
347
+ }
@@ -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,6 +106,10 @@ 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
+ );
165
113
  const gridCells = new Map(
166
114
  (spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
167
115
  );
@@ -179,6 +127,7 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
179
127
  align: spec.align,
180
128
  padding: spec.padding,
181
129
  titleHeight: title.height,
130
+ headerLayout: preparedHeaders.headerLayout,
182
131
  sharedLegend: spec.facet.guides.legend === "shared",
183
132
  ...(preparedLegend === undefined ? {} : {
184
133
  sharedLegendGap: preparedLegend.reservation.gap,
@@ -195,16 +144,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
195
144
  placements: layout.children,
196
145
  plots
197
146
  });
198
- return { layout, title, plot, plots };
147
+ return { layout, title, plot, plots, preparedHeaders };
199
148
  }
200
149
 
201
150
  export function materializeFacetGraphics(program) {
202
151
  const preparedLegend = prepareSharedFacetLegend(program);
203
- const { layout, title, plot, plots } = resolveFacetProgramLayout(
152
+ const { layout, title, plot, plots, preparedHeaders } = resolveFacetProgramLayout(
204
153
  program,
205
154
  preparedLegend
206
155
  );
207
- const config = facetConfig(program);
156
+ const background = program.graphicSpec.objects.canvas?.properties
157
+ ?.background ?? "white";
208
158
  let next = clearCompositionChildren(program);
209
159
  if (next.graphicSpec.objects.canvas === undefined) {
210
160
  next = next.createGraphics({ id: "canvas", type: "canvas" });
@@ -212,7 +162,7 @@ export function materializeFacetGraphics(program) {
212
162
  for (const [property, value] of Object.entries({
213
163
  width: layout.width,
214
164
  height: layout.height,
215
- background: "white"
165
+ background
216
166
  })) {
217
167
  next = next.editGraphics({ target: "canvas", property, value });
218
168
  }
@@ -225,7 +175,7 @@ export function materializeFacetGraphics(program) {
225
175
  });
226
176
  next = attachSnapshotObject(next, snapshot, snapshot.order[0], "canvas");
227
177
  }
228
- next = materializeHeaders(next, layout, plots, config.headers);
178
+ next = materializeFacetHeaders(next, layout, plots, preparedHeaders);
229
179
  next = next.composeFacetGuides({ layout, plot });
230
180
  if (title.height > 0) next = materializeTitle(next, plot, title);
231
181
  return next._withCanvasConfig({
@@ -4,12 +4,12 @@ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
4
4
 
5
5
  // Only domain-owned guides participate. Extension primitives may deliberately
6
6
  // overlap, and a combined categorical/size legend is one occupied group.
7
- export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
7
+ export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig, profile) {
8
8
  const blocks = [];
9
9
  const append = (id, kind, position, ids) => {
10
10
  if (position === undefined) return;
11
11
  const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
12
- const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
12
+ const bounds = unionConcreteGraphicBounds(graphicSpec, existing, profile);
13
13
  if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
14
14
  };
15
15
  append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
@@ -46,7 +46,7 @@ export function withGuideLayoutTransaction(program, apply) {
46
46
  context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
47
47
  }
48
48
  const next = result._clone({ context });
49
- assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
49
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig, next.materializationConfigs.textMetrics));
50
50
  return next;
51
51
  }
52
52
 
@@ -22,6 +22,14 @@ 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",
@@ -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",
@@ -10,7 +10,7 @@ export function hasMaterializedLegend(program) {
10
10
  return LEGEND_CONFIG_KINDS.some(kind => {
11
11
  const config = program.guideConfigs?.legend?.[kind];
12
12
  if (config === undefined) return false;
13
- if (kind === "series" || kind === "color") {
13
+ if (["series", "color", "stroke"].includes(kind)) {
14
14
  return program.semanticSpec.guides.legend?.[kind] !== undefined;
15
15
  }
16
16
  return true;
@@ -36,6 +36,9 @@ export function getScaleConsumerMarkSteps(program, scaleIds) {
36
36
  }
37
37
 
38
38
  export function getMarkRematerializationStep(program, layer) {
39
+ if (program.markConfigs?.[layer.id]?.statisticalReference !== undefined) {
40
+ return { op: "rematerializeStatisticalReference", args: { id: layer.id } };
41
+ }
39
42
  if (program.markConfigs?.[layer.id]?.markFilter?.empty === true) {
40
43
  return { op: "materializeEmptyMark", args: { id: layer.id } };
41
44
  }
@@ -54,15 +57,18 @@ export function getMarkMaterializationStep(program, layer) {
54
57
  }
55
58
 
56
59
  export function getSourceDependentMarkSteps(program, sourceId) {
57
- return (program.semanticSpec.layers ?? []).flatMap(layer =>
58
- layer.source === sourceId &&
59
- getMarkMaterializationPolicy(layer)?.sourceDependent === true
60
+ return (program.semanticSpec.layers ?? []).flatMap(layer => {
61
+ const sourceOwned = layer.source === sourceId &&
62
+ getMarkMaterializationPolicy(layer)?.sourceDependent === true;
63
+ const statistical = program.markConfigs?.[layer.id]
64
+ ?.statisticalReference?.source === sourceId;
65
+ return sourceOwned || statistical
60
66
  ? [getMarkMaterializationStep(program, layer) ??
61
67
  getExistingMarkRematerializationStep(program, layer)].filter(
62
68
  step => step !== undefined
63
69
  )
64
- : []
65
- );
70
+ : [];
71
+ });
66
72
  }
67
73
 
68
74
  export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
@@ -50,7 +50,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
50
50
  op: "rematerializeAreaMark",
51
51
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
52
52
  encoding: Object.freeze({
53
- sharedChannels: Object.freeze(["color"]),
53
+ sharedChannels: Object.freeze(["color", "stroke"]),
54
54
  skipRematerialization: isIntentionallyEmptyArea
55
55
  }),
56
56
  scaleApplication: Object.freeze({ default: "defer" })
@@ -58,6 +58,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
58
58
  arc: Object.freeze({
59
59
  canMaterialize: canMaterializeArc,
60
60
  op: "rematerializeArcMark",
61
+ acceptsResolvedScales: true,
61
62
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
62
63
  encoding: Object.freeze({ completeOnly: true }),
63
64
  scaleApplication: Object.freeze({ default: "defer" })