ggaction 0.0.16 → 0.0.18

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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -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 +10 -4
  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 +16 -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 +4 -4
  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/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  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 +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  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/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -1,11 +1,13 @@
1
+ import { axisThemeTypography } from "../../theme/state.js";
2
+ import { canvasOverflowError } from "../../../layout/canvas.js";
1
3
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
4
  import { action } from "../../../core/action.js";
3
5
  import { validateUserId } from "../../../core/identifiers.js";
4
6
  import {
5
7
  sameOrderedValues,
6
8
  validateGeneratedItemLimit,
9
+ validateTextStyle,
7
10
  validateOptionObject,
8
- validateNonEmptyString,
9
11
  validateNonNegativeFinite,
10
12
  validatePositiveFinite
11
13
  } from "../../../core/validation.js";
@@ -19,7 +21,7 @@ import {
19
21
  import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
20
22
  import { resolveRotation } from "../../../grammar/rotation.js";
21
23
  import { valuesFromTickConfig } from "../tickValues.js";
22
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
24
+ import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
23
25
  "../../../theme/defaults.js";
24
26
  import {
25
27
  defaultAxisPosition,
@@ -40,19 +42,21 @@ import { resolveConcreteGraphicBounds } from
40
42
  import { wrapText } from "../../../layout/text.js";
41
43
  import {
42
44
  normalizeDisplayLabelMap,
45
+ normalizeDisplayColorMap,
46
+ resolveDisplayLabelColor,
43
47
  resolveDisplayLabel
44
48
  } from "../../../grammar/displayLabels.js";
45
49
 
46
50
  const OPTIONS = [
47
51
  "scale", "position", "count", "values", "offset", "format", "color",
48
52
  "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
49
- "lineHeight", "overlap", "labelMap"
53
+ "lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap"
50
54
  ];
51
55
 
52
56
  const DEFAULTS = {
53
57
  count: 5,
54
58
  color: DEFAULT_COLORS.text,
55
- fontSize: 12,
59
+ fontSize: AXIS_LABEL_FONT_SIZES.cartesian,
56
60
  fontFamily: DEFAULT_FONT_FAMILY,
57
61
  fontWeight: "normal",
58
62
  rotation: 0,
@@ -78,15 +82,7 @@ function normalizeLabelLayout(config, args, operation) {
78
82
  return next;
79
83
  }
80
84
 
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
- }
85
+ export { validateTextStyle as validateAxisTextStyle };
90
86
 
91
87
  function validateOptions(args, operation, create) {
92
88
  validateOptionObject(
@@ -94,6 +90,8 @@ function validateOptions(args, operation, create) {
94
90
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
95
91
  operation
96
92
  );
93
+ if (Object.hasOwn(args, "flush") && typeof args.flush !== "boolean") throw new TypeError("Label flush must be a boolean.");
94
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
97
95
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
98
96
  throw new Error(`${operation} cannot use count and values together.`);
99
97
  }
@@ -116,9 +114,11 @@ function validateConfig(channel, config) {
116
114
  if (config.mode === "values") {
117
115
  validateGeneratedItemLimit(config.values.length, "Label value count");
118
116
  }
117
+ if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
119
118
  validateNonNegativeFinite(config.offset, "Label offset");
120
- validateAxisTextStyle(config, "Label");
119
+ validateTextStyle(config, "Label");
121
120
  validateAxisFormat(config.format);
121
+ if (config.flush === true && config.rotation !== 0) throw new Error("Label flush requires unrotated Cartesian labels.");
122
122
  if (!Number.isFinite(config.rotation)) {
123
123
  throw new TypeError("Label rotation must resolve to finite radians.");
124
124
  }
@@ -140,24 +140,30 @@ function validateConfig(channel, config) {
140
140
  }
141
141
  }
142
142
 
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
- }
143
+ function normalizeAxisLabelMaps(program, config, args, operation) {
149
144
  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
- );
145
+ for (const [property, normalize] of [
146
+ ["labelMap", normalizeDisplayLabelMap],
147
+ ["colorMap", normalizeDisplayColorMap]
148
+ ]) {
149
+ if (!Object.hasOwn(args, property)) continue;
150
+ if (args[property] === "auto") delete next[property];
151
+ else next[property] = normalize(args[property], `${operation} ${property}`);
157
152
  }
153
+ validateAxisLabelMapScale(next, program.resolvedScales[config.scale], operation);
158
154
  return next;
159
155
  }
160
156
 
157
+ function validateAxisLabelMapScale(config, scale, operation = "Axis") {
158
+ if (["ordinal", "band", "point"].includes(scale?.type)) return;
159
+ for (const property of ["labelMap", "colorMap"]) {
160
+ if (config[property] !== undefined &&
161
+ !config[property].every(entry => Number.isFinite(entry.value))) {
162
+ throw new TypeError(`${operation} continuous ${property} values must be finite numbers or timestamps.`);
163
+ }
164
+ }
165
+ }
166
+
161
167
  function assertTickCompatibility(ticks, config, operation) {
162
168
  if (!ticks) return;
163
169
  if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
@@ -226,15 +232,13 @@ function resolve(program, channel, config) {
226
232
  channel
227
233
  });
228
234
  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
- }
235
+ validateAxisLabelMapScale(config, scale);
232
236
  if ((
233
237
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
234
238
  !isTransformedScaleType(scale?.type)
235
239
  ) || !bounds) throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");
236
240
  if (discrete && config.mode !== "values") throw new Error("Discrete axis labels require explicit or inferred values, not count.");
237
- const values = valuesFromTickConfig(program, config);
241
+ let values = valuesFromTickConfig(program, config);
238
242
  validateGeneratedItemLimit(values.length, "Label value count");
239
243
  if (discrete) {
240
244
  const domainValues = new Set(scale.domain);
@@ -243,7 +247,7 @@ function resolve(program, channel, config) {
243
247
  const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
244
248
  if (!values.every(value => value >= low && value <= high)) throw new RangeError("Label values must be inside the scale domain.");
245
249
  }
246
- const positions = discrete
250
+ let positions = discrete
247
251
  ? mapOrdinalPositionValues(values, scale)
248
252
  : mapContinuousScaleValues(values, scale);
249
253
  const fallback = value => formatAxisValue(
@@ -256,24 +260,39 @@ function resolve(program, channel, config) {
256
260
  ? formatTransformedTick(scale.type, item)
257
261
  : String(item)
258
262
  );
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);
263
+ const automaticTimeLabels = scale.type === "time" && config.format === "auto"
264
+ ? formatTimeTicks(values, scale.domain)
265
+ : undefined;
266
+ let text = values.map((value, index) =>
267
+ resolveDisplayLabel(value, config.labelMap, raw =>
268
+ automaticTimeLabels?.[index] ?? fallback(raw)));
269
+ // Empty mapped text suppresses the label, never its raw tick or scale value.
270
+ // Filter all aligned arrays before wrapping so group/color indices stay dense.
271
+ if (text.includes("")) {
272
+ const visible = text.map((value, index) => value === "" ? -1 : index)
273
+ .filter(index => index !== -1);
274
+ values = visible.map(index => values[index]);
275
+ positions = visible.map(index => positions[index]);
276
+ text = visible.map(index => text[index]);
277
+ }
264
278
  const geometry = {
265
279
  values,
266
280
  ...resolveAxisLabelGeometry({
267
281
  bounds,
268
282
  channel,
269
283
  position: config.position,
270
- positions,
284
+ positions: positions.map(value => value + (config.alongOffset ?? 0)),
271
285
  offset: config.offset
272
286
  })
273
287
  };
274
288
  const resolved = expandWrappedLabels(geometry, text, config, channel, program.materializationConfigs.textMetrics);
289
+ if (channel === "x" && Math.abs(Math.sin(config.rotation)) > 1e-9) {
290
+ const outward = config.position === "bottom" ? 1 : -1;
291
+ resolved.textAlign = Math.sin(config.rotation) * outward < 0 ? "right" : "left";
292
+ resolved.textBaseline = "middle";
293
+ }
275
294
  const canvas = findCanvasGraphic(program)?.properties;
276
- const labelBounds = resolved.text.map((value, index) => resolveTextBounds({
295
+ let labelBounds = resolved.text.map((value, index) => resolveTextBounds({
277
296
  x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
278
297
  y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
279
298
  text: value,
@@ -284,7 +303,58 @@ function resolve(program, channel, config) {
284
303
  textBaseline: resolved.textBaseline,
285
304
  rotation: config.rotation
286
305
  }, program.materializationConfigs.textMetrics));
287
- const groupedBounds = unionLabelBounds(labelBounds.map((bounds, index) => ({
306
+ if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
307
+ const horizontal = channel === "x";
308
+ const near = ["bottom", "right"].includes(config.position);
309
+ const edge = horizontal ? bounds.y + (near ? bounds.height : 0)
310
+ : bounds.x + (near ? bounds.width : 0);
311
+ const boundKey = horizontal ? (near ? "top" : "bottom") : (near ? "left" : "right");
312
+ const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
313
+ const shifts = grouped.map(item => edge + (near ? config.offset : -config.offset) - item[boundKey]);
314
+ const axis = horizontal ? "y" : "x";
315
+ const positions = resolved[axis];
316
+ resolved[axis] = resolved.text.map((_, index) =>
317
+ (Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
318
+ labelBounds = labelBounds.map((item, index) => {
319
+ const delta = shifts[resolved.groups[index]];
320
+ return horizontal ? { ...item, top: item.top + delta, bottom: item.bottom + delta }
321
+ : { ...item, left: item.left + delta, right: item.right + delta };
322
+ });
323
+ }
324
+ if (config.flush === true) {
325
+ const along = channel === "x" ? "x" : "y";
326
+ const lowKey = channel === "x" ? "left" : "top";
327
+ const highKey = channel === "x" ? "right" : "bottom";
328
+ const low = Math.min(...scale.range) + (config.alongOffset ?? 0);
329
+ const high = Math.max(...scale.range) + (config.alongOffset ?? 0);
330
+ const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
331
+ const shifts = grouped.map(item => {
332
+ if (item[highKey] - item[lowKey] > high - low + 1e-9) {
333
+ throw new Error(`The ${channel}-axis label cannot flush inside the scale span.`);
334
+ }
335
+ return item[lowKey] < low ? low - item[lowKey]
336
+ : item[highKey] > high ? high - item[highKey] : 0;
337
+ });
338
+ const positions = resolved[along];
339
+ resolved[along] = resolved.text.map((_, index) =>
340
+ (Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
341
+ labelBounds = labelBounds.map((item, index) => {
342
+ const delta = shifts[resolved.groups[index]];
343
+ return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
344
+ });
345
+ }
346
+ const rotatedOverlap = config.rotation !== 0;
347
+ const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
348
+ const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
349
+ const y = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
350
+ const cosine = Math.cos(config.rotation), sine = Math.sin(config.rotation);
351
+ return resolveTextBounds({
352
+ x: x * cosine + y * sine, y: -x * sine + y * cosine, text,
353
+ fontSize: config.fontSize, fontFamily: config.fontFamily, fontWeight: config.fontWeight,
354
+ textAlign: resolved.textAlign, textBaseline: resolved.textBaseline
355
+ }, program.materializationConfigs.textMetrics);
356
+ }) : labelBounds;
357
+ const groupedBounds = unionLabelBounds(collisionBounds.map((bounds, index) => ({
288
358
  bounds,
289
359
  group: resolved.groups[index]
290
360
  })));
@@ -292,10 +362,11 @@ function resolve(program, channel, config) {
292
362
  ? left.left - right.left
293
363
  : left.top - right.top);
294
364
  if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
295
- throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
365
+ throw canvasOverflowError(`The ${channel}-axis labels do not fit the Canvas margin.`, labelBounds, canvas);
296
366
  }
297
- if (config.overlap === "error" && orderedBounds.some((item, index) => index > 0 &&
298
- textBoundsIntersect(orderedBounds[index - 1], item))) {
367
+ if (config.overlap === "error" && orderedBounds.some((item, index) => rotatedOverlap
368
+ ? orderedBounds.slice(0, index).some(previous => textBoundsIntersect(previous, item))
369
+ : index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
299
370
  throw new Error(`The ${channel}-axis labels overlap each other.`);
300
371
  }
301
372
  const title = program.graphicSpec.objects[`${channel}AxisTitle`]
@@ -333,7 +404,7 @@ function makeEdit(channel) {
333
404
  ...(explicitMode ? { inferredValues: false } : {}),
334
405
  mode
335
406
  }, args, op);
336
- config = normalizeAxisLabelMap(this, config, args, op);
407
+ config = normalizeAxisLabelMaps(this, config, args, op);
337
408
  if (mode === "values") delete config.count; else delete config.values;
338
409
  validateConfig(channel, config);
339
410
  assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
@@ -344,7 +415,9 @@ function makeEdit(channel) {
344
415
  x: resolved.x,
345
416
  y: resolved.y,
346
417
  text: resolved.text,
347
- fill: config.color,
418
+ fill: config.colorMap === undefined ? config.color
419
+ : resolved.text.map((_, index) => resolveDisplayLabelColor(
420
+ resolved.values[resolved.groups[index]], config.colorMap, config.color)),
348
421
  fontSize: config.fontSize,
349
422
  fontFamily: config.fontFamily,
350
423
  fontWeight: config.fontWeight,
@@ -399,6 +472,7 @@ function makeCreate(channel) {
399
472
  fontSize: DEFAULTS.fontSize,
400
473
  fontFamily: DEFAULTS.fontFamily,
401
474
  fontWeight: DEFAULTS.fontWeight,
475
+ ...axisThemeTypography(this.materializationConfigs.theme, "labels"),
402
476
  rotation: DEFAULTS.rotation,
403
477
  overlap: DEFAULTS.overlap,
404
478
  ...args,
@@ -406,7 +480,7 @@ function makeCreate(channel) {
406
480
  (ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
407
481
  mode
408
482
  }, args, op);
409
- config = normalizeAxisLabelMap(this, config, args, op);
483
+ config = normalizeAxisLabelMaps(this, config, args, op);
410
484
  if (mode === "values") {
411
485
  config.values ??= ticks?.values ?? inferredDiscreteValues;
412
486
  } else {
@@ -1,3 +1,4 @@
1
+ import { requireSemanticScale } from "../../../selectors/scales.js";
1
2
  import { editGraphicProperties } from "../../primitives/graphicProperties.js";
2
3
  import { isSourceOwnedText } from "../../../grammar/text.js";
3
4
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
@@ -8,7 +9,7 @@ import {
8
9
  validateNonEmptyString,
9
10
  validateNonNegativeFinite
10
11
  } from "../../../core/validation.js";
11
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
12
+ import { axisScaleBindings, resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
12
13
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
13
14
  import {
14
15
  defaultAxisPosition,
@@ -27,10 +28,10 @@ function validateStyle({ color, lineWidth }) {
27
28
  validateNonNegativeFinite(lineWidth, "Axis lineWidth");
28
29
  }
29
30
 
30
- function resolveLineRange(resolvedScale, bounds, channel) {
31
+ function resolveLineRange(resolvedScale, bounds, channel, explicitRange) {
31
32
  const range = resolvedScale?.range;
32
33
  if (
33
- resolvedScale?.type !== "time" ||
34
+ explicitRange || resolvedScale?.type !== "time" ||
34
35
  !Number.isFinite(resolvedScale.bandwidth)
35
36
  ) return range;
36
37
  const plotRange = channel === "x"
@@ -46,7 +47,7 @@ function resolveGeometry(program, channel, scaleId, position) {
46
47
  layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
47
48
  );
48
49
 
49
- if (!hasConsumer) {
50
+ if (!hasConsumer && !axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) {
50
51
  throw new Error(
51
52
  `Axis line requires scale "${scaleId}" on the ${channel} channel.`
52
53
  );
@@ -75,7 +76,7 @@ function resolveGeometry(program, channel, scaleId, position) {
75
76
  bounds,
76
77
  channel,
77
78
  position,
78
- resolveLineRange(resolvedScale, bounds, channel)
79
+ resolveLineRange(resolvedScale, bounds, channel, Array.isArray(requireSemanticScale(program, scaleId).range))
79
80
  );
80
81
  }
81
82
 
@@ -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
  }
@@ -1,3 +1,4 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
2
  import { action } from "../../../core/action.js";
2
3
  import { validateKeys } from "../../../core/validation.js";
3
4
  import { axisGraphicIds } from
@@ -20,7 +21,7 @@ function makeRemoveAxis(channel) {
20
21
  op: operation,
21
22
  description: `Remove the complete ${channel}-axis resource.`
22
23
  },
23
- function (args = {}) {
24
+ withGuideLayoutValidation(function (args = {}) {
24
25
  validateKeys(args, OPTIONS, operation);
25
26
  const semantic = this.semanticSpec.guides.axis?.[channel];
26
27
  const config = this.guideConfigs.axis?.[channel];
@@ -50,7 +51,7 @@ function makeRemoveAxis(channel) {
50
51
  }
51
52
  }
52
53
  return next._withoutMaterializationConfig(["guides", "axis", channel]);
53
- }
54
+ })
54
55
  );
55
56
  }
56
57
 
@@ -18,7 +18,7 @@ const EDIT_OPTIONS = Object.freeze([
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"]);
22
22
  const LABEL_OPTIONS = Object.freeze([
23
23
  "offset",
24
24
  "format",
@@ -31,7 +31,10 @@ const LABEL_OPTIONS = Object.freeze([
31
31
  "wrap",
32
32
  "lineHeight",
33
33
  "overlap",
34
- "labelMap"
34
+ "labelMap",
35
+ "alongOffset",
36
+ "flush",
37
+ "colorMap"
35
38
  ]);
36
39
  const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
37
40
  const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
@@ -1,3 +1,4 @@
1
+ import { canvasOverflowError } from "../../../layout/canvas.js";
1
2
  import { editGraphicProperties } from "../../primitives/graphicProperties.js";
2
3
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
3
4
  import { applyMaterializationPlan } from "../../../materialization/planner.js";
@@ -31,7 +32,7 @@ import {
31
32
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
32
33
  "../../../materialization/graphicHierarchy.js";
33
34
 
34
- const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth"]);
35
+ const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth", "alongOffset"]);
35
36
  const DEFAULTS = Object.freeze({
36
37
  count: DEFAULT_TICK_COUNT,
37
38
  length: 6,
@@ -45,6 +46,7 @@ function validateOptions(args, operation, create) {
45
46
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
46
47
  operation
47
48
  );
49
+ if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
48
50
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
49
51
  if (Array.isArray(args.values)) {
50
52
  validateGeneratedItemLimit(args.values.length, "Tick value count");
@@ -65,6 +67,7 @@ function validateConfig(channel, config) {
65
67
  (typeof value === "number" && Number.isFinite(value))
66
68
  ))
67
69
  ) throw new TypeError("Tick values must be nominal values or finite numbers.");
70
+ if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
68
71
  validateNonNegativeFinite(config.length, "Tick length");
69
72
  validateNonNegativeFinite(config.lineWidth, "Tick lineWidth");
70
73
  validateNonEmptyString(config.color, "Tick color");
@@ -102,7 +105,7 @@ function geometry(program, channel, config) {
102
105
  bounds,
103
106
  channel,
104
107
  position: config.position,
105
- positions,
108
+ positions: positions.map(value => value + (config.alongOffset ?? 0)),
106
109
  length: config.length
107
110
  })
108
111
  };
@@ -127,7 +130,7 @@ function geometry(program, channel, config) {
127
130
  coordinates.some(value => !Number.isFinite(value)) ||
128
131
  !fits
129
132
  ) {
130
- throw new Error(`The ${channel}-axis ticks do not fit the Canvas margin.`);
133
+ 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
134
  }
132
135
  return resolved;
133
136
  }