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,15 +1,20 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
+ import { axisThemeTypography } from "../../theme/state.js";
4
+ import { validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
5
+ import { canvasOverflowError } from "../../../layout/canvas.js";
1
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
7
  import { action } from "../../../core/action.js";
3
8
  import { validateUserId } from "../../../core/identifiers.js";
4
9
  import {
5
10
  sameOrderedValues,
6
11
  validateGeneratedItemLimit,
12
+ validateTextStyle,
7
13
  validateOptionObject,
8
- validateNonEmptyString,
9
14
  validateNonNegativeFinite,
10
15
  validatePositiveFinite
11
16
  } from "../../../core/validation.js";
12
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
17
+ import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
13
18
  import {
14
19
  isTransformedScaleType,
15
20
  mapContinuousScaleValues,
@@ -19,7 +24,7 @@ import {
19
24
  import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
20
25
  import { resolveRotation } from "../../../grammar/rotation.js";
21
26
  import { valuesFromTickConfig } from "../tickValues.js";
22
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
27
+ import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
23
28
  "../../../theme/defaults.js";
24
29
  import {
25
30
  defaultAxisPosition,
@@ -40,19 +45,21 @@ import { resolveConcreteGraphicBounds } from
40
45
  import { wrapText } from "../../../layout/text.js";
41
46
  import {
42
47
  normalizeDisplayLabelMap,
48
+ normalizeDisplayColorMap,
49
+ resolveDisplayLabelColor,
43
50
  resolveDisplayLabel
44
51
  } from "../../../grammar/displayLabels.js";
45
52
 
46
53
  const OPTIONS = [
47
- "scale", "position", "count", "values", "offset", "format", "color",
54
+ "target", "scale", "position", "count", "values", "offset", "format", "color",
48
55
  "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
49
- "lineHeight", "overlap", "labelMap"
56
+ "lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
50
57
  ];
51
58
 
52
59
  const DEFAULTS = {
53
60
  count: 5,
54
61
  color: DEFAULT_COLORS.text,
55
- fontSize: 12,
62
+ fontSize: AXIS_LABEL_FONT_SIZES.cartesian,
56
63
  fontFamily: DEFAULT_FONT_FAMILY,
57
64
  fontWeight: "normal",
58
65
  rotation: 0,
@@ -78,28 +85,24 @@ function normalizeLabelLayout(config, args, operation) {
78
85
  return next;
79
86
  }
80
87
 
81
- export function validateAxisTextStyle(config, label) {
82
- validateNonEmptyString(config.color, `${label} color`);
83
- validatePositiveFinite(config.fontSize, `${label} fontSize`);
84
- validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
85
- if (typeof config.fontWeight !== "string" &&
86
- !Number.isFinite(config.fontWeight)) {
87
- throw new TypeError(`${label} fontWeight must be a string or number.`);
88
- }
89
- }
88
+ export { validateTextStyle as validateAxisTextStyle };
90
89
 
91
90
  function validateOptions(args, operation, create) {
91
+ validateAxisTextPlacement(args);
92
92
  validateOptionObject(
93
93
  args,
94
94
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
95
95
  operation
96
96
  );
97
+ if (Object.hasOwn(args, "flush") && typeof args.flush !== "boolean") throw new TypeError("Label flush must be a boolean.");
98
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
97
99
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
98
100
  throw new Error(`${operation} cannot use count and values together.`);
99
101
  }
100
102
  }
101
103
 
102
104
  function validateConfig(channel, config) {
105
+ validateAxisTextPlacement(config);
103
106
  validateAxisPosition(channel, config.position);
104
107
  if (config.mode === "count") {
105
108
  if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
@@ -116,9 +119,11 @@ function validateConfig(channel, config) {
116
119
  if (config.mode === "values") {
117
120
  validateGeneratedItemLimit(config.values.length, "Label value count");
118
121
  }
122
+ if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
119
123
  validateNonNegativeFinite(config.offset, "Label offset");
120
- validateAxisTextStyle(config, "Label");
124
+ validateTextStyle(config, "Label");
121
125
  validateAxisFormat(config.format);
126
+ if (config.flush === true && config.rotation !== 0) throw new Error("Label flush requires unrotated Cartesian labels.");
122
127
  if (!Number.isFinite(config.rotation)) {
123
128
  throw new TypeError("Label rotation must resolve to finite radians.");
124
129
  }
@@ -140,24 +145,30 @@ function validateConfig(channel, config) {
140
145
  }
141
146
  }
142
147
 
143
- function normalizeAxisLabelMap(program, config, args, operation) {
144
- if (!Object.hasOwn(args, "labelMap")) return config;
145
- const scale = program.resolvedScales[config.scale];
146
- if (!["ordinal", "band", "point"].includes(scale?.type)) {
147
- throw new Error(`${operation} labelMap requires a categorical scale.`);
148
- }
148
+ function normalizeAxisLabelMaps(program, config, args, operation) {
149
149
  const next = { ...config };
150
- if (args.labelMap === "auto") {
151
- delete next.labelMap;
152
- } else {
153
- next.labelMap = normalizeDisplayLabelMap(
154
- args.labelMap,
155
- `${operation} labelMap`
156
- );
150
+ for (const [property, normalize] of [
151
+ ["labelMap", normalizeDisplayLabelMap],
152
+ ["colorMap", normalizeDisplayColorMap]
153
+ ]) {
154
+ if (!Object.hasOwn(args, property)) continue;
155
+ if (args[property] === "auto") delete next[property];
156
+ else next[property] = normalize(args[property], `${operation} ${property}`);
157
157
  }
158
+ validateAxisLabelMapScale(next, program.resolvedScales[config.scale], operation);
158
159
  return next;
159
160
  }
160
161
 
162
+ function validateAxisLabelMapScale(config, scale, operation = "Axis") {
163
+ if (["ordinal", "band", "point"].includes(scale?.type)) return;
164
+ for (const property of ["labelMap", "colorMap"]) {
165
+ if (config[property] !== undefined &&
166
+ !config[property].every(entry => Number.isFinite(entry.value))) {
167
+ throw new TypeError(`${operation} continuous ${property} values must be finite numbers or timestamps.`);
168
+ }
169
+ }
170
+ }
171
+
161
172
  function assertTickCompatibility(ticks, config, operation) {
162
173
  if (!ticks) return;
163
174
  if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
@@ -218,23 +229,17 @@ function unionLabelBounds(records) {
218
229
  return [...byGroup.values()];
219
230
  }
220
231
 
221
- function resolve(program, channel, config) {
232
+ function resolve(program, channel, config, axisId = channel) {
222
233
  const scale = program.resolvedScales[config.scale];
223
- const bounds = resolveGuideCoordinateBounds(program, {
224
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
225
- scale: config.scale,
226
- channel
227
- });
234
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
228
235
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
229
- if (config.labelMap !== undefined && !discrete) {
230
- throw new Error("Axis labelMap requires a categorical scale.");
231
- }
236
+ validateAxisLabelMapScale(config, scale);
232
237
  if ((
233
238
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
234
239
  !isTransformedScaleType(scale?.type)
235
240
  ) || !bounds) throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");
236
241
  if (discrete && config.mode !== "values") throw new Error("Discrete axis labels require explicit or inferred values, not count.");
237
- const values = valuesFromTickConfig(program, config);
242
+ let values = valuesFromTickConfig(program, config);
238
243
  validateGeneratedItemLimit(values.length, "Label value count");
239
244
  if (discrete) {
240
245
  const domainValues = new Set(scale.domain);
@@ -243,7 +248,7 @@ function resolve(program, channel, config) {
243
248
  const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
244
249
  if (!values.every(value => value >= low && value <= high)) throw new RangeError("Label values must be inside the scale domain.");
245
250
  }
246
- const positions = discrete
251
+ let positions = discrete
247
252
  ? mapOrdinalPositionValues(values, scale)
248
253
  : mapContinuousScaleValues(values, scale);
249
254
  const fallback = value => formatAxisValue(
@@ -256,24 +261,39 @@ function resolve(program, channel, config) {
256
261
  ? formatTransformedTick(scale.type, item)
257
262
  : String(item)
258
263
  );
259
- const text = discrete
260
- ? values.map(value => resolveDisplayLabel(value, config.labelMap, fallback))
261
- : scale.type === "time" && config.format === "auto"
262
- ? formatTimeTicks(values, scale.domain)
263
- : values.map(fallback);
264
+ const automaticTimeLabels = scale.type === "time" && config.format === "auto"
265
+ ? formatTimeTicks(values, scale.domain)
266
+ : undefined;
267
+ let text = values.map((value, index) =>
268
+ resolveDisplayLabel(value, config.labelMap, raw =>
269
+ automaticTimeLabels?.[index] ?? fallback(raw)));
270
+ // Empty mapped text suppresses the label, never its raw tick or scale value.
271
+ // Filter all aligned arrays before wrapping so group/color indices stay dense.
272
+ if (text.includes("")) {
273
+ const visible = text.map((value, index) => value === "" ? -1 : index)
274
+ .filter(index => index !== -1);
275
+ values = visible.map(index => values[index]);
276
+ positions = visible.map(index => positions[index]);
277
+ text = visible.map(index => text[index]);
278
+ }
264
279
  const geometry = {
265
280
  values,
266
281
  ...resolveAxisLabelGeometry({
267
282
  bounds,
268
283
  channel,
269
284
  position: config.position,
270
- positions,
285
+ positions: positions.map(value => value + (config.alongOffset ?? 0)),
271
286
  offset: config.offset
272
287
  })
273
288
  };
274
289
  const resolved = expandWrappedLabels(geometry, text, config, channel, program.materializationConfigs.textMetrics);
290
+ if (channel === "x" && Math.abs(Math.sin(config.rotation)) > 1e-9) {
291
+ const outward = config.position === "bottom" ? 1 : -1;
292
+ resolved.textAlign = Math.sin(config.rotation) * outward < 0 ? "right" : "left";
293
+ resolved.textBaseline = "middle";
294
+ }
275
295
  const canvas = findCanvasGraphic(program)?.properties;
276
- const labelBounds = resolved.text.map((value, index) => resolveTextBounds({
296
+ const measuredBounds = () => resolved.text.map((value, index) => resolveTextBounds({
277
297
  x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
278
298
  y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
279
299
  text: value,
@@ -284,7 +304,63 @@ function resolve(program, channel, config) {
284
304
  textBaseline: resolved.textBaseline,
285
305
  rotation: config.rotation
286
306
  }, program.materializationConfigs.textMetrics));
287
- const groupedBounds = unionLabelBounds(labelBounds.map((bounds, index) => ({
307
+ let labelBounds = measuredBounds();
308
+ if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
309
+ const horizontal = channel === "x";
310
+ const near = ["bottom", "right"].includes(config.position);
311
+ const edge = horizontal ? bounds.y + (near ? bounds.height : 0)
312
+ : bounds.x + (near ? bounds.width : 0);
313
+ const boundKey = horizontal ? (near ? "top" : "bottom") : (near ? "left" : "right");
314
+ const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
315
+ const shifts = grouped.map(item => edge + (near ? config.offset : -config.offset) - item[boundKey]);
316
+ const axis = horizontal ? "y" : "x";
317
+ const positions = resolved[axis];
318
+ resolved[axis] = resolved.text.map((_, index) =>
319
+ (Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
320
+ labelBounds = labelBounds.map((item, index) => {
321
+ const delta = shifts[resolved.groups[index]];
322
+ return horizontal ? { ...item, top: item.top + delta, bottom: item.bottom + delta }
323
+ : { ...item, left: item.left + delta, right: item.right + delta };
324
+ });
325
+ }
326
+ if (config.flush === true) {
327
+ const along = channel === "x" ? "x" : "y";
328
+ const lowKey = channel === "x" ? "left" : "top";
329
+ const highKey = channel === "x" ? "right" : "bottom";
330
+ const low = Math.min(...scale.range) + (config.alongOffset ?? 0);
331
+ const high = Math.max(...scale.range) + (config.alongOffset ?? 0);
332
+ const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
333
+ const shifts = grouped.map(item => {
334
+ if (item[highKey] - item[lowKey] > high - low + 1e-9) {
335
+ throw new Error(`The ${channel}-axis label cannot flush inside the scale span.`);
336
+ }
337
+ return item[lowKey] < low ? low - item[lowKey]
338
+ : item[highKey] > high ? high - item[highKey] : 0;
339
+ });
340
+ const positions = resolved[along];
341
+ resolved[along] = resolved.text.map((_, index) =>
342
+ (Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
343
+ labelBounds = labelBounds.map((item, index) => {
344
+ const delta = shifts[resolved.groups[index]];
345
+ return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
346
+ });
347
+ }
348
+ if (config.align !== undefined) {
349
+ resolved.textAlign = config.align;
350
+ labelBounds = measuredBounds();
351
+ }
352
+ const rotatedOverlap = config.rotation !== 0;
353
+ const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
354
+ const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
355
+ const y = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
356
+ const cosine = Math.cos(config.rotation), sine = Math.sin(config.rotation);
357
+ return resolveTextBounds({
358
+ x: x * cosine + y * sine, y: -x * sine + y * cosine, text,
359
+ fontSize: config.fontSize, fontFamily: config.fontFamily, fontWeight: config.fontWeight,
360
+ textAlign: resolved.textAlign, textBaseline: resolved.textBaseline
361
+ }, program.materializationConfigs.textMetrics);
362
+ }) : labelBounds;
363
+ const groupedBounds = unionLabelBounds(collisionBounds.map((bounds, index) => ({
288
364
  bounds,
289
365
  group: resolved.groups[index]
290
366
  })));
@@ -292,17 +368,18 @@ function resolve(program, channel, config) {
292
368
  ? left.left - right.left
293
369
  : left.top - right.top);
294
370
  if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
295
- throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
371
+ throw canvasOverflowError(`The ${channel}-axis labels do not fit the Canvas margin.`, labelBounds, canvas);
296
372
  }
297
- if (config.overlap === "error" && orderedBounds.some((item, index) => index > 0 &&
298
- textBoundsIntersect(orderedBounds[index - 1], item))) {
373
+ if (config.overlap === "error" && orderedBounds.some((item, index) => rotatedOverlap
374
+ ? orderedBounds.slice(0, index).some(previous => textBoundsIntersect(previous, item))
375
+ : index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
299
376
  throw new Error(`The ${channel}-axis labels overlap each other.`);
300
377
  }
301
- const title = program.graphicSpec.objects[`${channel}AxisTitle`]
302
- ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisTitle`, program.materializationConfigs.textMetrics)
378
+ const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
379
+ ? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
303
380
  : undefined;
304
381
  if (title &&
305
- program.guideConfigs.axis?.[channel]?.title?.inferredOffset !== true &&
382
+ program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
306
383
  labelBounds.some(item => textBoundsIntersect(item, title))) {
307
384
  throw new Error(`The ${channel}-axis labels overlap the axis title.`);
308
385
  }
@@ -313,12 +390,14 @@ function makeEdit(channel) {
313
390
  const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
314
391
  return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
315
392
  validateOptions(args, op, false);
316
- const id = `${channel}AxisLabels`;
393
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
394
+ const axisId = selected.id; args = selected.options;
395
+ const id = axisGraphicIds(channel, axisId)[2];
317
396
  if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
318
- const previous = this.guideConfigs.axis?.[channel]?.labels;
397
+ const previous = this.guideConfigs.axis?.[axisId]?.labels;
319
398
  if (!previous) throw new Error(`${op} requires label configuration.`);
320
399
  const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
321
- const ticks = this.guideConfigs.axis?.[channel]?.ticks;
400
+ const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
322
401
  const inferredValues = !explicitMode && previous.inferredValues === true &&
323
402
  ticks?.mode === "values"
324
403
  ? ticks.values
@@ -333,18 +412,20 @@ function makeEdit(channel) {
333
412
  ...(explicitMode ? { inferredValues: false } : {}),
334
413
  mode
335
414
  }, args, op);
336
- config = normalizeAxisLabelMap(this, config, args, op);
415
+ config = normalizeAxisLabelMaps(this, config, args, op);
337
416
  if (mode === "values") delete config.count; else delete config.values;
338
417
  validateConfig(channel, config);
339
- assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
340
- const resolved = resolve(this, channel, config);
341
- let next = this._withGuideConfig(channel, "labels", config);
418
+ assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
419
+ const resolved = resolve(this, channel, config, axisId);
420
+ let next = this._withGuideConfig(axisId, "labels", config);
342
421
  const properties = {
343
422
  length: resolved.text.length,
344
423
  x: resolved.x,
345
424
  y: resolved.y,
346
425
  text: resolved.text,
347
- fill: config.color,
426
+ fill: config.colorMap === undefined ? config.color
427
+ : resolved.text.map((_, index) => resolveDisplayLabelColor(
428
+ resolved.values[resolved.groups[index]], config.colorMap, config.color)),
348
429
  fontSize: config.fontSize,
349
430
  fontFamily: config.fontFamily,
350
431
  fontWeight: config.fontWeight,
@@ -357,10 +438,10 @@ function makeEdit(channel) {
357
438
  for (const [property, value] of Object.entries(properties)) {
358
439
  next = next.editGraphics({ target: id, property, value });
359
440
  }
360
- const titleConfig = next.guideConfigs.axis?.[channel]?.title;
441
+ const titleConfig = next.guideConfigs.axis?.[axisId]?.title;
361
442
  if (titleConfig?.inferredOffset === true) {
362
443
  const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
363
- next = next[editTitle]();
444
+ next = next[editTitle](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
364
445
  }
365
446
  return next;
366
447
  }));
@@ -374,12 +455,14 @@ function makeCreate(channel) {
374
455
  const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
375
456
  return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
376
457
  validateOptions(args, op, true);
458
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
459
+ const axisId = selected.id; args = selected.options;
377
460
  const scale = validateUserId(args.scale ?? channel, "Scale id");
378
- const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
461
+ const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
379
462
  if (guideScale && guideScale !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
380
- const id = `${channel}AxisLabels`;
463
+ const id = axisGraphicIds(channel, axisId)[2];
381
464
  if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis labels.`);
382
- const ticks = this.guideConfigs.axis?.[channel]?.ticks;
465
+ const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
383
466
  const hasValues = Object.hasOwn(args, "values");
384
467
  const hasCount = Object.hasOwn(args, "count");
385
468
  const resolvedScale = this.resolvedScales[scale];
@@ -399,6 +482,7 @@ function makeCreate(channel) {
399
482
  fontSize: DEFAULTS.fontSize,
400
483
  fontFamily: DEFAULTS.fontFamily,
401
484
  fontWeight: DEFAULTS.fontWeight,
485
+ ...axisThemeTypography(this.materializationConfigs.theme, "labels"),
402
486
  rotation: DEFAULTS.rotation,
403
487
  overlap: DEFAULTS.overlap,
404
488
  ...args,
@@ -406,7 +490,7 @@ function makeCreate(channel) {
406
490
  (ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
407
491
  mode
408
492
  }, args, op);
409
- config = normalizeAxisLabelMap(this, config, args, op);
493
+ config = normalizeAxisLabelMaps(this, config, args, op);
410
494
  if (mode === "values") {
411
495
  config.values ??= ticks?.values ?? inferredDiscreteValues;
412
496
  } else {
@@ -414,15 +498,15 @@ function makeCreate(channel) {
414
498
  }
415
499
  validateConfig(channel, config);
416
500
  assertTickCompatibility(ticks, config, op);
417
- resolve(this, channel, config);
418
- return this.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
501
+ resolve(this, channel, config, axisId);
502
+ return this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
419
503
  .createGraphics({
420
504
  id,
421
505
  type: "text",
422
506
  length: 0,
423
507
  ...resolvePlotGraphicPlacement(this)
424
508
  })
425
- ._withGuideConfig(channel, "labels", config)[edit]();
509
+ ._withGuideConfig(axisId, "labels", config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
426
510
  }));
427
511
  }
428
512
 
@@ -1,3 +1,6 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
+ import { requireSemanticScale } from "../../../selectors/scales.js";
1
4
  import { editGraphicProperties } from "../../primitives/graphicProperties.js";
2
5
  import { isSourceOwnedText } from "../../../grammar/text.js";
3
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
@@ -8,7 +11,7 @@ import {
8
11
  validateNonEmptyString,
9
12
  validateNonNegativeFinite
10
13
  } from "../../../core/validation.js";
11
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
14
+ import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
12
15
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
13
16
  import {
14
17
  defaultAxisPosition,
@@ -19,18 +22,18 @@ import { resolvePlotGraphicPlacement } from
19
22
  "../../../materialization/graphicHierarchy.js";
20
23
 
21
24
  const DEFAULT_STYLE = Object.freeze({ color: DEFAULT_COLORS.text, lineWidth: 1 });
22
- const CREATE_OPTIONS = Object.freeze(["scale", "position", "color", "lineWidth"]);
23
- const EDIT_OPTIONS = Object.freeze(["position", "color", "lineWidth"]);
25
+ const CREATE_OPTIONS = Object.freeze(["target", "scale", "position", "color", "lineWidth"]);
26
+ const EDIT_OPTIONS = Object.freeze(["target", "position", "color", "lineWidth"]);
24
27
 
25
28
  function validateStyle({ color, lineWidth }) {
26
29
  validateNonEmptyString(color, "Axis line color");
27
30
  validateNonNegativeFinite(lineWidth, "Axis lineWidth");
28
31
  }
29
32
 
30
- function resolveLineRange(resolvedScale, bounds, channel) {
33
+ function resolveLineRange(resolvedScale, bounds, channel, explicitRange) {
31
34
  const range = resolvedScale?.range;
32
35
  if (
33
- resolvedScale?.type !== "time" ||
36
+ explicitRange || resolvedScale?.type !== "time" ||
34
37
  !Number.isFinite(resolvedScale.bandwidth)
35
38
  ) return range;
36
39
  const plotRange = channel === "x"
@@ -41,12 +44,12 @@ function resolveLineRange(resolvedScale, bounds, channel) {
41
44
  : plotRange.reverse();
42
45
  }
43
46
 
44
- function resolveGeometry(program, channel, scaleId, position) {
47
+ function resolveGeometry(program, channel, scaleId, position, axisId = channel) {
45
48
  const hasConsumer = program.semanticSpec.layers.some(
46
49
  layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
47
50
  );
48
51
 
49
- if (!hasConsumer) {
52
+ if (!hasConsumer && !axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) {
50
53
  throw new Error(
51
54
  `Axis line requires scale "${scaleId}" on the ${channel} channel.`
52
55
  );
@@ -54,11 +57,7 @@ function resolveGeometry(program, channel, scaleId, position) {
54
57
 
55
58
  const resolvedScale = program.resolvedScales[scaleId];
56
59
  const range = resolvedScale?.range;
57
- const bounds = resolveGuideCoordinateBounds(program, {
58
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
59
- scale: scaleId,
60
- channel
61
- });
60
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, position, scaleId, axisId);
62
61
 
63
62
  if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
64
63
  throw new Error(`Axis line requires a resolved numeric scale "${scaleId}".`);
@@ -75,14 +74,14 @@ function resolveGeometry(program, channel, scaleId, position) {
75
74
  bounds,
76
75
  channel,
77
76
  position,
78
- resolveLineRange(resolvedScale, bounds, channel)
77
+ resolveLineRange(resolvedScale, bounds, channel, Array.isArray(requireSemanticScale(program, scaleId).range))
79
78
  );
80
79
  }
81
80
 
82
- function axisIds(channel) {
81
+ function axisIds(channel, axisId = channel) {
83
82
  return {
84
- graphic: `${channel}AxisLine`,
85
- guidePath: `guide.axis.${channel}.scale`
83
+ graphic: axisGraphicIds(channel, axisId)[0],
84
+ guidePath: `guide.axis.${axisId}.scale`
86
85
  };
87
86
  }
88
87
 
@@ -93,24 +92,26 @@ function createEditAxisLine(channel) {
93
92
  { op: operation, description: `Edit the concrete ${channel}-axis line.` },
94
93
  withGuideLayoutValidation(function (args = {}) {
95
94
  validateKeys(args, EDIT_OPTIONS, operation);
96
- const { graphic } = axisIds(channel);
95
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
96
+ const axisId = selected.id; args = selected.options;
97
+ const { graphic } = axisIds(channel, axisId);
97
98
  const line = this.graphicSpec.objects[graphic];
98
99
 
99
100
  if (line?.type !== "line") {
100
101
  throw new Error(`${operation} requires an existing ${channel}-axis line.`);
101
102
  }
102
103
 
103
- const previous = this.guideConfigs.axis?.[channel]?.line;
104
+ const previous = this.guideConfigs.axis?.[axisId]?.line;
104
105
  const position = validateAxisPosition(
105
106
  channel,
106
107
  args.position ?? previous?.position ?? defaultAxisPosition(channel)
107
108
  );
108
- const scaleId = this.semanticSpec.guides.axis?.[channel]?.scale;
109
- const geometry = resolveGeometry(this, channel, scaleId, position);
109
+ const scaleId = this.semanticSpec.guides.axis?.[axisId]?.scale;
110
+ const geometry = resolveGeometry(this, channel, scaleId, position, axisId);
110
111
  const color = args.color ?? line.properties.stroke;
111
112
  const lineWidth = args.lineWidth ?? line.properties.strokeWidth;
112
113
  validateStyle({ color, lineWidth });
113
- let next = this._withGuideConfig(channel, "line", {
114
+ let next = this._withGuideConfig(axisId, "line", {
114
115
  position,
115
116
  color,
116
117
  lineWidth
@@ -139,6 +140,8 @@ function createAxisLine(channel) {
139
140
  { op: operation, description: `Create the concrete ${channel}-axis line.` },
140
141
  withGuideLayoutValidation(function (args = {}) {
141
142
  validateKeys(args, CREATE_OPTIONS, operation);
143
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
144
+ const axisId = selected.id; args = selected.options;
142
145
  const scale = validateUserId(args.scale ?? channel, "Scale id");
143
146
  const position = validateAxisPosition(
144
147
  channel,
@@ -147,8 +150,8 @@ function createAxisLine(channel) {
147
150
  const color = args.color ?? DEFAULT_STYLE.color;
148
151
  const lineWidth = args.lineWidth ?? DEFAULT_STYLE.lineWidth;
149
152
  validateStyle({ color, lineWidth });
150
- resolveGeometry(this, channel, scale, position);
151
- const { graphic, guidePath } = axisIds(channel);
153
+ resolveGeometry(this, channel, scale, position, axisId);
154
+ const { graphic, guidePath } = axisIds(channel, axisId);
152
155
 
153
156
  if (this.graphicSpec.objects[graphic] !== undefined) {
154
157
  throw new Error(`${operation} requires a missing ${channel}-axis line.`);
@@ -161,7 +164,7 @@ function createAxisLine(channel) {
161
164
  type: "line",
162
165
  ...resolvePlotGraphicPlacement(this)
163
166
  })
164
- [editOperation]({ position, color, lineWidth });
167
+ [editOperation]({ ...axisTargetArgs(axisId, channel), position, color, lineWidth });
165
168
  })
166
169
  );
167
170
  }
@@ -68,7 +68,7 @@ function makeAxisAction(create) {
68
68
  ? { mode: "selected", dimensions: resolved.dimensions.map(dimension => ({ field: dimension.field })) }
69
69
  : resolveParallelAxisConfigs(this, resolved.dimensions);
70
70
  const previous = config.dimensions.find(dimension => dimension.field === args.field);
71
- const updated = patchParallelAxis(previous, args, create, operation);
71
+ const updated = patchParallelAxis(previous, args, create, operation, this.materializationConfigs.theme);
72
72
  const next = withTitle(withOwner(this, resolved, {
73
73
  ...config, dimensions: config.dimensions.map(dimension => dimension.field === args.field ? updated : dimension)
74
74
  }), args, create);
@@ -1,10 +1,11 @@
1
+ import { axisThemeTypography } from "../../../theme/state.js";
1
2
  import {
2
3
  validateGeneratedItemLimit,
3
4
  validateNonEmptyString,
4
5
  validateNonNegativeFinite,
5
6
  validateOptionObject
6
7
  } from "../../../../core/validation.js";
7
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
8
+ import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
8
9
  import { validateAxisTextStyle } from "../labels.js";
9
10
  import { validateAxisFormat } from "../policy.js";
10
11
 
@@ -20,15 +21,15 @@ const STYLE_OPTIONS = Object.freeze({
20
21
  title: ["text", "offset", "color", "fontSize", "fontFamily", "fontWeight"]
21
22
  });
22
23
 
23
- export function defaultParallelAxis(field) {
24
+ export function defaultParallelAxis(field, theme) {
24
25
  return {
25
26
  field,
26
27
  line: { color: DEFAULT_COLORS.axis, lineWidth: 1.25 },
27
28
  ticks: { mode: "auto", length: 8, color: DEFAULT_COLORS.mutedText, lineWidth: 1 },
28
29
  labels: { mode: "auto", offset: 9, format: "auto", color: DEFAULT_COLORS.axis,
29
- fontSize: 11, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: "normal" },
30
+ fontSize: AXIS_LABEL_FONT_SIZES.parallel, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: "normal", ...axisThemeTypography(theme, "labels") },
30
31
  title: { offset: 20, color: DEFAULT_COLORS.axisTitle,
31
- fontSize: 13, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 600 }
32
+ fontSize: AXIS_TITLE_FONT_SIZE, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 600, ...axisThemeTypography(theme, "title") }
32
33
  };
33
34
  }
34
35
 
@@ -98,8 +99,8 @@ function patchComponent(previous, defaults, options, part, create, operation) {
98
99
  return next;
99
100
  }
100
101
 
101
- export function patchParallelAxis(previous, args, create, operation) {
102
- const defaults = defaultParallelAxis(args.field);
102
+ export function patchParallelAxis(previous, args, create, operation, theme) {
103
+ const defaults = defaultParallelAxis(args.field, theme);
103
104
  const next = { ...previous, field: args.field };
104
105
  const updates = {};
105
106
  for (const part of ["line", "title"]) {
@@ -132,6 +133,6 @@ export function resolveParallelAxisConfigs(program, dimensions) {
132
133
  const previous = program.guideConfigs.axis?.parallel?.axes;
133
134
  const mode = previous?.mode ?? "all";
134
135
  const configs = dimensions.map(dimension => previous?.dimensions?.find(config => config.field === dimension.field) ??
135
- (mode === "all" ? defaultParallelAxis(dimension.field) : { field: dimension.field }));
136
+ (mode === "all" ? defaultParallelAxis(dimension.field, program.materializationConfigs.theme) : { field: dimension.field }));
136
137
  return { mode, dimensions: configs };
137
138
  }