ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
1
2
  import {
2
3
  BAR_ORIENTATIONS,
3
4
  resolveBarOrientation
@@ -8,6 +9,7 @@ import {
8
9
  } from "../../core/vocabulary.js";
9
10
  import { findSemanticScale } from "../../selectors/scales.js";
10
11
  import { findDataset } from "../../selectors/datasets.js";
12
+ import { isOpacityLegendLayer } from "../../materialization/legends.js";
11
13
  import { findUpstreamTransform } from
12
14
  "../../materialization/dataProvenance.js";
13
15
 
@@ -19,8 +21,8 @@ function isHorizonLayer(program, layer) {
19
21
  ) !== undefined;
20
22
  }
21
23
 
22
- function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
23
- return program.semanticSpec.layers.some(layer => {
24
+ function hasEncoding(program, layers, channel, { scaled = false, grid = false } = {}) {
25
+ return layers.some(layer => {
24
26
  if (isHorizonLayer(program, layer) && channel !== "x") return false;
25
27
  const encoding = layer.encoding?.[channel];
26
28
  if (!scaled && !grid) return encoding !== undefined;
@@ -31,23 +33,22 @@ function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
31
33
  });
32
34
  }
33
35
 
34
- function positionalApplicability(program, channels) {
36
+ function positionalApplicability(program, layers, channels) {
35
37
  return Object.freeze(Object.fromEntries(
36
38
  channels.map(channel => [
37
39
  channel,
38
40
  Object.freeze({
39
- axis: hasEncoding(program, channel),
40
- grid: hasEncoding(program, channel, { grid: true })
41
+ axis: hasEncoding(program, layers, channel),
42
+ grid: hasEncoding(program, layers, channel, { grid: true })
41
43
  })
42
44
  ])
43
45
  ));
44
46
  }
45
47
 
46
- export function hasInferableLegend(program) {
47
- return program.semanticSpec.layers.some(layer =>
48
+ export function hasInferableLegend(program, layers = program.semanticSpec.layers) {
49
+ return layers.some(layer =>
48
50
  !isHorizonLayer(program, layer) && (
49
- (layer.mark?.type === "point" &&
50
- layer.encoding?.opacity?.scale !== undefined) ||
51
+ isOpacityLegendLayer(layer) ||
51
52
  (layer.mark?.type === "point" &&
52
53
  layer.encoding?.size?.scale !== undefined) ||
53
54
  (layer.mark?.type === "point" &&
@@ -66,13 +67,14 @@ export function hasInferableLegend(program) {
66
67
  );
67
68
  }
68
69
 
69
- export function resolveGuideApplicability(program) {
70
+ export function resolveGuideApplicability(program, layers = program.semanticSpec.layers) {
70
71
  const cartesian = positionalApplicability(
71
72
  program,
73
+ layers,
72
74
  CARTESIAN_POSITION_CHANNELS
73
75
  );
74
- const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
75
- const parallel = program.semanticSpec.layers.some(layer =>
76
+ const polar = positionalApplicability(program, layers, POLAR_POSITION_CHANNELS);
77
+ const parallel = layers.some(layer =>
76
78
  layer.encoding?.parallel?.dimensions?.length >= 2
77
79
  );
78
80
  return Object.freeze({
@@ -101,12 +103,13 @@ export function resolveGuideApplicability(program) {
101
103
  radial: polar.radius.grid
102
104
  })
103
105
  }),
104
- legend: hasInferableLegend(program)
106
+ legend: hasInferableLegend(program, layers)
105
107
  });
106
108
  }
107
109
 
108
- export function resolveAutomaticGridOptions(program) {
109
- const applicability = resolveGuideApplicability(program);
110
+ export function resolveAutomaticGridOptions(program, layers = program.semanticSpec.layers) {
111
+ layers = layers.filter(layer => !isSourceOwnedText(layer));
112
+ const applicability = resolveGuideApplicability(program, layers);
110
113
  const directions = applicability.grid.directions;
111
114
  if (applicability.grid.polar && !applicability.grid.cartesian) {
112
115
  return Object.freeze({
@@ -118,10 +121,10 @@ export function resolveAutomaticGridOptions(program) {
118
121
  return Object.freeze({ horizontal: false, vertical: {} });
119
122
  }
120
123
 
121
- const barOrientations = program.semanticSpec.layers
124
+ const barOrientations = layers
122
125
  .map(resolveBarOrientation)
123
126
  .filter(Boolean);
124
- const positionedLayers = program.semanticSpec.layers.filter(
127
+ const positionedLayers = layers.filter(
125
128
  layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
126
129
  );
127
130
  const horizontalQuantitative = positionedLayers.length > 0 &&
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { action } from "../../../core/action.js";
2
3
  import { isPlainObject } from "../../../core/immutable.js";
3
4
  import { validateUserId } from "../../../core/identifiers.js";
@@ -19,7 +20,7 @@ function validateAxisOption(value, channel) {
19
20
  }
20
21
  }
21
22
 
22
- function validateArgs(args) {
23
+ export function validateAxesArgs(args) {
23
24
  validateOptionObject(args, TOP_OPTIONS, "createAxes");
24
25
  validateAxisOption(args.x, "x");
25
26
  validateAxisOption(args.y, "y");
@@ -189,9 +190,9 @@ const createAxes = action(
189
190
  description: "Read a semantic coordinate and create its Cartesian axes."
190
191
  },
191
192
  function (args = {}) {
192
- const coordinateDescriptor = validateArgs(args);
193
+ const coordinateDescriptor = validateAxesArgs(args);
193
194
  const { cartesianLayers, polarLayers, parallelLayers } = inspectChannels(
194
- this.semanticSpec.layers
195
+ this.semanticSpec.layers.filter(layer => !isSourceOwnedText(layer))
195
196
  );
196
197
  const requestedCoordinateType = coordinateDescriptor.id === undefined
197
198
  ? undefined
@@ -1,8 +1,11 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
2
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
3
  import { action } from "../../../core/action.js";
2
4
  import { isPlainObject } from "../../../core/immutable.js";
3
5
  import { validateUserId } from "../../../core/identifiers.js";
4
6
  import { validateKeys } from "../../../core/validation.js";
5
7
  import { hasCoordinate } from "../../../selectors/index.js";
8
+ import { validateEnabledAxisComponents } from "./components.js";
6
9
 
7
10
  const TOP_OPTIONS = Object.freeze([
8
11
  "scale",
@@ -38,14 +41,15 @@ function validateNested(value, supported, label) {
38
41
  validateKeys(value, supported, label);
39
42
  }
40
43
 
41
- function validateArgs(args, operation) {
44
+ export function validateAxisArgs(args, operation) {
42
45
  validateKeys(args, TOP_OPTIONS, operation);
43
46
 
44
- if (Object.hasOwn(args, "line")) {
47
+ validateEnabledAxisComponents(args, operation);
48
+ if (Object.hasOwn(args, "line") && args.line !== false) {
45
49
  validateNested(args.line, LINE_OPTIONS, `${operation}.line`);
46
50
  }
47
51
 
48
- if (Object.hasOwn(args, "ticksAndLabels")) {
52
+ if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
49
53
  validateNested(
50
54
  args.ticksAndLabels,
51
55
  TICK_GROUP_OPTIONS,
@@ -53,7 +57,7 @@ function validateArgs(args, operation) {
53
57
  );
54
58
  }
55
59
 
56
- if (Object.hasOwn(args, "title")) {
60
+ if (Object.hasOwn(args, "title") && args.title !== false) {
57
61
  validateNested(args.title, TITLE_OPTIONS, `${operation}.title`);
58
62
  }
59
63
  }
@@ -76,8 +80,8 @@ function makeCreateAxis(channel) {
76
80
  op: operation.create,
77
81
  description: `Create the complete ${channel}-axis.`
78
82
  },
79
- function (args = {}) {
80
- validateArgs(args, operation.create);
83
+ withGuideLayoutValidation(function (args = {}) {
84
+ validateAxisArgs(args, operation.create);
81
85
  const shared = {};
82
86
  if (Object.hasOwn(args, "scale")) shared.scale = args.scale;
83
87
  if (Object.hasOwn(args, "position")) shared.position = args.position;
@@ -89,7 +93,7 @@ function makeCreateAxis(channel) {
89
93
  const exists = hasCoordinate(next, coordinate);
90
94
  const hasConsumer = next.semanticSpec.layers.some(
91
95
  layer =>
92
- layer.coordinate === coordinate &&
96
+ !isSourceOwnedText(layer) && layer.coordinate === coordinate &&
93
97
  layer.encoding?.[channel]?.scale === scale
94
98
  );
95
99
 
@@ -108,17 +112,13 @@ function makeCreateAxis(channel) {
108
112
  });
109
113
  }
110
114
 
111
- return next[operation.line]({
112
- ...shared,
113
- ...(args.line ?? {})
114
- })[operation.ticksAndLabels]({
115
- ...shared,
116
- ...(args.ticksAndLabels ?? {})
117
- })[operation.title]({
118
- ...shared,
119
- ...(args.title ?? {})
120
- });
121
- }
115
+ for (const component of ["line", "ticksAndLabels", "title"]) {
116
+ if (args[component] !== false) {
117
+ next = next[operation[component]]({ ...shared, ...(args[component] ?? {}) });
118
+ }
119
+ }
120
+ return next;
121
+ })
122
122
  );
123
123
  }
124
124
 
@@ -0,0 +1,43 @@
1
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
2
+
3
+ export const AXIS_COMPONENTS = Object.freeze(["line", "ticks", "labels", "title"]);
4
+
5
+ export function axisComponentId(channel, component) {
6
+ return axisGraphicIds(channel)[AXIS_COMPONENTS.indexOf(component)];
7
+ }
8
+
9
+ export function axisComponentDisabled(args, component) {
10
+ return args[component === "ticks" || component === "labels"
11
+ ? "ticksAndLabels" : component] === false;
12
+ }
13
+
14
+ export function validateEnabledAxisComponents(args, operation) {
15
+ if (AXIS_COMPONENTS.every(component => axisComponentDisabled(args, component))) {
16
+ throw new Error(`${operation} requires at least one enabled axis component.`);
17
+ }
18
+ }
19
+
20
+ export function hasAxisComponent(program, channel, component) {
21
+ return program.graphicSpec.objects[axisComponentId(channel, component)] !== undefined ||
22
+ program.guideConfigs.axis?.[channel]?.[component] !== undefined ||
23
+ (component === "title" &&
24
+ program.semanticSpec.guides.axis?.[channel]?.title !== undefined);
25
+ }
26
+
27
+ export function assertRemovableAxisComponent(program, channel, component, operation) {
28
+ if (!hasAxisComponent(program, channel, component)) {
29
+ throw new Error(`${operation}.${component} requires an existing ${channel}-axis ${component}.`);
30
+ }
31
+ }
32
+
33
+ export function removeAxisComponent(program, channel, component) {
34
+ const id = axisComponentId(channel, component);
35
+ let next = program;
36
+ if (component === "title" && next.semanticSpec.guides.axis?.[channel]?.title !== undefined) {
37
+ next = next.editSemantic({ property: `guide.axis.${channel}.title`, remove: true });
38
+ }
39
+ if (next.graphicSpec.objects[id] !== undefined) {
40
+ next = next.editGraphics({ target: id, remove: true });
41
+ }
42
+ return next._withoutMaterializationConfig(["guides", "axis", channel, component]);
43
+ }
@@ -1,7 +1,16 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
2
  import { action } from "../../../core/action.js";
2
3
  import { isPlainObject } from "../../../core/immutable.js";
3
4
  import { validateKeys, validateOptionObject } from "../../../core/validation.js";
4
5
 
6
+ import {
7
+ AXIS_COMPONENTS,
8
+ axisComponentId as graphicId,
9
+ hasAxisComponent as hasComponent,
10
+ assertRemovableAxisComponent as assertRemovable,
11
+ removeAxisComponent as removeComponent
12
+ } from "./components.js";
13
+
5
14
  const OPTIONS = Object.freeze([
6
15
  "position", "line", "ticks", "labels", "ticksAndLabels", "title"
7
16
  ]);
@@ -11,19 +20,14 @@ const TICK_OPTIONS = Object.freeze([
11
20
  ]);
12
21
  const LABEL_OPTIONS = Object.freeze([
13
22
  "count", "values", "offset", "format", "color", "fontSize",
14
- "fontFamily", "fontWeight"
23
+ "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
24
+ "lineHeight", "overlap"
15
25
  ]);
16
26
  const GROUP_OPTIONS = Object.freeze(["count", "values", "ticks", "labels"]);
17
27
  const TITLE_OPTIONS = Object.freeze([
18
28
  "text", "at", "offset", "rotation", "color", "fontSize",
19
29
  "fontFamily", "fontWeight"
20
30
  ]);
21
- const COMPONENTS = Object.freeze({
22
- line: Object.freeze({ suffix: "Line", config: "line" }),
23
- ticks: Object.freeze({ suffix: "Ticks", config: "ticks" }),
24
- labels: Object.freeze({ suffix: "Labels", config: "labels" }),
25
- title: Object.freeze({ suffix: "Title", config: "title" })
26
- });
27
31
 
28
32
  function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
29
33
  if (!Object.hasOwn(args, key)) return;
@@ -59,7 +63,8 @@ function validateArgs(args, operation) {
59
63
  }
60
64
  if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
61
65
  validateNested(args.ticksAndLabels, "labels", [
62
- "offset", "format", "color", "fontSize", "fontFamily", "fontWeight"
66
+ "offset", "format", "color", "fontSize", "fontFamily", "fontWeight",
67
+ "rotation", "maxWidth", "wrap", "lineHeight", "overlap"
63
68
  ], `${operation}.ticksAndLabels`);
64
69
  }
65
70
  }
@@ -77,46 +82,6 @@ function names(channel) {
77
82
  };
78
83
  }
79
84
 
80
- function graphicId(channel, component) {
81
- return `${channel}Axis${COMPONENTS[component].suffix}`;
82
- }
83
-
84
- function hasComponent(program, channel, component) {
85
- return program.graphicSpec.objects[graphicId(channel, component)] !== undefined ||
86
- program.guideConfigs.axis?.[channel]?.[COMPONENTS[component].config] !== undefined ||
87
- (component === "title" &&
88
- program.semanticSpec.guides.axis?.[channel]?.title !== undefined);
89
- }
90
-
91
- function assertRemovable(program, channel, component, operation) {
92
- if (!hasComponent(program, channel, component)) {
93
- throw new Error(
94
- `${operation}.${component} requires an existing ${channel}-axis ${component}.`
95
- );
96
- }
97
- }
98
-
99
- function removeComponent(program, channel, component) {
100
- const id = graphicId(channel, component);
101
- let next = program;
102
-
103
- if (
104
- component === "title" &&
105
- next.semanticSpec.guides.axis?.[channel]?.title !== undefined
106
- ) {
107
- next = next.editSemantic({
108
- property: `guide.axis.${channel}.title`,
109
- remove: true
110
- });
111
- }
112
- if (next.graphicSpec.objects[id] !== undefined) {
113
- next = next.editGraphics({ target: id, remove: true });
114
- }
115
- return next._withoutMaterializationConfig([
116
- "guides", "axis", channel, COMPONENTS[component].config
117
- ]);
118
- }
119
-
120
85
  function buildPlan(program, channel, args, operation) {
121
86
  const shared = Object.hasOwn(args, "position")
122
87
  ? { position: args.position }
@@ -168,7 +133,7 @@ function applyPlan(program, channel, plan, operation) {
168
133
  : next[step.operation](step.args);
169
134
  }
170
135
 
171
- const hasRetainedGraphic = Object.keys(COMPONENTS).some(
136
+ const hasRetainedGraphic = AXIS_COMPONENTS.some(
172
137
  component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
173
138
  );
174
139
  if (!hasRetainedGraphic && (
@@ -187,7 +152,7 @@ function makeEditAxis(channel) {
187
152
  op: operation.operation,
188
153
  description: `Edit selected existing ${channel}-axis components.`
189
154
  },
190
- function (args = {}) {
155
+ withGuideLayoutValidation(function (args = {}) {
191
156
  validateArgs(args, operation.operation);
192
157
  const plan = buildPlan(this, channel, args, operation);
193
158
 
@@ -195,7 +160,7 @@ function makeEditAxis(channel) {
195
160
  // later leaf failure cannot begin the returned action trace or state.
196
161
  applyPlan(this, channel, plan, operation);
197
162
  return applyPlan(this, channel, plan, operation);
198
- }
163
+ })
199
164
  );
200
165
  }
201
166
 
@@ -1,3 +1,4 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
2
  import { action } from "../../../core/action.js";
2
3
  import { validateUserId } from "../../../core/identifiers.js";
3
4
  import {
@@ -16,6 +17,7 @@ import {
16
17
  mapOrdinalPositionValues
17
18
  } from "../../../grammar/scales/index.js";
18
19
  import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
20
+ import { resolveRotation } from "../../../grammar/rotation.js";
19
21
  import { valuesFromTickConfig } from "../tickValues.js";
20
22
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
21
23
  "../../../theme/defaults.js";
@@ -35,10 +37,12 @@ import {
35
37
  } from "../../../core/textMetrics.js";
36
38
  import { resolveConcreteGraphicBounds } from
37
39
  "../../../grammar/schemas/graphicBounds.js";
40
+ import { wrapText } from "../../../layout/text.js";
38
41
 
39
42
  const OPTIONS = [
40
43
  "scale", "position", "count", "values", "offset", "format", "color",
41
- "fontSize", "fontFamily", "fontWeight"
44
+ "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
45
+ "lineHeight", "overlap"
42
46
  ];
43
47
 
44
48
  const DEFAULTS = {
@@ -46,9 +50,30 @@ const DEFAULTS = {
46
50
  color: DEFAULT_COLORS.text,
47
51
  fontSize: 12,
48
52
  fontFamily: DEFAULT_FONT_FAMILY,
49
- fontWeight: "normal"
53
+ fontWeight: "normal",
54
+ rotation: 0,
55
+ overlap: "error"
50
56
  };
51
57
 
58
+ function normalizeLabelLayout(config, args, operation) {
59
+ const next = { ...config };
60
+ next.rotation = Object.hasOwn(args, "rotation")
61
+ ? resolveRotation(args.rotation, `${operation} rotation`)
62
+ : next.rotation ?? 0;
63
+ next.overlap ??= "error";
64
+ if (args.maxWidth === false) {
65
+ if (Object.hasOwn(args, "wrap") || Object.hasOwn(args, "lineHeight")) {
66
+ throw new Error(`${operation} cannot combine maxWidth false with wrap or lineHeight.`);
67
+ }
68
+ delete next.maxWidth;
69
+ delete next.wrap;
70
+ delete next.lineHeight;
71
+ } else if (next.maxWidth !== undefined) {
72
+ next.wrap ??= "word";
73
+ }
74
+ return next;
75
+ }
76
+
52
77
  export function validateAxisTextStyle(config, label) {
53
78
  validateNonEmptyString(config.color, `${label} color`);
54
79
  validatePositiveFinite(config.fontSize, `${label} fontSize`);
@@ -90,6 +115,25 @@ function validateConfig(channel, config) {
90
115
  validateNonNegativeFinite(config.offset, "Label offset");
91
116
  validateAxisTextStyle(config, "Label");
92
117
  validateAxisFormat(config.format);
118
+ if (!Number.isFinite(config.rotation)) {
119
+ throw new TypeError("Label rotation must resolve to finite radians.");
120
+ }
121
+ if (config.maxWidth !== undefined) {
122
+ validatePositiveFinite(config.maxWidth, "Label maxWidth");
123
+ if (!["word", "character"].includes(config.wrap)) {
124
+ throw new Error(`Unsupported axis label wrap "${config.wrap}".`);
125
+ }
126
+ } else if (config.wrap !== undefined || config.lineHeight !== undefined) {
127
+ throw new Error("Axis label wrap and lineHeight require maxWidth.");
128
+ }
129
+ if (config.lineHeight !== undefined && (
130
+ !Number.isFinite(config.lineHeight) || config.lineHeight < config.fontSize
131
+ )) {
132
+ throw new RangeError("Axis label lineHeight must cover fontSize.");
133
+ }
134
+ if (!["error", "allow"].includes(config.overlap)) {
135
+ throw new Error(`Unsupported axis label overlap policy "${config.overlap}".`);
136
+ }
93
137
  }
94
138
 
95
139
  function assertTickCompatibility(ticks, config, operation) {
@@ -99,6 +143,59 @@ function assertTickCompatibility(ticks, config, operation) {
99
143
  if (config.mode === "values" && !sameOrderedValues(ticks.values, config.values)) throw new Error(`${operation} conflicts with axis ticks.`);
100
144
  }
101
145
 
146
+ function expandWrappedLabels(resolved, text, config, channel) {
147
+ if (config.maxWidth === undefined) {
148
+ return {
149
+ ...resolved,
150
+ text,
151
+ groups: text.map((_, index) => index)
152
+ };
153
+ }
154
+ const style = {
155
+ fontSize: config.fontSize,
156
+ fontFamily: config.fontFamily,
157
+ fontWeight: config.fontWeight
158
+ };
159
+ const lineHeight = config.lineHeight ?? config.fontSize * 1.2;
160
+ const expanded = { text: [], x: [], y: [], groups: [] };
161
+ for (let index = 0; index < text.length; index += 1) {
162
+ const lines = wrapText(text[index], {
163
+ maxWidth: config.maxWidth,
164
+ mode: config.wrap,
165
+ style
166
+ });
167
+ validateGeneratedItemLimit(
168
+ expanded.text.length + lines.length,
169
+ "Axis label line count"
170
+ );
171
+ const baseX = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
172
+ const baseY = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
173
+ for (let line = 0; line < lines.length; line += 1) {
174
+ expanded.text.push(lines[line]);
175
+ expanded.x.push(baseX);
176
+ expanded.y.push(channel === "x"
177
+ ? baseY + (resolved.textBaseline === "top" ? line : -line) * lineHeight
178
+ : baseY + (line - (lines.length - 1) / 2) * lineHeight);
179
+ expanded.groups.push(index);
180
+ }
181
+ }
182
+ return { ...resolved, ...expanded };
183
+ }
184
+
185
+ function unionLabelBounds(records) {
186
+ const byGroup = new Map();
187
+ for (const { bounds, group } of records) {
188
+ const previous = byGroup.get(group);
189
+ byGroup.set(group, previous === undefined ? bounds : {
190
+ left: Math.min(previous.left, bounds.left),
191
+ right: Math.max(previous.right, bounds.right),
192
+ top: Math.min(previous.top, bounds.top),
193
+ bottom: Math.max(previous.bottom, bounds.bottom)
194
+ });
195
+ }
196
+ return [...byGroup.values()];
197
+ }
198
+
102
199
  function resolve(program, channel, config) {
103
200
  const scale = program.resolvedScales[config.scale];
104
201
  const bounds = resolveGraphicBounds(program);
@@ -132,9 +229,8 @@ function resolve(program, channel, config) {
132
229
  ? formatTransformedTick(scale.type, item)
133
230
  : String(item)
134
231
  ));
135
- const resolved = {
232
+ const geometry = {
136
233
  values,
137
- text,
138
234
  ...resolveAxisLabelGeometry({
139
235
  bounds,
140
236
  channel,
@@ -143,8 +239,9 @@ function resolve(program, channel, config) {
143
239
  offset: config.offset
144
240
  })
145
241
  };
242
+ const resolved = expandWrappedLabels(geometry, text, config, channel);
146
243
  const canvas = findCanvasGraphic(program)?.properties;
147
- const labelBounds = text.map((value, index) => resolveTextBounds({
244
+ const labelBounds = resolved.text.map((value, index) => resolveTextBounds({
148
245
  x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
149
246
  y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
150
247
  text: value,
@@ -152,15 +249,20 @@ function resolve(program, channel, config) {
152
249
  fontFamily: config.fontFamily,
153
250
  fontWeight: config.fontWeight,
154
251
  textAlign: resolved.textAlign,
155
- textBaseline: resolved.textBaseline
252
+ textBaseline: resolved.textBaseline,
253
+ rotation: config.rotation
156
254
  }));
157
- const orderedBounds = [...labelBounds].sort((left, right) => channel === "x"
255
+ const groupedBounds = unionLabelBounds(labelBounds.map((bounds, index) => ({
256
+ bounds,
257
+ group: resolved.groups[index]
258
+ })));
259
+ const orderedBounds = groupedBounds.sort((left, right) => channel === "x"
158
260
  ? left.left - right.left
159
261
  : left.top - right.top);
160
262
  if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
161
263
  throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
162
264
  }
163
- if (orderedBounds.some((item, index) => index > 0 &&
265
+ if (config.overlap === "error" && orderedBounds.some((item, index) => index > 0 &&
164
266
  textBoundsIntersect(orderedBounds[index - 1], item))) {
165
267
  throw new Error(`The ${channel}-axis labels overlap each other.`);
166
268
  }
@@ -177,7 +279,7 @@ function resolve(program, channel, config) {
177
279
 
178
280
  function makeEdit(channel) {
179
281
  const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
180
- return action({ op, description: `Edit concrete ${channel}-axis labels.` }, function (args = {}) {
282
+ return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
181
283
  validateOptions(args, op, false);
182
284
  const id = `${channel}AxisLabels`;
183
285
  if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
@@ -192,20 +294,20 @@ function makeEdit(channel) {
192
294
  const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
193
295
  ? "values"
194
296
  : Object.hasOwn(args, "count") ? "count" : previous.mode;
195
- const config = {
297
+ const config = normalizeLabelLayout({
196
298
  ...previous,
197
299
  ...args,
198
300
  ...(inferredValues === undefined ? {} : { values: inferredValues }),
199
301
  ...(explicitMode ? { inferredValues: false } : {}),
200
302
  mode
201
- };
303
+ }, args, op);
202
304
  if (mode === "values") delete config.count; else delete config.values;
203
305
  validateConfig(channel, config);
204
306
  assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
205
307
  const resolved = resolve(this, channel, config);
206
308
  let next = this._withGuideConfig(channel, "labels", config);
207
- for (const [property, value] of Object.entries({
208
- length: resolved.values.length,
309
+ const properties = {
310
+ length: resolved.text.length,
209
311
  x: resolved.x,
210
312
  y: resolved.y,
211
313
  text: resolved.text,
@@ -215,14 +317,20 @@ function makeEdit(channel) {
215
317
  fontWeight: config.fontWeight,
216
318
  textAlign: resolved.textAlign,
217
319
  textBaseline: resolved.textBaseline
218
- })) next = next.editGraphics({ target: id, property, value });
320
+ };
321
+ if (config.rotation !== 0 || this.graphicSpec.objects[id].items.some(
322
+ item => Object.hasOwn(item.properties, "rotation")
323
+ )) properties.rotation = config.rotation;
324
+ for (const [property, value] of Object.entries(properties)) {
325
+ next = next.editGraphics({ target: id, property, value });
326
+ }
219
327
  const titleConfig = next.guideConfigs.axis?.[channel]?.title;
220
328
  if (titleConfig?.inferredOffset === true) {
221
329
  const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
222
330
  next = next[editTitle]();
223
331
  }
224
332
  return next;
225
- });
333
+ }));
226
334
  }
227
335
 
228
336
  const editXAxisLabels = makeEdit("x");
@@ -231,7 +339,7 @@ const editYAxisLabels = makeEdit("y");
231
339
  function makeCreate(channel) {
232
340
  const op = channel === "x" ? "createXAxisLabels" : "createYAxisLabels";
233
341
  const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
234
- return action({ op, description: `Create concrete ${channel}-axis labels.` }, function (args = {}) {
342
+ return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
235
343
  validateOptions(args, op, true);
236
344
  const scale = validateUserId(args.scale ?? channel, "Scale id");
237
345
  const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
@@ -242,7 +350,7 @@ function makeCreate(channel) {
242
350
  const hasValues = Object.hasOwn(args, "values");
243
351
  const hasCount = Object.hasOwn(args, "count");
244
352
  const mode = hasValues ? "values" : hasCount ? "count" : ticks?.mode ?? "count";
245
- const config = {
353
+ const config = normalizeLabelLayout({
246
354
  scale,
247
355
  position: defaultAxisPosition(channel),
248
356
  offset: channel === "x" ? 18 : 12,
@@ -251,10 +359,12 @@ function makeCreate(channel) {
251
359
  fontSize: DEFAULTS.fontSize,
252
360
  fontFamily: DEFAULTS.fontFamily,
253
361
  fontWeight: DEFAULTS.fontWeight,
362
+ rotation: DEFAULTS.rotation,
363
+ overlap: DEFAULTS.overlap,
254
364
  ...args,
255
365
  inferredValues: !hasValues && !hasCount && ticks?.inferredValues === true,
256
366
  mode
257
- };
367
+ }, args, op);
258
368
  if (mode === "values") config.values ??= ticks?.values; else config.count ??= ticks?.count ?? DEFAULTS.count;
259
369
  validateConfig(channel, config);
260
370
  assertTickCompatibility(ticks, config, op);
@@ -267,7 +377,7 @@ function makeCreate(channel) {
267
377
  ...resolvePlotGraphicPlacement(this)
268
378
  })
269
379
  ._withGuideConfig(channel, "labels", config)[edit]();
270
- });
380
+ }));
271
381
  }
272
382
 
273
383
  const createXAxisLabels = makeCreate("x");