ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -19,7 +19,6 @@ import {
19
19
  findSelectionPolicy,
20
20
  requireSelectionPolicy
21
21
  } from "../../materialization/selection/policies/index.js";
22
- import { legendGraphicIds } from "../../materialization/guides/resources.js";
23
22
  import {
24
23
  normalizeDimOthers,
25
24
  validateUnitInterval
@@ -84,18 +83,7 @@ function categoricalLegendKinds(program, target) {
84
83
  function resetCategoricalLegendSymbols(program, target) {
85
84
  const kinds = categoricalLegendKinds(program, target);
86
85
  if (kinds.length === 0) return program;
87
- let next = program;
88
- for (const kind of kinds) {
89
- for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
90
- const graphic = next.graphicSpec.objects[id];
91
- if (graphic === undefined) continue;
92
- next = graphic.type === "collection"
93
- ? next.editGraphics({ target: id, property: "items", value: [] })
94
- : next.editGraphics({ target: id, property: "length", value: 0 });
95
- }
96
- next = next.rematerializeLegendSymbols({ kind });
97
- }
98
- return next;
86
+ return program.rematerializeLegend();
99
87
  }
100
88
 
101
89
  function targetHighlightEntries(program, target) {
@@ -1,5 +1,6 @@
1
+ import { synchronizeFacetLegendAppearance } from "../../materialization/facetGuides/owner.js";
1
2
  import { rematerializeTypography } from "../../materialization/typography.js";
2
- import { themeTokens } from "../../theme/defaults.js";
3
+ import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, themeTokens } from "../../theme/defaults.js";
3
4
  import {
4
5
  normalizeThemeState,
5
6
  resolveEffectiveThemeTokens,
@@ -21,7 +22,7 @@ const THEME_STYLE_PROPERTIES = Object.freeze([
21
22
  "background",
22
23
  "fill",
23
24
  "stroke",
24
- "fontFamily"
25
+ "fontFamily", "fontSize"
25
26
  ]);
26
27
  const COLOR_TOKENS = Object.freeze([
27
28
  "mark",
@@ -51,15 +52,38 @@ function tokenFor(value, sourceTokens, targetTokens) {
51
52
  return matches[0];
52
53
  }
53
54
 
54
- function roleValues(role, sourceTokens, legacy = []) {
55
+ function axisFamily(name) {
56
+ return name.startsWith("parallel") ? "parallel"
57
+ : /^(theta|radial|radius)/u.test(name) ? "polar" : "cartesian";
58
+ }
59
+
60
+ function axisTextRole(property, component) {
61
+ if (!["labels", "title"].includes(component)) return undefined;
62
+ const role = component === "labels" ? "axisLabel" : "axisTitle";
63
+ if (property === "fontFamily") return `${role}FontFamily`;
64
+ if (property === "fontSize") return `${role}FontSize`;
65
+ if (["fill", "color"].includes(property)) return role;
66
+ return undefined;
67
+ }
68
+
69
+ function roleToken(tokens, role, family = "cartesian") {
70
+ if (tokens[role] !== undefined) return tokens[role];
71
+ if (role === "axisLabel") return family === "parallel" ? tokens.axis : tokens.text;
72
+ if (["axisLabelFontFamily", "axisTitleFontFamily"].includes(role)) return tokens.fontFamily;
73
+ if (role === "axisLabelFontSize") return AXIS_LABEL_FONT_SIZES[family];
74
+ if (role === "axisTitleFontSize") return AXIS_TITLE_FONT_SIZE;
75
+ return undefined;
76
+ }
77
+
78
+ function roleValues(role, sourceTokens, legacy = [], family) {
55
79
  return new Set([
56
- sourceTokens[role],
57
- themeTokens("light")[role],
80
+ roleToken(sourceTokens, role, family),
81
+ roleToken(themeTokens("light"), role, family),
58
82
  ...legacy.map(token => themeTokens("light")[token] ?? token)
59
83
  ]);
60
84
  }
61
85
 
62
- function mapRoleValue(value, role, sourceTokens, targetTokens, legacy) {
86
+ function mapRoleValue(value, role, sourceTokens, targetTokens, legacy, family) {
63
87
  if (Array.isArray(value)) {
64
88
  let changed = false;
65
89
  const next = value.map(item => {
@@ -68,17 +92,18 @@ function mapRoleValue(value, role, sourceTokens, targetTokens, legacy) {
68
92
  role,
69
93
  sourceTokens,
70
94
  targetTokens,
71
- legacy
95
+ legacy, family
72
96
  );
73
97
  changed ||= mapped.changed;
74
98
  return mapped.value;
75
99
  });
76
100
  return { changed, value: changed ? next : value };
77
101
  }
78
- if (!roleValues(role, sourceTokens, legacy).has(value)) {
102
+ if (!roleValues(role, sourceTokens, legacy, family).has(value)) {
79
103
  return { changed: false, value };
80
104
  }
81
- return { changed: value !== targetTokens[role], value: targetTokens[role] };
105
+ const target = roleToken(targetTokens, role, family);
106
+ return { changed: value !== target, value: target };
82
107
  }
83
108
 
84
109
  function mapColorValue(value, sourceTokens, targetTokens) {
@@ -158,6 +183,9 @@ function componentRole(program, id, property) {
158
183
  }
159
184
 
160
185
  function graphicRole(program, id, property) {
186
+ const axisPart = /(?:Axis|^axis_.+__)(Labels|Titles?)$/u.exec(id);
187
+ const scoped = axisPart === null ? undefined : axisTextRole(property, axisPart[1] === "Labels" ? "labels" : "title");
188
+ if (scoped !== undefined) return scoped;
161
189
  if (property === "fontFamily") return "fontFamily";
162
190
  if (id === "canvas" && property === "background") return "background";
163
191
  if (id === `${program.compositionSpec?.id}-headers` && property === "fill") {
@@ -173,11 +201,11 @@ function graphicRole(program, id, property) {
173
201
  }
174
202
  return layer.mark?.type === "text" ? "text" : "mark";
175
203
  }
176
- if (/AxisLines?$/u.test(id) && property === "stroke") return "axis";
177
- if (/AxisTicks$/u.test(id) && property === "stroke") return "mutedText";
204
+ if (/(?:Axis|^axis_.+__)Lines?$/u.test(id) && property === "stroke") return "axis";
205
+ if (/(?:Axis|^axis_.+__)Ticks$/u.test(id) && property === "stroke") return "mutedText";
178
206
  if (/^parallelAxisLabels$/u.test(id) && property === "fill") return "axis";
179
- if (/AxisLabels$/u.test(id) && property === "fill") return "text";
180
- if (/AxisTitles?$/u.test(id) && property === "fill") return "axisTitle";
207
+ if (/(?:Axis|^axis_.+__)Labels$/u.test(id) && property === "fill") return "text";
208
+ if (/(?:Axis|^axis_.+__)Titles?$/u.test(id) && property === "fill") return "axisTitle";
181
209
  if (/Grid(?:Lines|Circles)?$/u.test(id) && property === "stroke") return "grid";
182
210
  if (id === "chartTitle" && property === "fill") return "strongText";
183
211
  if (id === "chartSubtitle" && property === "fill") return "mutedText";
@@ -198,6 +226,7 @@ function graphicRole(program, id, property) {
198
226
  }
199
227
 
200
228
  function roleLegacy(role) {
229
+ if (role === "axisLabel") return ["text", "axis"];
201
230
  if (role === "axis") return ["text"];
202
231
  if (role === "axisTitle") return ["text", "strongText"];
203
232
  if (role === "strongText") return ["text"];
@@ -238,7 +267,7 @@ function mapGraphicProperties(
238
267
  role,
239
268
  sourceTokens,
240
269
  targetTokens,
241
- roleLegacy(role)
270
+ roleLegacy(role), axisFamily(id)
242
271
  );
243
272
  if (mapped.changed) {
244
273
  next[property] = mapped.value;
@@ -304,6 +333,11 @@ function recolorGraphic(
304
333
 
305
334
  function configRole(program, path) {
306
335
  const property = path.at(-1);
336
+ if (path[0] === "guides" && path[1] === "axis") {
337
+ const component = path[2] === "parallel" ? path.at(-2) : path[3];
338
+ const scoped = axisTextRole(property, component);
339
+ if (scoped !== undefined) return scoped;
340
+ }
307
341
  if (property === "fontFamily") return "fontFamily";
308
342
  if (path[0] === "facets" && path.includes("headers") &&
309
343
  property === "color") {
@@ -369,7 +403,7 @@ function recolorConfig(
369
403
  }
370
404
  if (value === null || typeof value !== "object") {
371
405
  const property = path.at(-1);
372
- if (!["color", "fill", "stroke", "background", "fontFamily"]
406
+ if (!["color", "fill", "stroke", "background", "fontFamily", "fontSize"]
373
407
  .includes(property)) {
374
408
  return { changed: false, value };
375
409
  }
@@ -387,7 +421,7 @@ function recolorConfig(
387
421
  role,
388
422
  sourceTokens,
389
423
  targetTokens,
390
- roleLegacy(role)
424
+ roleLegacy(role), axisFamily(String(path[2] ?? ""))
391
425
  );
392
426
  }
393
427
  if (["theme", "highlights", "selections"].includes(path[0])) {
@@ -643,12 +677,13 @@ function clearRemovedResources(overrides, node) {
643
677
  clearPrefix(overrides, `g:${mark[1]}.`);
644
678
  return;
645
679
  }
646
- const axis = /^guide\.axis\.(x|y|theta|radius)$/u
680
+ const axis = /^guide\.axis\.([A-Za-z0-9_-]+)$/u
647
681
  .exec(current.args.property);
648
682
  if (axis !== null) {
649
683
  const prefix = axis[1] === "radius" ? "radial" : axis[1];
684
+ const graphicPrefix = ["x", "y", "theta", "radius"].includes(axis[1]) ? `${prefix}Axis` : `axis_${axis[1]}__`;
650
685
  clearPrefix(overrides, `c:guides.axis.${axis[1]}.`);
651
- clearPrefix(overrides, `g:${prefix}Axis`);
686
+ clearPrefix(overrides, `g:${graphicPrefix}`);
652
687
  return;
653
688
  }
654
689
  const grid = /^guide\.grid\.([^\.]+)$/u.exec(current.args.property);
@@ -704,7 +739,23 @@ function clearRecreatedOverrides(overrides, op, args) {
704
739
  }
705
740
  }
706
741
 
707
- function addAxisOverrides(overrides, op, args) {
742
+ function axisOverrideId(node, channel, args) {
743
+ if (args.target !== undefined || args.id !== undefined) return args.target ?? args.id;
744
+ const ids = new Set();
745
+ const visit = entry => {
746
+ if (["createGraphics", "editGraphics"].includes(entry.op)) {
747
+ const id = entry.args.target ?? entry.args.id;
748
+ const named = /^axis_(.+)__(Line|Ticks|Labels|Title)$/u.exec(id ?? "");
749
+ if (named !== null) ids.add(named[1]);
750
+ else if (new RegExp(`^${channel}Axis(?:Line|Ticks|Labels|Title)$`).test(id ?? "")) ids.add(channel);
751
+ }
752
+ entry.children?.forEach(visit);
753
+ };
754
+ if (node !== undefined) visit(node);
755
+ return ids.size === 1 ? [...ids][0] : channel;
756
+ }
757
+
758
+ function addAxisOverrides(overrides, op, args, node) {
708
759
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis(Line|Ticks|Labels|Title)$/u
709
760
  .exec(op);
710
761
  if (match === null) return;
@@ -715,22 +766,25 @@ function addAxisOverrides(overrides, op, args) {
715
766
  Radial: "radius"
716
767
  }[match[1]];
717
768
  const component = match[2].toLowerCase();
769
+ const axisId = axisOverrideId(node, channel, args);
718
770
  const idPrefix = match[1] === "Radial" ? "radial" : channel;
719
771
  if (Object.hasOwn(args, "color")) {
720
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
772
+ overrides.add(`c:guides.axis.${axisId}.${component}.color`);
721
773
  overrides.add(
722
- `g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
774
+ `g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
723
775
  );
724
776
  }
725
- if (["labels", "title"].includes(component) &&
726
- Object.hasOwn(args, "fontFamily")) {
727
- overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
728
- overrides.add(`g:${idPrefix}Axis${match[2]}.fontFamily`);
777
+ for (const property of ["fontFamily", "fontSize"]) {
778
+ if (["labels", "title"].includes(component) && Object.hasOwn(args, property)) {
779
+ overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
780
+ overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${property}`);
781
+ }
729
782
  }
730
783
  }
731
784
 
732
- function addCompleteAxisOverride(overrides, name, args) {
785
+ function addCompleteAxisOverride(overrides, name, args, node) {
733
786
  const channel = { X: "x", Y: "y", Theta: "theta", Radial: "radius" }[name];
787
+ const axisId = axisOverrideId(node, channel, args);
734
788
  const idPrefix = name === "Radial" ? "radial" : channel;
735
789
  const components = {
736
790
  line: args.line,
@@ -745,25 +799,32 @@ function addCompleteAxisOverride(overrides, name, args) {
745
799
  for (const [component, options] of Object.entries(components)) {
746
800
  const suffix = `${component[0].toUpperCase()}${component.slice(1)}`;
747
801
  if (Object.hasOwn(options ?? {}, "color")) {
748
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
802
+ overrides.add(`c:guides.axis.${axisId}.${component}.color`);
749
803
  overrides.add(
750
- `g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
804
+ `g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
751
805
  );
752
806
  }
753
- if (["labels", "title"].includes(component) &&
754
- Object.hasOwn(options ?? {}, "fontFamily")) {
755
- overrides.add(`c:guides.axis.${channel}.${component}.fontFamily`);
756
- overrides.add(`g:${idPrefix}Axis${suffix}.fontFamily`);
807
+ for (const property of ["fontFamily", "fontSize"]) {
808
+ if (["labels", "title"].includes(component) && Object.hasOwn(options ?? {}, property)) {
809
+ overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
810
+ overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${property}`);
811
+ }
757
812
  }
758
813
  }
759
814
  }
760
815
 
761
- function addCompleteAxisOverrides(overrides, op, args) {
816
+ function addCompleteAxisOverrides(overrides, op, args, node) {
817
+ if (args.guides?.axes !== null && typeof args.guides?.axes === "object") {
818
+ addCompleteAxisOverrides(overrides, "createAxes", args.guides.axes);
819
+ }
820
+ if (op === "createGuides" && args.axes !== null && typeof args.axes === "object") {
821
+ addCompleteAxisOverrides(overrides, "createAxes", args.axes);
822
+ }
762
823
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis$/u.exec(op);
763
- if (match !== null) addCompleteAxisOverride(overrides, match[1], args);
824
+ if (match !== null) addCompleteAxisOverride(overrides, match[1], args, node);
764
825
  const group = /^(?:create|edit)(X|Y)AxisTicksAndLabels$/u.exec(op);
765
826
  if (group !== null) {
766
- addCompleteAxisOverride(overrides, group[1], { ticksAndLabels: args });
827
+ addCompleteAxisOverride(overrides, group[1], { target: args.target, ticksAndLabels: args }, node);
767
828
  }
768
829
  if (op !== "createAxes") return;
769
830
  for (const [option, name] of [
@@ -773,7 +834,7 @@ function addCompleteAxisOverrides(overrides, op, args) {
773
834
  ["radius", "Radial"]
774
835
  ]) {
775
836
  if (args[option] && typeof args[option] === "object") {
776
- addCompleteAxisOverride(overrides, name, args[option]);
837
+ for (const entry of Array.isArray(args[option]) ? args[option] : [args[option]]) addCompleteAxisOverride(overrides, name, entry);
777
838
  }
778
839
  }
779
840
  }
@@ -823,11 +884,10 @@ function addParallelOverrides(overrides, program, op, args) {
823
884
  `c:guides.axis.parallel.axes.dimensions.${index}.${component}.color`
824
885
  );
825
886
  }
826
- if (["labels", "title"].includes(component) &&
827
- Object.hasOwn(options ?? {}, "fontFamily")) {
828
- overrides.add(
829
- `c:guides.axis.parallel.axes.dimensions.${index}.${component}.fontFamily`
830
- );
887
+ for (const property of ["fontFamily", "fontSize"]) {
888
+ if (["labels", "title"].includes(component) && Object.hasOwn(options ?? {}, property)) {
889
+ overrides.add(`c:guides.axis.parallel.axes.dimensions.${index}.${component}.${property}`);
890
+ }
831
891
  }
832
892
  }
833
893
  }
@@ -921,7 +981,7 @@ function explicitOverrides(program, node) {
921
981
  const overrides = new Set();
922
982
  if (op === "editGraphics" &&
923
983
  typeof args.target === "string" &&
924
- ["background", "fill", "stroke", "fontFamily"].includes(args.property)) {
984
+ ["background", "fill", "stroke", "fontFamily", "fontSize"].includes(args.property)) {
925
985
  overrides.add(`g:${args.target}.${args.property}`);
926
986
  }
927
987
  if (["createCanvas", "editCanvas"].includes(op) &&
@@ -937,8 +997,8 @@ function explicitOverrides(program, node) {
937
997
  addErrorBarOverrides(overrides, program, node);
938
998
  addErrorBandOverrides(overrides, program, node);
939
999
  addRegressionOverrides(overrides, node);
940
- addAxisOverrides(overrides, op, args);
941
- addCompleteAxisOverrides(overrides, op, args);
1000
+ addAxisOverrides(overrides, op, args, node);
1001
+ addCompleteAxisOverrides(overrides, op, args, node);
942
1002
  addGridOverrides(overrides, op, args);
943
1003
  addParallelOverrides(overrides, program, op, args);
944
1004
  if (["createTitle", "editTitle"].includes(op)) {
@@ -959,9 +1019,21 @@ function explicitOverrides(program, node) {
959
1019
  if (op === "editLegendBlock") {
960
1020
  addLegendBlockOverrides(overrides, program, args);
961
1021
  }
1022
+ const parentLegendOptions = op === "editFacetGuides" ? args.legend
1023
+ : ["facet", "facetGrid", "repeatCharts"].includes(op) ? args.guides?.legend : undefined;
1024
+ if (parentLegendOptions !== null && typeof parentLegendOptions === "object") {
1025
+ addLegendOverrides(overrides, program, parentLegendOptions);
1026
+ }
962
1027
  if (op === "editFacetHeaders") {
963
1028
  addFacetOverrides(overrides, program, args);
964
1029
  }
1030
+ if (["facet", "facetGrid", "repeatCharts"].includes(op) && args.headers !== undefined) {
1031
+ const { row, column, ...common } = args.headers;
1032
+ addFacetOverrides(overrides, program, common);
1033
+ for (const [role, patch] of [["row", row], ["column", column]]) {
1034
+ if (patch !== undefined) addFacetOverrides(overrides, program, { ...patch, role });
1035
+ }
1036
+ }
965
1037
  return overrides;
966
1038
  }
967
1039
 
@@ -979,6 +1051,12 @@ function collectOverrides(program) {
979
1051
  return overrides;
980
1052
  }
981
1053
 
1054
+ function axisTypographyChanged(before, after) {
1055
+ if (before === after || after === null || typeof after !== "object") return false;
1056
+ return Object.entries(after).some(([key, value]) => ["fontFamily", "fontSize"].includes(key)
1057
+ ? value !== before?.[key] : axisTypographyChanged(before?.[key], value));
1058
+ }
1059
+
982
1060
  export function reconcileProgramTheme(program, { source, metadata }) {
983
1061
  program = reconcileFollowingRegressions(program);
984
1062
  const storedState = program.materializationConfigs.theme;
@@ -1031,9 +1109,12 @@ export function reconcileProgramTheme(program, { source, metadata }) {
1031
1109
  next = next._withMaterializationConfig(["facets"], configs.value.facets);
1032
1110
  }
1033
1111
 
1112
+ if (configs.changed) next = synchronizeFacetLegendAppearance(next);
1113
+
1034
1114
  const facetsChanged = configs.value.facets !==
1035
1115
  themed.materializationConfigs.facets;
1036
- const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily;
1116
+ const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily ||
1117
+ axisTypographyChanged(themed.guideConfigs.axis, configs.value.guides.axis);
1037
1118
  if (facetsChanged && next.compositionSpec?.type === "facet") {
1038
1119
  next = next.materializeComposition();
1039
1120
  } else if (typographyChanged) {
@@ -158,3 +158,15 @@ export function setThemeStateOverrides(state, overrides) {
158
158
  overrides: cloneAndFreeze([...overrides].sort())
159
159
  });
160
160
  }
161
+
162
+ // Resolve active font defaults before guide layout validates their occupied bounds.
163
+ export function axisThemeTypography(state, component) {
164
+ if (resolveEffectiveThemeFrame(state) === undefined) return {};
165
+ const tokens = resolveEffectiveThemeTokens(state);
166
+ const role = component === "labels" ? "axisLabel" : "axisTitle";
167
+ const fontSize = tokens[`${role}FontSize`];
168
+ return {
169
+ fontFamily: tokens[`${role}FontFamily`] ?? tokens.fontFamily,
170
+ ...(fontSize === undefined ? {} : { fontSize })
171
+ };
172
+ }
@@ -1,3 +1,4 @@
1
+ import { paintedTitleComponent, titleTextProperties } from "../../layout/title.js";
1
2
  import { withGuideLayoutValidation } from "../../materialization/guides/layout.js";
2
3
  import { action } from "../../core/action.js";
3
4
  import { noOptions } from "../../core/validation.js";
@@ -56,24 +57,9 @@ function ensureTextShape(program, id, component) {
56
57
  return program;
57
58
  }
58
59
 
59
- function distributed(value, count) {
60
- return count === 1 && Array.isArray(value) ? value[0] : value;
61
- }
62
-
63
60
  function editTextGraphic(program, id, component, style) {
64
- const count = component.lines.length;
65
61
  let next = ensureTextShape(program, id, component);
66
- for (const [property, value] of Object.entries({
67
- x: distributed(component.x, count),
68
- y: distributed(component.y, count),
69
- text: distributed(component.lines, count),
70
- fill: style.color,
71
- fontSize: style.fontSize,
72
- fontFamily: style.fontFamily,
73
- fontWeight: style.fontWeight,
74
- textAlign: component.textAlign,
75
- textBaseline: "middle"
76
- })) {
62
+ for (const [property, value] of Object.entries(titleTextProperties(component, style))) {
77
63
  next = next.editGraphics({ target: id, property, value });
78
64
  }
79
65
  if (component.explicitRotation) {
@@ -99,10 +85,11 @@ function titleTextAction(kind, create) {
99
85
  if (create && this.graphicSpec.objects[id] !== undefined) {
100
86
  throw new Error(`${op} requires a missing chart ${kind} graphic.`);
101
87
  }
102
- const component = resolveTitleLayout(this, config)[kind];
103
- if (component === undefined) {
88
+ const layout = resolveTitleLayout(this, config)[kind];
89
+ if (layout === undefined) {
104
90
  throw new Error(`${op} requires semantic subtitle text.`);
105
91
  }
92
+ const component = paintedTitleComponent(layout);
106
93
  if (!create) {
107
94
  return editTextGraphic(this, id, component, config[`${kind}Style`]);
108
95
  }
@@ -1,3 +1,7 @@
1
+ import { textBoundsFitCanvas } from "../../core/textMetrics.js";
2
+ import { resolveGuideCollisionBlocks } from "../../materialization/guides/resources.js";
3
+ import { canvasOverflowError } from "../../layout/canvas.js";
4
+ import { validateFontStyle } from "../../core/font.js";
1
5
  import { isPlainObject } from "../../core/immutable.js";
2
6
  import {
3
7
  validateKeys,
@@ -19,11 +23,11 @@ import { findCanvasGraphic } from
19
23
  "../../materialization/graphicHierarchy.js";
20
24
 
21
25
  const OPTIONS = Object.freeze([
22
- "text", "subtitle", "position", "align", "offset", "gap", "titleStyle",
26
+ "text", "subtitle", "position", "align", "offset", "alongOffset", "gap", "titleStyle",
23
27
  "subtitleStyle", "maxWidth", "wrap", "lineHeight"
24
28
  ]);
25
29
  const STYLE_OPTIONS = Object.freeze([
26
- "color", "fontSize", "fontFamily", "fontWeight"
30
+ "color", "fontSize", "fontFamily", "fontWeight", "fontStyle"
27
31
  ]);
28
32
  const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
29
33
  const ALIGNS = Object.freeze(["left", "center", "right"]);
@@ -32,6 +36,7 @@ const DEFAULTS = Object.freeze({
32
36
  position: "top",
33
37
  align: "left",
34
38
  offset: 0,
39
+ alongOffset: 0,
35
40
  gap: 8,
36
41
  titleStyle: Object.freeze({
37
42
  color: DEFAULT_COLORS.strongText,
@@ -47,9 +52,12 @@ const DEFAULTS = Object.freeze({
47
52
  })
48
53
  });
49
54
 
50
- export function validateTitleString(value, label) {
55
+ export function validateTitleString(value, label, multiline = false) {
51
56
  validateNonEmptyString(value, label);
52
- if (/\r|\n/u.test(value)) {
57
+ if (multiline && value.split(/\r\n|\r|\n/u).every(line => line === "")) {
58
+ throw new Error(`${label} requires at least one non-empty line.`);
59
+ }
60
+ if (!multiline && /\r|\n/u.test(value)) {
53
61
  throw new Error(`${label} does not accept explicit newlines.`);
54
62
  }
55
63
  return value;
@@ -70,6 +78,7 @@ function normalizeStyle(value, defaults, label) {
70
78
  )) {
71
79
  throw new TypeError(`${label} fontWeight must be a non-empty string or number.`);
72
80
  }
81
+ if (style.fontStyle !== undefined) validateFontStyle(style.fontStyle, label);
73
82
  return style;
74
83
  }
75
84
 
@@ -78,6 +87,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
78
87
  const align = args.align ?? defaults.align;
79
88
  const offset = args.offset ?? defaults.offset;
80
89
  const gap = args.gap ?? defaults.gap;
90
+ const alongOffset = args.alongOffset ?? defaults.alongOffset ?? 0;
81
91
  const titleStyle = normalizeStyle(
82
92
  args.titleStyle,
83
93
  defaults.titleStyle,
@@ -100,6 +110,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
100
110
  if (!Number.isFinite(offset)) {
101
111
  throw new TypeError("Chart title offset must be a finite number.");
102
112
  }
113
+ if (!Number.isFinite(alongOffset)) throw new TypeError("Chart title alongOffset must be a finite number.");
103
114
  validateNonNegativeFinite(gap, "Chart title gap");
104
115
  if (maxWidth !== undefined) {
105
116
  validatePositiveFinite(maxWidth, "Chart title maxWidth");
@@ -121,6 +132,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
121
132
  position,
122
133
  align,
123
134
  offset,
135
+ alongOffset,
124
136
  gap,
125
137
  titleStyle,
126
138
  subtitleStyle,
@@ -132,10 +144,10 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
132
144
 
133
145
  export function normalizeTitleOptions(args) {
134
146
  validateOptionObject(args, OPTIONS, "createTitle");
135
- const text = validateTitleString(args.text, "Chart title text");
147
+ const text = validateTitleString(args.text, "Chart title text", true);
136
148
  const subtitle = args.subtitle === undefined
137
149
  ? undefined
138
- : validateTitleString(args.subtitle, "Chart subtitle");
150
+ : validateTitleString(args.subtitle, "Chart subtitle", true);
139
151
  return {
140
152
  text,
141
153
  subtitle,
@@ -151,12 +163,12 @@ export function normalizeTitleEditOptions(args, previous, semanticTitle) {
151
163
  });
152
164
  const text = args.text === undefined
153
165
  ? semanticTitle.text
154
- : validateTitleString(args.text, "Chart title text");
166
+ : validateTitleString(args.text, "Chart title text", true);
155
167
  const subtitle = args.subtitle === false
156
168
  ? undefined
157
169
  : args.subtitle === undefined
158
170
  ? semanticTitle.subtitle
159
- : validateTitleString(args.subtitle, "Chart subtitle");
171
+ : validateTitleString(args.subtitle, "Chart subtitle", true);
160
172
  if (args.subtitle !== undefined && args.subtitle !== false && typeof args.subtitle !== "string") {
161
173
  throw new TypeError("Chart subtitle must be a non-empty string or false.");
162
174
  }
@@ -175,11 +187,8 @@ export function requireTitleConfig(program) {
175
187
  }
176
188
 
177
189
  function validateLayout(position, titleBounds, plot, canvas) {
178
- if (
179
- titleBounds.left < 0 || titleBounds.top < 0 ||
180
- titleBounds.right > canvas.width || titleBounds.bottom > canvas.height
181
- ) {
182
- throw new Error(`Chart title requires more ${position}-margin space.`);
190
+ if (!textBoundsFitCanvas(titleBounds, canvas)) {
191
+ throw canvasOverflowError(`Chart title requires more ${position}-margin space.`, [titleBounds], canvas);
183
192
  }
184
193
  const outsidePlot = {
185
194
  top: titleBounds.bottom <= plot.y,
@@ -205,19 +214,31 @@ export function resolveTitleLayout(program, config) {
205
214
  throw new Error("Chart title layout requires Canvas bounds and dimensions.");
206
215
  }
207
216
  const block = buildTitleReadingBlock({
208
- text: validateTitleString(program.semanticSpec.title.text, "Chart title text"),
217
+ text: validateTitleString(program.semanticSpec.title.text, "Chart title text", true),
209
218
  subtitle: program.semanticSpec.title.subtitle === undefined
210
219
  ? undefined
211
220
  : validateTitleString(
212
221
  program.semanticSpec.title.subtitle,
213
- "Chart subtitle"
222
+ "Chart subtitle", true
214
223
  )
215
224
  }, config, program.materializationConfigs.textMetrics);
225
+ const automatic = program.materializationConfigs.canvas?.plot !== undefined;
226
+ const near = ["top", "left"].includes(config.position);
216
227
  const horizontal = ["top", "bottom"].includes(config.position);
228
+ let titleEdge = horizontal ? plot.y + (near ? 0 : plot.height)
229
+ : plot.x + (near ? 0 : plot.width);
230
+ if (automatic) {
231
+ const occupied = resolveGuideCollisionBlocks(program.graphicSpec, program.guideConfigs,
232
+ undefined, program.materializationConfigs.textMetrics).filter(item => item.position === config.position);
233
+ const key = horizontal ? (near ? "top" : "bottom") : (near ? "left" : "right");
234
+ for (const item of occupied) titleEdge = near ? Math.min(titleEdge, item.bounds[key]) : Math.max(titleEdge, item.bounds[key]);
235
+ titleEdge += (near ? -1 : 1) * (16 + config.offset);
236
+ }
217
237
  let component;
218
238
  if (horizontal) {
219
- const x = alignedTextAnchor(plot.x, plot.width, config.align);
220
- const blockTop = config.position === "top"
239
+ const x = alignedTextAnchor(plot.x, plot.width, config.align) + (config.alongOffset ?? 0);
240
+ const blockTop = automatic ? titleEdge - (near ? block.height : 0)
241
+ : config.position === "top"
221
242
  ? 16 + config.offset
222
243
  : plot.y + plot.height + config.offset;
223
244
  component = (lines, centers) => ({
@@ -230,8 +251,9 @@ export function resolveTitleLayout(program, config) {
230
251
  });
231
252
  } else {
232
253
  const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
233
- const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
234
- const edge = config.position === "left"
254
+ const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align) + (config.alongOffset ?? 0);
255
+ const edge = automatic ? titleEdge + (near ? -block.height : block.height)
256
+ : config.position === "left"
235
257
  ? 16 + config.offset
236
258
  : canvas.properties.width - 16 + config.offset;
237
259
  const mapX = value => config.position === "left" ? edge + value : edge - value;
@@ -2,7 +2,7 @@ import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
4
  import { findDataset } from "../../selectors/datasets.js";
5
- import { findLayer } from "../../selectors/layers.js";
5
+ import { findLayer, resolveConfiguredOwner } from "../../selectors/layers.js";
6
6
  import { rebindDistributionGuides } from "../distributions/revision.js";
7
7
  import {
8
8
  resolveViolinDensity,
@@ -15,24 +15,7 @@ const OPTIONS = Object.freeze([
15
15
  "target", "data", "x", "y", "split", "density"
16
16
  ]);
17
17
 
18
- function resolveOwner(program, requested) {
19
- const eligible = program.semanticSpec.layers.filter(
20
- layer => program.markConfigs[layer.id]?.violinPlot?.materialized === true
21
- );
22
- if (requested !== undefined) {
23
- const id = validateUserId(requested, "Violin-plot owner id");
24
- const layer = findLayer(program, id);
25
- if (layer === undefined || !eligible.includes(layer)) {
26
- throw new Error(`Unknown violin-plot owner "${id}".`);
27
- }
28
- return layer;
29
- }
30
- const current = findLayer(program, program.context.currentMark);
31
- if (current !== undefined && eligible.includes(current)) return current;
32
- if (eligible.length === 1) return eligible[0];
33
- if (eligible.length === 0) throw new Error(`${OPERATION} requires a violin plot.`);
34
- throw new Error(`${OPERATION} target is ambiguous; provide target.`);
35
- }
18
+
36
19
 
37
20
  function currentPosition(owner, current, channel) {
38
21
  const categorical = current.orientation === "vertical"
@@ -84,7 +67,7 @@ export const editViolinPlot = /* @__PURE__ */ action(
84
67
  if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
85
68
  throw new Error(`${OPERATION} requires at least one violin-plot option.`);
86
69
  }
87
- const owner = resolveOwner(this, args.target);
70
+ const owner = resolveConfiguredOwner(this, args.target, { config: "violinPlot", operation: OPERATION, kind: "violin", materialized: true }, validateUserId);
88
71
  const current = this.markConfigs[owner.id].violinPlot;
89
72
  const source = Object.hasOwn(args, "data")
90
73
  ? validateUserId(args.data, "Violin-plot data id")