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
@@ -1,3 +1,5 @@
1
+ import { selectCartesianAxis } from "../../../selectors/axes.js";
2
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
3
  import { action } from "../../../core/action.js";
2
4
  import { validateKeys } from "../../../core/validation.js";
3
5
  import { axisGraphicIds } from
@@ -14,16 +16,19 @@ function makeRemoveAxis(channel) {
14
16
  ? "Theta"
15
17
  : "Radial";
16
18
  const operation = `remove${prefix}Axis`;
17
- const graphicIds = axisGraphicIds(channel);
18
19
  return action(
19
20
  {
20
21
  op: operation,
21
22
  description: `Remove the complete ${channel}-axis resource.`
22
23
  },
23
- function (args = {}) {
24
- validateKeys(args, OPTIONS, operation);
25
- const semantic = this.semanticSpec.guides.axis?.[channel];
26
- const config = this.guideConfigs.axis?.[channel];
24
+ withGuideLayoutValidation(function (args = {}) {
25
+ const cartesian = ["x", "y"].includes(channel);
26
+ validateKeys(args, cartesian ? [...OPTIONS, "target"] : OPTIONS, operation);
27
+ const selected = cartesian ? selectCartesianAxis(this, channel, args, { whole: true }) : { id: channel, options: args };
28
+ const axisId = selected.id; args = selected.options;
29
+ const graphicIds = axisGraphicIds(channel, axisId);
30
+ const semantic = this.semanticSpec.guides.axis?.[axisId];
31
+ const config = this.guideConfigs.axis?.[axisId];
27
32
  const hasGraphic = graphicIds.some(
28
33
  id => this.graphicSpec.objects[id] !== undefined
29
34
  );
@@ -41,7 +46,7 @@ function makeRemoveAxis(channel) {
41
46
  let next = semantic === undefined
42
47
  ? this
43
48
  : this.editSemantic({
44
- property: `guide.axis.${channel}`,
49
+ property: `guide.axis.${axisId}`,
45
50
  remove: true
46
51
  });
47
52
  for (const id of graphicIds) {
@@ -49,8 +54,8 @@ function makeRemoveAxis(channel) {
49
54
  next = next.editGraphics({ target: id, remove: true });
50
55
  }
51
56
  }
52
- return next._withoutMaterializationConfig(["guides", "axis", channel]);
53
- }
57
+ return next._withoutMaterializationConfig(["guides", "axis", axisId]);
58
+ })
54
59
  );
55
60
  }
56
61
 
@@ -3,7 +3,7 @@ import { action } from "../../../core/action.js";
3
3
  import { isPlainObject } from "../../../core/immutable.js";
4
4
  import { validateKeys, validateOptionObject } from "../../../core/validation.js";
5
5
 
6
- const CREATE_OPTIONS = Object.freeze([
6
+ const CREATE_OPTIONS = Object.freeze(["target",
7
7
  "scale",
8
8
  "position",
9
9
  "count",
@@ -11,14 +11,14 @@ const CREATE_OPTIONS = Object.freeze([
11
11
  "ticks",
12
12
  "labels"
13
13
  ]);
14
- const EDIT_OPTIONS = Object.freeze([
14
+ const EDIT_OPTIONS = Object.freeze(["target",
15
15
  "position",
16
16
  "count",
17
17
  "values",
18
18
  "ticks",
19
19
  "labels"
20
20
  ]);
21
- const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth"]);
21
+ const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
22
22
  const LABEL_OPTIONS = Object.freeze([
23
23
  "offset",
24
24
  "format",
@@ -31,10 +31,14 @@ const LABEL_OPTIONS = Object.freeze([
31
31
  "wrap",
32
32
  "lineHeight",
33
33
  "overlap",
34
- "labelMap"
34
+ "labelMap",
35
+ "alongOffset",
36
+ "flush",
37
+ "colorMap",
38
+ "align"
35
39
  ]);
36
- const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
37
- const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
40
+ const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
41
+ const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
38
42
 
39
43
  function validateNested(value, supported, label) {
40
44
  if (!isPlainObject(value)) {
@@ -101,7 +105,7 @@ function makeCreate(channel) {
101
105
  const shared = select(args, SHARED_CREATE);
102
106
  const tickArgs = { ...shared, ...(args.ticks ?? {}) };
103
107
  const labelArgs = {
104
- ...select(shared, ["scale", "position"]),
108
+ ...select(shared, ["target", "scale", "position"]),
105
109
  ...(args.labels ?? {})
106
110
  };
107
111
 
@@ -1,3 +1,7 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
+ import { canvasOverflowError } from "../../../layout/canvas.js";
4
+ import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
1
5
  import { editGraphicProperties } from "../../primitives/graphicProperties.js";
2
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
3
7
  import { applyMaterializationPlan } from "../../../materialization/planner.js";
@@ -10,7 +14,7 @@ import {
10
14
  validateNonNegativeFinite,
11
15
  validateOptionObject
12
16
  } from "../../../core/validation.js";
13
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
17
+ import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
14
18
  import {
15
19
  isTransformedScaleType,
16
20
  mapContinuousScaleValues,
@@ -31,7 +35,7 @@ import {
31
35
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
32
36
  "../../../materialization/graphicHierarchy.js";
33
37
 
34
- const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth"]);
38
+ const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
35
39
  const DEFAULTS = Object.freeze({
36
40
  count: DEFAULT_TICK_COUNT,
37
41
  length: 6,
@@ -45,6 +49,7 @@ function validateOptions(args, operation, create) {
45
49
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
46
50
  operation
47
51
  );
52
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
48
53
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
49
54
  if (Array.isArray(args.values)) {
50
55
  validateGeneratedItemLimit(args.values.length, "Tick value count");
@@ -52,6 +57,8 @@ function validateOptions(args, operation, create) {
52
57
  }
53
58
 
54
59
  function validateConfig(channel, config) {
60
+ if (config.strokeDash !== undefined) validateGuideDash(config.strokeDash, "Tick strokeDash");
61
+ if (config.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
55
62
  validateAxisPosition(channel, config.position);
56
63
  if (config.mode === "count") {
57
64
  if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Tick count must be a positive integer.");
@@ -65,18 +72,15 @@ function validateConfig(channel, config) {
65
72
  (typeof value === "number" && Number.isFinite(value))
66
73
  ))
67
74
  ) throw new TypeError("Tick values must be nominal values or finite numbers.");
75
+ if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
68
76
  validateNonNegativeFinite(config.length, "Tick length");
69
77
  validateNonNegativeFinite(config.lineWidth, "Tick lineWidth");
70
78
  validateNonEmptyString(config.color, "Tick color");
71
79
  }
72
80
 
73
- function geometry(program, channel, config) {
81
+ function geometry(program, channel, config, axisId = channel) {
74
82
  const scale = program.resolvedScales[config.scale];
75
- const bounds = resolveGuideCoordinateBounds(program, {
76
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
77
- scale: config.scale,
78
- channel
79
- });
83
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
80
84
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
81
85
  if ((
82
86
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
@@ -102,7 +106,7 @@ function geometry(program, channel, config) {
102
106
  bounds,
103
107
  channel,
104
108
  position: config.position,
105
- positions,
109
+ positions: positions.map(value => value + (config.alongOffset ?? 0)),
106
110
  length: config.length
107
111
  })
108
112
  };
@@ -127,7 +131,7 @@ function geometry(program, channel, config) {
127
131
  coordinates.some(value => !Number.isFinite(value)) ||
128
132
  !fits
129
133
  ) {
130
- throw new Error(`The ${channel}-axis ticks do not fit the Canvas margin.`);
134
+ throw canvasOverflowError(`The ${channel}-axis ticks do not fit the Canvas margin.`, [{ left: left - strokeExtent, right: right + strokeExtent, top: top - strokeExtent, bottom: bottom + strokeExtent }], canvas);
131
135
  }
132
136
  return resolved;
133
137
  }
@@ -136,9 +140,11 @@ function makeEdit(channel) {
136
140
  const op = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
137
141
  return action({ op, description: `Edit concrete ${channel}-axis ticks.` }, withGuideLayoutValidation(function (args = {}) {
138
142
  validateOptions(args, op, false);
139
- const id = `${channel}AxisTicks`;
143
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
144
+ const axisId = selected.id; args = selected.options;
145
+ const id = axisGraphicIds(channel, axisId)[1];
140
146
  if (this.graphicSpec.objects[id]?.type !== "line") throw new Error(`${op} requires existing axis ticks.`);
141
- const previous = this.guideConfigs.axis?.[channel]?.ticks;
147
+ const previous = this.guideConfigs.axis?.[axisId]?.ticks;
142
148
  if (!previous) throw new Error(`${op} requires tick configuration.`);
143
149
  const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
144
150
  const inferredValues = !explicitMode && previous.inferredValues === true
@@ -152,24 +158,28 @@ function makeEdit(channel) {
152
158
  const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
153
159
  ? "values"
154
160
  : Object.hasOwn(args, "count") ? "count" : previous.mode;
155
- const config = {
161
+ const config = patchGuideDash({
156
162
  ...previous,
157
163
  ...args,
158
164
  ...(inferredValues === undefined ? {} : { values: inferredValues }),
159
165
  ...(explicitMode ? { inferredValues: false } : {}),
160
166
  mode
161
- };
167
+ }, args);
162
168
  if (mode === "values") delete config.count; else delete config.values;
163
169
  validateConfig(channel, config);
164
- const resolved = geometry(this, channel, config);
165
- let next = this._withGuideConfig(channel, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
170
+ const resolved = geometry(this, channel, config, axisId);
171
+ let next = this._withGuideConfig(axisId, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
166
172
  for (const property of ["x1", "y1", "x2", "y2"]) next = next.editGraphics({ target: id, property, value: resolved[property] });
167
173
  next = editGraphicProperties(next, id, {
168
174
  stroke: config.color,
169
175
  strokeWidth: config.lineWidth
170
176
  });
177
+ if (config.strokeDash !== undefined || config.strokeDashMap !== undefined ||
178
+ previous.strokeDashMap !== undefined) {
179
+ next = next.editGraphics({ target: id, property: "strokeDash", value: resolveGuideDashes(resolved.values, config) });
180
+ }
171
181
  // Structural dependency plans own grids during a scale/role handoff.
172
- return explicitMode ? applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel)) : next;
182
+ return explicitMode ? applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId)) : next;
173
183
  }));
174
184
  }
175
185
 
@@ -180,10 +190,12 @@ function makeCreate(channel) {
180
190
  const edit = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
181
191
  return action({ op, description: `Create concrete ${channel}-axis ticks.` }, withGuideLayoutValidation(function (args = {}) {
182
192
  validateOptions(args, op, true);
193
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
194
+ const axisId = selected.id; args = selected.options;
183
195
  const scale = validateUserId(args.scale ?? channel, "Scale id");
184
- const existingGuide = this.semanticSpec.guides.axis?.[channel]?.scale;
196
+ const existingGuide = this.semanticSpec.guides.axis?.[axisId]?.scale;
185
197
  if (existingGuide && existingGuide !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
186
- const id = `${channel}AxisTicks`;
198
+ const id = axisGraphicIds(channel, axisId)[1];
187
199
  if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis ticks.`);
188
200
  const resolvedScale = this.resolvedScales[scale];
189
201
  const inferredValues =
@@ -194,18 +206,18 @@ function makeCreate(channel) {
194
206
  : inferHistogramBoundaries(this, channel, scale);
195
207
  const options =
196
208
  inferredValues === undefined ? args : { ...args, values: inferredValues };
197
- const config = { scale, position: defaultAxisPosition(channel), length: DEFAULTS.length, color: DEFAULTS.color, lineWidth: DEFAULTS.lineWidth, ...options, inferredValues: inferredValues !== undefined, mode: Object.hasOwn(options, "values") ? "values" : "count" };
209
+ const config = patchGuideDash({ scale, position: defaultAxisPosition(channel), length: DEFAULTS.length, color: DEFAULTS.color, lineWidth: DEFAULTS.lineWidth, ...options, inferredValues: inferredValues !== undefined, mode: Object.hasOwn(options, "values") ? "values" : "count" }, args);
198
210
  if (config.mode === "values") delete config.count; else config.count ??= DEFAULTS.count;
199
- validateConfig(channel, config); geometry(this, channel, config);
200
- const next = this.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
211
+ validateConfig(channel, config); geometry(this, channel, config, axisId);
212
+ const next = this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
201
213
  .createGraphics({
202
214
  id,
203
215
  type: "line",
204
216
  length: 0,
205
217
  ...resolvePlotGraphicPlacement(this)
206
218
  })
207
- ._withGuideConfig(channel, config)[edit]();
208
- return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel));
219
+ ._withGuideConfig(axisId, config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
220
+ return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId));
209
221
  }));
210
222
  }
211
223
 
@@ -1,19 +1,24 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
+ import { materializeTextLines } from "../../../materialization/textLines.js";
4
+ import { axisTitleTextLines as titleLines, validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
5
+ import { axisThemeTypography } from "../../theme/state.js";
6
+ import { canvasOverflowError } from "../../../layout/canvas.js";
1
7
  import { isSourceOwnedText } from "../../../grammar/text.js";
2
8
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
3
9
  import { action } from "../../../core/action.js";
4
10
  import { validateUserId } from "../../../core/identifiers.js";
5
11
  import {
6
12
  validateKeys,
7
- validateNonEmptyString,
8
- validateNonNegativeFinite
13
+ validateNonEmptyString
9
14
  } from "../../../core/validation.js";
10
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
15
+ import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
11
16
  import {
12
17
  isTransformedScaleType,
13
18
  mapContinuousScaleValues,
14
19
  mapOrdinalPositionValues
15
20
  } from "../../../grammar/scales/index.js";
16
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
21
+ import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
17
22
  "../../../theme/defaults.js";
18
23
  import { findDataset } from "../../../selectors/datasets.js";
19
24
  import { formatAggregateTitle } from "../../../grammar/aggregate.js";
@@ -40,24 +45,49 @@ function titleBounds(geometry, config, text, profile) {
40
45
  return resolveTextBounds({
41
46
  ...geometry,
42
47
  text,
48
+ lines: titleLines(text, config),
43
49
  fontSize: config.fontSize,
44
50
  fontFamily: config.fontFamily,
45
51
  fontWeight: config.fontWeight,
46
- textAlign: "center",
47
- textBaseline: "middle",
52
+ textAlign: config.align ?? "center",
53
+ textBaseline: config.baseline ?? "middle",
48
54
  rotation: config.rotation
49
55
  }, profile);
50
56
  }
51
57
 
52
- function inferredTitleOffset(program, channel, config) {
53
- const plot = resolveGuideCoordinateBounds(program, {
54
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
55
- scale: config.scale,
56
- channel
57
- });
58
+ function resolveTitleAlong(scale, config) {
59
+ const discrete = ["ordinal", "band", "point"].includes(scale?.type);
60
+ let along;
61
+ if (config.at === "center") {
62
+ along = finiteMidpoint(scale.range[0], scale.range[1]);
63
+ } else if (config.at === "start" || config.at === "end") {
64
+ along = scale.range[config.at === "start" ? 0 : 1];
65
+ } else {
66
+ if (discrete) {
67
+ if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
68
+ along = mapOrdinalPositionValues([config.at], scale)[0];
69
+ } else {
70
+ const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
71
+ if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
72
+ along = mapContinuousScaleValues([config.at], scale)[0];
73
+ }
74
+ }
75
+ return along + (config.alongOffset ?? 0);
76
+ }
77
+
78
+ function axisLabelBounds(program, channel, axisId = channel) {
79
+ const id = axisGraphicIds(channel, axisId)[2], labels = program.graphicSpec.objects[id];
80
+ const bounds = target => resolveConcreteGraphicBounds(
81
+ program.graphicSpec, target, program.materializationConfigs.textMetrics
82
+ );
83
+ return labels?.items?.map(item => bounds(item.id)) ?? (labels ? [bounds(id)] : []);
84
+ }
85
+
86
+ function inferredTitleOffset(program, channel, config, axisId = channel) {
87
+ const plot = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
58
88
  const canvas = findCanvasGraphic(program)?.properties;
59
- const labels = program.graphicSpec.objects[`${channel}AxisLabels`]
60
- ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`, program.materializationConfigs.textMetrics)
89
+ const labels = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[2]]
90
+ ? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[2], program.materializationConfigs.textMetrics)
61
91
  : undefined;
62
92
  const preferred = channel === "x" ? 42 : 52;
63
93
  if (!plot || !canvas || !labels) return preferred;
@@ -71,24 +101,43 @@ function inferredTitleOffset(program, channel, config) {
71
101
  position: config.position,
72
102
  along: 0,
73
103
  offset: preferred
74
- }), config, program.semanticSpec.guides.axis?.[channel]?.title ?? "", program.materializationConfigs.textMetrics);
104
+ }), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
75
105
  const needed = positive
76
106
  ? labels[far] + 4 - bounds[near]
77
107
  : bounds[far] - labels[near] + 4;
78
108
  const available = positive
79
109
  ? canvas[horizontal ? "height" : "width"] - bounds[far]
80
110
  : bounds[near];
81
- return preferred + Math.min(available, Math.max(0, needed));
111
+ // Font ink can extend beyond the one-em layout box, especially after
112
+ // rotating a middle-baseline title. Keep inferred edge-clamped placement
113
+ // inside the Canvas; explicit offsets retain their exact geometry.
114
+ const inkClearance = channel === "y" &&
115
+ Math.abs(Math.cos(config.rotation)) < 1e-10
116
+ ? config.fontSize / 4 : 0;
117
+ if (program.materializationConfigs.canvas?.plot !== undefined) {
118
+ return preferred + Math.max(0, needed);
119
+ }
120
+ const legacyAdjustment = Math.min(available, Math.max(0, needed));
121
+ if (inkClearance === 0) return preferred + legacyAdjustment;
122
+ const adjustment = Math.min(available - inkClearance, Math.max(0, needed));
123
+ const candidateBounds = titleBounds(resolveAxisTitleGeometry({
124
+ bounds: plot, channel, position: config.position,
125
+ along: resolveTitleAlong(program.resolvedScales[config.scale], config),
126
+ offset: preferred + adjustment
127
+ }), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
128
+ // Optional ink reserve must not introduce a label collision.
129
+ return preferred + (axisLabelBounds(program, channel, axisId).some(label => label && textBoundsIntersect(candidateBounds, label))
130
+ ? legacyAdjustment : adjustment);
82
131
  }
83
132
 
84
- const CREATE_OPTIONS = [
133
+ const CREATE_OPTIONS = ["target",
85
134
  "text", "scale", "position", "at", "offset", "rotation", "color",
86
- "fontSize", "fontFamily", "fontWeight"
135
+ "fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
87
136
  ];
88
137
  const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
89
138
  const DEFAULTS = {
90
139
  color: DEFAULT_COLORS.text,
91
- fontSize: 13,
140
+ fontSize: AXIS_TITLE_FONT_SIZE,
92
141
  fontFamily: DEFAULT_FONT_FAMILY,
93
142
  fontWeight: 600
94
143
  };
@@ -100,7 +149,7 @@ function validateText(text) {
100
149
  function validateConfig(channel, config) {
101
150
  validateAxisPosition(channel, config.position);
102
151
  if (!["start", "center", "end"].includes(config.at) && !Number.isFinite(config.at)) throw new TypeError("Axis title at must be start, center, end, or a finite number.");
103
- validateNonNegativeFinite(config.offset, "Axis title offset");
152
+ validateAxisTextPlacement(config, { title: true });
104
153
  if (!Number.isFinite(config.rotation)) throw new TypeError("Axis title rotation must be finite radians.");
105
154
  validateAxisTextStyle(config, "Axis title");
106
155
  }
@@ -149,37 +198,19 @@ export function inferAxisTitleText(program, channel, scaleId) {
149
198
  }
150
199
  }
151
200
  if (primaryTitles.size === 1) return [...primaryTitles][0];
201
+ if (titles.size === 0 && axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) return scaleId;
152
202
  if (titles.size !== 1) throw new Error(`Axis title text cannot be inferred for scale "${scaleId}".`);
153
203
  return [...titles][0];
154
204
  }
155
205
 
156
- function resolveGeometry(program, channel, config) {
206
+ function resolveGeometry(program, channel, config, axisId = channel) {
157
207
  const scale = program.resolvedScales[config.scale];
158
- const bounds = resolveGuideCoordinateBounds(program, {
159
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
160
- scale: config.scale,
161
- channel
162
- });
163
- const discrete = ["ordinal", "band", "point"].includes(scale?.type);
208
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
164
209
  if ((
165
210
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
166
211
  !isTransformedScaleType(scale?.type)
167
212
  ) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
168
- let along;
169
- if (config.at === "center") {
170
- along = finiteMidpoint(scale.range[0], scale.range[1]);
171
- } else if (config.at === "start" || config.at === "end") {
172
- along = scale.range[config.at === "start" ? 0 : 1];
173
- } else {
174
- if (discrete) {
175
- if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
176
- along = mapOrdinalPositionValues([config.at], scale)[0];
177
- } else {
178
- const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
179
- if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
180
- along = mapContinuousScaleValues([config.at], scale)[0];
181
- }
182
- }
213
+ const along = resolveTitleAlong(scale, config);
183
214
  const geometry = resolveAxisTitleGeometry({
184
215
  bounds,
185
216
  channel,
@@ -190,16 +221,14 @@ function resolveGeometry(program, channel, config) {
190
221
  const resolvedBounds = titleBounds(
191
222
  geometry,
192
223
  config,
193
- program.semanticSpec.guides.axis?.[channel]?.title ?? ""
224
+ program.semanticSpec.guides.axis?.[axisId]?.title ?? ""
194
225
  , program.materializationConfigs.textMetrics);
195
226
  const canvas = findCanvasGraphic(program)?.properties;
196
227
  if (!canvas || !textBoundsFitCanvas(resolvedBounds, canvas)) {
197
- throw new Error(`The ${channel}-axis title does not fit the Canvas margin.`);
228
+ throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
198
229
  }
199
- const labelsBounds = program.graphicSpec.objects[`${channel}AxisLabels`]
200
- ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`, program.materializationConfigs.textMetrics)
201
- : undefined;
202
- if (labelsBounds && textBoundsIntersect(resolvedBounds, labelsBounds)) {
230
+ const labelBounds = axisLabelBounds(program, channel, axisId);
231
+ if (labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
203
232
  throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
204
233
  }
205
234
  return geometry;
@@ -207,15 +236,19 @@ function resolveGeometry(program, channel, config) {
207
236
 
208
237
  function names(channel) {
209
238
  const prefix = channel === "x" ? "X" : "Y";
210
- return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle`, graphic: `${channel}AxisTitle` };
239
+ return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle` };
211
240
  }
212
241
 
213
242
  function makeEdit(channel) {
214
243
  const operation = names(channel);
215
244
  return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
216
245
  validateKeys(args, EDIT_OPTIONS, operation.edit);
217
- if (this.graphicSpec.objects[operation.graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
218
- const previous = this.guideConfigs.axis?.[channel]?.title;
246
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
247
+ const axisId = selected.id; args = selected.options;
248
+ const graphic = axisGraphicIds(channel, axisId)[3];
249
+ validateAxisTextPlacement(args, { title: true });
250
+ if (this.graphicSpec.objects[graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
251
+ const previous = this.guideConfigs.axis?.[axisId]?.title;
219
252
  if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
220
253
  const { text, rotation: requestedRotation, ...appearance } = args;
221
254
  const explicitText = Object.hasOwn(args, "text");
@@ -245,27 +278,27 @@ function makeEdit(channel) {
245
278
  ? validateText(text)
246
279
  : config.inferredText === true
247
280
  ? inferAxisTitleText(this, channel, config.scale)
248
- : this.semanticSpec.guides.axis?.[channel]?.title;
249
- if (resolvedTitle !== this.semanticSpec.guides.axis?.[channel]?.title) {
281
+ : this.semanticSpec.guides.axis?.[axisId]?.title;
282
+ if (resolvedTitle !== this.semanticSpec.guides.axis?.[axisId]?.title) {
250
283
  next = next.editSemantic({
251
- property: `guide.axis.${channel}.title`,
284
+ property: `guide.axis.${axisId}.title`,
252
285
  value: validateText(resolvedTitle)
253
286
  });
254
287
  }
255
- const resolvedText = validateText(next.semanticSpec.guides.axis?.[channel]?.title);
288
+ const resolvedText = validateText(next.semanticSpec.guides.axis?.[axisId]?.title);
256
289
  if (config.inferredOffset) {
257
- config.offset = inferredTitleOffset(next, channel, config);
290
+ config.offset = inferredTitleOffset(next, channel, config, axisId);
258
291
  }
259
- const geometry = resolveGeometry(next, channel, config);
260
- next = next._withGuideConfig(channel, "title", config);
292
+ const geometry = resolveGeometry(next, channel, config, axisId);
293
+ next = next._withGuideConfig(axisId, "title", config);
261
294
  const properties = {
262
295
  x: geometry.x, y: geometry.y, text: resolvedText, fill: config.color,
263
296
  fontSize: config.fontSize, fontFamily: config.fontFamily,
264
- fontWeight: config.fontWeight, textAlign: "center", textBaseline: "middle",
297
+ fontWeight: config.fontWeight, textAlign: config.align ?? "center", textBaseline: config.baseline ?? "middle",
265
298
  rotation: config.rotation
266
299
  };
267
- for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: operation.graphic, property, value });
268
- return next;
300
+ for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: graphic, property, value });
301
+ return materializeTextLines(next, graphic, titleLines(resolvedText, config));
269
302
  }));
270
303
  }
271
304
 
@@ -276,6 +309,10 @@ function makeCreate(channel) {
276
309
  const operation = names(channel);
277
310
  return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
278
311
  validateKeys(args, CREATE_OPTIONS, operation.create);
312
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
313
+ const axisId = selected.id; args = selected.options;
314
+ const graphic = axisGraphicIds(channel, axisId)[3];
315
+ validateAxisTextPlacement(args, { title: true });
279
316
  const {
280
317
  text: requestedText,
281
318
  scale: requestedScale,
@@ -284,9 +321,9 @@ function makeCreate(channel) {
284
321
  ...appearance
285
322
  } = args;
286
323
  const scale = validateUserId(requestedScale ?? channel, "Scale id");
287
- const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
324
+ const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
288
325
  if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
289
- if (this.graphicSpec.objects[operation.graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
326
+ if (this.graphicSpec.objects[graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
290
327
  const inferredText = !Object.hasOwn(args, "text");
291
328
  const text = validateText(
292
329
  requestedText ?? inferAxisTitleText(this, channel, scale)
@@ -309,21 +346,22 @@ function makeCreate(channel) {
309
346
  fontSize: DEFAULTS.fontSize,
310
347
  fontFamily: DEFAULTS.fontFamily,
311
348
  fontWeight: DEFAULTS.fontWeight,
349
+ ...axisThemeTypography(this.materializationConfigs.theme, "title"),
312
350
  ...appearance
313
351
  };
314
352
  let next = this
315
- .editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
316
- .editSemantic({ property: `guide.axis.${channel}.title`, value: text });
317
- if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config);
353
+ .editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
354
+ .editSemantic({ property: `guide.axis.${axisId}.title`, value: text });
355
+ if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config, axisId);
318
356
  validateConfig(channel, config);
319
- resolveGeometry(next, channel, config);
357
+ resolveGeometry(next, channel, config, axisId);
320
358
  return next
321
359
  .createGraphics({
322
- id: operation.graphic,
360
+ id: graphic,
323
361
  type: "text",
324
362
  ...resolvePlotGraphicPlacement(this)
325
363
  })
326
- ._withGuideConfig(channel, "title", config)[operation.edit]();
364
+ ._withGuideConfig(axisId, "title", config)[operation.edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
327
365
  }));
328
366
  }
329
367
 
@@ -1,3 +1,4 @@
1
+ import { registerStrokeColorLegendActions } from "./legends/continuous/stroke.js";
1
2
  import { registerBasicCartesianAxisActions } from "./axes/index.js";
2
3
  import { registerGuideCollectionActions } from "./guides.js";
3
4
  import { registerBasicGridActions } from "./grids/index.js";
@@ -15,6 +16,7 @@ export function registerBasicGuideActions(ProgramClass) {
15
16
  registerBasicCategoricalLegendActions(ProgramClass);
16
17
  registerGradientLegendActions(ProgramClass);
17
18
  registerIntervalLegendActions(ProgramClass);
19
+ registerStrokeColorLegendActions(ProgramClass);
18
20
  registerSizeLegendActions(ProgramClass);
19
21
  registerBasicGridActions(ProgramClass);
20
22
  registerGuideCollectionActions(ProgramClass);