ggaction 0.0.21 → 0.0.22

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 (253) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/jitter.js +2 -2
  165. package/src/grammar/legendSpacing.js +45 -0
  166. package/src/grammar/lineMissing.js +90 -0
  167. package/src/grammar/lineSeries.js +1 -376
  168. package/src/grammar/lookup.js +97 -0
  169. package/src/grammar/markFilter.js +4 -2
  170. package/src/grammar/markLabels.js +8 -3
  171. package/src/grammar/numeric.js +1 -1
  172. package/src/grammar/pathCommands.js +1 -1
  173. package/src/grammar/pathSeries.js +1 -109
  174. package/src/grammar/pointPolygon.js +81 -0
  175. package/src/grammar/pointShapes.js +15 -15
  176. package/src/grammar/rects.js +1 -1
  177. package/src/grammar/regression/derive.js +1 -195
  178. package/src/grammar/scales/appearance.js +1 -1
  179. package/src/grammar/scales/color.js +1 -1
  180. package/src/grammar/scales/colorConsumers.js +1 -63
  181. package/src/grammar/scales/continuous.js +58 -1
  182. package/src/grammar/scales/definition.js +1 -1
  183. package/src/grammar/scales/ordinal.js +5 -2
  184. package/src/grammar/scales/policies.js +3 -3
  185. package/src/grammar/schemas/graphicBounds.js +40 -12
  186. package/src/grammar/schemas/graphicClip.js +23 -0
  187. package/src/grammar/schemas/graphicTree.js +16 -0
  188. package/src/grammar/statisticalReference.js +22 -0
  189. package/src/grammar/text.js +65 -1
  190. package/src/grammar/ticks.js +92 -1
  191. package/src/grammar/transformTopology.js +3 -1
  192. package/src/grammar/transforms.js +24 -0
  193. package/src/inspection.js +1 -495
  194. package/src/inspectionDescriptors.js +1 -1
  195. package/src/layout/canvas.js +1 -1
  196. package/src/layout/facetContentFlow.js +101 -0
  197. package/src/layout/facetHeaderTitles.js +1 -0
  198. package/src/layout/facetTopLane.js +29 -0
  199. package/src/layout/facets.js +1 -1
  200. package/src/layout/legendGroup.js +1 -1
  201. package/src/layout/legendItems.js +26 -10
  202. package/src/layout/legendSpacing.js +66 -0
  203. package/src/layout/plotDrawOrder.js +17 -0
  204. package/src/materialization/bars/histogram.js +8 -2
  205. package/src/materialization/bars/resolve.js +1 -1
  206. package/src/materialization/composition.js +1 -0
  207. package/src/materialization/compositionSnapshot.js +22 -2
  208. package/src/materialization/dataProvenance.js +1 -1
  209. package/src/materialization/facetGuides/layout.js +1 -1
  210. package/src/materialization/facetGuides/placement.js +23 -11
  211. package/src/materialization/facetGuides/preparation.js +43 -24
  212. package/src/materialization/facetHeaders.js +1 -1
  213. package/src/materialization/facetTopLane.js +25 -0
  214. package/src/materialization/facets.js +1 -1
  215. package/src/materialization/graphicHierarchy.js +5 -0
  216. package/src/materialization/graphicTranslation.js +2 -0
  217. package/src/materialization/guides/layout.js +1 -1
  218. package/src/materialization/guides/resources.js +1 -1
  219. package/src/materialization/legends.js +1 -1
  220. package/src/materialization/markClip.js +26 -0
  221. package/src/materialization/marks/capabilities.js +3 -2
  222. package/src/materialization/marks/grouping.js +7 -5
  223. package/src/materialization/marks/index.js +4 -2
  224. package/src/materialization/marks/policies.js +4 -0
  225. package/src/materialization/plotDrawOrder.js +44 -0
  226. package/src/materialization/rect.js +11 -3
  227. package/src/materialization/scaleGuideDependencies.js +13 -5
  228. package/src/materialization/selection/items/common.js +1 -1
  229. package/src/materialization/selection/items/path.js +1 -0
  230. package/src/materialization/selection/policies/index.js +2 -1
  231. package/src/materialization/text.js +10 -5
  232. package/src/mcp/cli.js +0 -0
  233. package/src/persistence/validation.js +108 -18
  234. package/src/persistence.js +4 -2
  235. package/src/renderers/canvas/index.js +16 -0
  236. package/src/renderers/canvas/isolated.js +11 -1
  237. package/src/renderers/canvas/native.js +7 -0
  238. package/src/renderers/canvas/scene.js +66 -0
  239. package/src/renderers/canvas/text.js +3 -2
  240. package/src/renderers/svg.js +1 -617
  241. package/src/renderers/svgSerialization.js +643 -0
  242. package/src/renderers/transitionScene.js +67 -0
  243. package/src/scene.js +89 -0
  244. package/src/selectors/axes.js +1 -1
  245. package/src/selectors/datasets.js +1 -0
  246. package/src/selectors/grids.js +9 -0
  247. package/src/selectors/markClip.js +28 -0
  248. package/src/selectors/raincloud.js +7 -0
  249. package/src/version.js +1 -1
  250. package/types/basic.d.ts +7 -0
  251. package/types/index.d.ts +8 -0
  252. package/types/program.d.ts +270 -60
  253. package/types/scene.d.ts +35 -0
@@ -1,3 +1,4 @@
1
+ import { axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
1
2
  import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
3
  import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
4
  import { axisThemeTypography } from "../../theme/state.js";
@@ -21,7 +22,7 @@ import {
21
22
  formatTransformedTick,
22
23
  mapOrdinalPositionValues
23
24
  } from "../../../grammar/scales/index.js";
24
- import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
25
+ import { formatTimeTick, formatTimeTicks, validateTickRounding } from "../../../grammar/ticks.js";
25
26
  import { resolveRotation } from "../../../grammar/rotation.js";
26
27
  import { valuesFromTickConfig } from "../tickValues.js";
27
28
  import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
@@ -29,6 +30,7 @@ import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
29
30
  import {
30
31
  defaultAxisPosition,
31
32
  formatAxisValue,
33
+ formatNumericAxisTicks,
32
34
  resolveAxisLabelGeometry,
33
35
  validateAxisFormat,
34
36
  validateAxisPosition
@@ -51,7 +53,7 @@ import {
51
53
  } from "../../../grammar/displayLabels.js";
52
54
 
53
55
  const OPTIONS = [
54
- "target", "scale", "position", "count", "values", "offset", "format", "color",
56
+ "target", "scale", "position", "count", "values", "rounding", "offset", "format", "color",
55
57
  "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
56
58
  "lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
57
59
  ];
@@ -88,6 +90,10 @@ function normalizeLabelLayout(config, args, operation) {
88
90
  export { validateTextStyle as validateAxisTextStyle };
89
91
 
90
92
  function validateOptions(args, operation, create) {
93
+ validateTickRounding(args.rounding, !Object.hasOwn(args, "rounding"));
94
+ if (Object.hasOwn(args, "rounding") && Object.hasOwn(args, "values")) {
95
+ throw new Error(`${operation} cannot use rounding and values together.`);
96
+ }
91
97
  validateAxisTextPlacement(args);
92
98
  validateOptionObject(
93
99
  args,
@@ -173,6 +179,9 @@ function assertTickCompatibility(ticks, config, operation) {
173
179
  if (!ticks) return;
174
180
  if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
175
181
  if (config.mode === "count" && ticks.count !== config.count) throw new Error(`${operation} conflicts with axis ticks.`);
182
+ if (config.mode === "count" && (ticks.rounding ?? "ceiling") !== (config.rounding ?? "ceiling")) {
183
+ throw new Error(`${operation} conflicts with axis tick rounding.`);
184
+ }
176
185
  if (config.mode === "values" && !sameOrderedValues(ticks.values, config.values)) throw new Error(`${operation} conflicts with axis ticks.`);
177
186
  }
178
187
 
@@ -261,12 +270,14 @@ function resolve(program, channel, config, axisId = channel) {
261
270
  ? formatTransformedTick(scale.type, item)
262
271
  : String(item)
263
272
  );
264
- const automaticTimeLabels = scale.type === "time" && config.format === "auto"
265
- ? formatTimeTicks(values, scale.domain)
266
- : undefined;
273
+ const automaticLabels = config.format === "ticks"
274
+ ? formatNumericAxisTicks(values, scale.type)
275
+ : scale.type === "time" && config.format === "auto"
276
+ ? formatTimeTicks(values, scale.domain)
277
+ : undefined;
267
278
  let text = values.map((value, index) =>
268
279
  resolveDisplayLabel(value, config.labelMap, raw =>
269
- automaticTimeLabels?.[index] ?? fallback(raw)));
280
+ automaticLabels?.[index] ?? fallback(raw)));
270
281
  // Empty mapped text suppresses the label, never its raw tick or scale value.
271
282
  // Filter all aligned arrays before wrapping so group/color indices stay dense.
272
283
  if (text.includes("")) {
@@ -378,7 +389,7 @@ function resolve(program, channel, config, axisId = channel) {
378
389
  const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
379
390
  ? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
380
391
  : undefined;
381
- if (title &&
392
+ if (title && !axisTitleAllowsLabelOverlap(program.guideConfigs.axis?.[axisId]?.title) &&
382
393
  program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
383
394
  labelBounds.some(item => textBoundsIntersect(item, title))) {
384
395
  throw new Error(`The ${channel}-axis labels overlap the axis title.`);
@@ -413,7 +424,10 @@ function makeEdit(channel) {
413
424
  mode
414
425
  }, args, op);
415
426
  config = normalizeAxisLabelMaps(this, config, args, op);
416
- if (mode === "values") delete config.count; else delete config.values;
427
+ if (mode === "values") {
428
+ delete config.count;
429
+ if (!Object.hasOwn(args, "rounding")) delete config.rounding;
430
+ } else delete config.values;
417
431
  validateConfig(channel, config);
418
432
  assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
419
433
  const resolved = resolve(this, channel, config, axisId);
@@ -485,6 +499,7 @@ function makeCreate(channel) {
485
499
  ...axisThemeTypography(this.materializationConfigs.theme, "labels"),
486
500
  rotation: DEFAULTS.rotation,
487
501
  overlap: DEFAULTS.overlap,
502
+ ...(ticks?.rounding === undefined ? {} : { rounding: ticks.rounding }),
488
503
  ...args,
489
504
  inferredValues: !hasValues && !hasCount &&
490
505
  (ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
@@ -493,6 +508,7 @@ function makeCreate(channel) {
493
508
  config = normalizeAxisLabelMaps(this, config, args, op);
494
509
  if (mode === "values") {
495
510
  config.values ??= ticks?.values ?? inferredDiscreteValues;
511
+ if (!Object.hasOwn(args, "rounding")) delete config.rounding;
496
512
  } else {
497
513
  config.count ??= ticks?.count ?? DEFAULTS.count;
498
514
  }
@@ -8,12 +8,10 @@ import {
8
8
  import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
9
9
  import { validateAxisTextStyle } from "../labels.js";
10
10
  import { validateAxisFormat } from "../policy.js";
11
+ import { axisGraphicIds } from "../../../../selectors/axes.js";
11
12
 
12
- export const PARALLEL_AXIS_PARTS = Object.freeze(["line", "ticks", "labels", "title"]);
13
- export const PARALLEL_AXIS_GRAPHICS = Object.freeze({
14
- line: "parallelAxisLines", ticks: "parallelAxisTicks",
15
- labels: "parallelAxisLabels", title: "parallelAxisTitles"
16
- });
13
+ import { PARALLEL_AXIS_PARTS, PARALLEL_AXIS_GRAPHICS } from "../../../../selectors/axes.js";
14
+ export { PARALLEL_AXIS_PARTS, PARALLEL_AXIS_GRAPHICS };
17
15
  const STYLE_OPTIONS = Object.freeze({
18
16
  line: ["color", "lineWidth"],
19
17
  ticks: ["count", "values", "length", "color", "lineWidth"],
@@ -9,7 +9,7 @@ import { niceTicks } from "../../../../grammar/ticks.js";
9
9
  import { resolveCoordinateBounds } from "../../../../materialization/coordinateBounds.js";
10
10
  import { findCoordinate } from "../../../../selectors/coordinates.js";
11
11
  import { findLayer } from "../../../../selectors/layers.js";
12
- import { formatAxisValue } from "../policy.js";
12
+ import { formatAxisValue, formatNumericAxisTicks } from "../policy.js";
13
13
  import { resolveParallelAxisConfigs } from "./policy.js";
14
14
 
15
15
  export function requireParallelAxisLayer(program, target) {
@@ -79,8 +79,9 @@ function componentValues(program, dimension, config) {
79
79
  return {
80
80
  values,
81
81
  y: discrete ? mapOrdinalPositionValues(values, scale) : mapContinuousScaleValues(values, scale),
82
- text: config.format === undefined ? undefined : values.map(value =>
83
- formatAxisValue(value, scale.type, config.format, formatValue))
82
+ text: config.format === undefined ? undefined
83
+ : config.format === "ticks" ? formatNumericAxisTicks(values, scale.type)
84
+ : values.map(value => formatAxisValue(value, scale.type, config.format, formatValue))
84
85
  };
85
86
  }
86
87
 
@@ -1,4 +1,6 @@
1
+ import { formatNumericTickValues } from "../../../grammar/ticks.js";
1
2
  import { formatVisibleText } from "../../../core/textMetrics.js";
3
+ import { validateBoolean } from "../../../core/validation.js";
2
4
  import {
3
5
  formatValue,
4
6
  isUtcValueFormat,
@@ -10,6 +12,15 @@ const AXIS_POSITIONS = Object.freeze({
10
12
  y: Object.freeze(["left", "right"])
11
13
  });
12
14
 
15
+ export function resolveAxisFrameConfig(current, args) {
16
+ if (args.underlay !== undefined) validateBoolean(args.underlay, "Axis underlay");
17
+ const frame = { ...current };
18
+ if (Object.hasOwn(args, "offset")) frame.offset = args.offset;
19
+ if (args.underlay === true) frame.underlay = true;
20
+ else if (args.underlay === false) delete frame.underlay;
21
+ return Object.keys(frame).length === 0 ? undefined : frame;
22
+ }
23
+
13
24
  export function defaultAxisPosition(channel) {
14
25
  return channel === "x" ? "bottom" : "left";
15
26
  }
@@ -96,12 +107,23 @@ export function resolveAxisTitleGeometry({
96
107
  }
97
108
 
98
109
  export function validateAxisFormat(format) {
110
+ if (format === "ticks") return format;
99
111
  return validateValueFormat(format, "Label format", {
100
112
  allowDecimalsObject: true
101
113
  });
102
114
  }
103
115
 
116
+ export function formatNumericAxisTicks(values, scaleType) {
117
+ if (["ordinal", "band", "point", "time"].includes(scaleType)) {
118
+ throw new Error('Axis format "ticks" requires a quantitative scale.');
119
+ }
120
+ return formatNumericTickValues(values);
121
+ }
122
+
104
123
  export function formatAxisValue(value, scaleType, format, autoFormatter) {
124
+ if (format === "ticks") {
125
+ throw new Error('Axis format "ticks" requires the complete settled tick values.');
126
+ }
105
127
  validateAxisFormat(format);
106
128
  if (format === "auto") return formatVisibleText(autoFormatter(value));
107
129
  if (["ordinal", "band", "point"].includes(scaleType)) {
@@ -8,6 +8,7 @@ const CREATE_OPTIONS = Object.freeze(["target",
8
8
  "position",
9
9
  "count",
10
10
  "values",
11
+ "rounding",
11
12
  "ticks",
12
13
  "labels"
13
14
  ]);
@@ -15,6 +16,7 @@ const EDIT_OPTIONS = Object.freeze(["target",
15
16
  "position",
16
17
  "count",
17
18
  "values",
19
+ "rounding",
18
20
  "ticks",
19
21
  "labels"
20
22
  ]);
@@ -37,8 +39,8 @@ const LABEL_OPTIONS = Object.freeze([
37
39
  "colorMap",
38
40
  "align"
39
41
  ]);
40
- const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
41
- const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
42
+ const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values", "rounding"]);
43
+ const SHARED_EDIT = Object.freeze(["target", "position", "count", "values", "rounding"]);
42
44
 
43
45
  function validateNested(value, supported, label) {
44
46
  if (!isPlainObject(value)) {
@@ -27,6 +27,7 @@ import {
27
27
  valuesFromTickConfig
28
28
  } from "../tickValues.js";
29
29
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
30
+ import { validateTickRounding } from "../../../grammar/ticks.js";
30
31
  import {
31
32
  defaultAxisPosition,
32
33
  resolveAxisTickGeometry,
@@ -35,7 +36,7 @@ import {
35
36
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
36
37
  "../../../materialization/graphicHierarchy.js";
37
38
 
38
- const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
39
+ const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "rounding", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
39
40
  const DEFAULTS = Object.freeze({
40
41
  count: DEFAULT_TICK_COUNT,
41
42
  length: 6,
@@ -44,6 +45,10 @@ const DEFAULTS = Object.freeze({
44
45
  });
45
46
 
46
47
  function validateOptions(args, operation, create) {
48
+ validateTickRounding(args.rounding, !Object.hasOwn(args, "rounding"));
49
+ if (Object.hasOwn(args, "rounding") && Object.hasOwn(args, "values")) {
50
+ throw new Error(`${operation} cannot use rounding and values together.`);
51
+ }
47
52
  validateOptionObject(
48
53
  args,
49
54
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
@@ -165,7 +170,10 @@ function makeEdit(channel) {
165
170
  ...(explicitMode ? { inferredValues: false } : {}),
166
171
  mode
167
172
  }, args);
168
- if (mode === "values") delete config.count; else delete config.values;
173
+ if (mode === "values") {
174
+ delete config.count;
175
+ if (!Object.hasOwn(args, "rounding")) delete config.rounding;
176
+ } else delete config.values;
169
177
  validateConfig(channel, config);
170
178
  const resolved = geometry(this, channel, config, axisId);
171
179
  let next = this._withGuideConfig(axisId, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
@@ -179,7 +187,11 @@ function makeEdit(channel) {
179
187
  next = next.editGraphics({ target: id, property: "strokeDash", value: resolveGuideDashes(resolved.values, config) });
180
188
  }
181
189
  // Structural dependency plans own grids during a scale/role handoff.
182
- return explicitMode ? applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId)) : next;
190
+ const explicitSelection = explicitMode || Object.hasOwn(args, "rounding");
191
+ return explicitSelection ? applyMaterializationPlan(next,
192
+ planAxisTickGridRematerialization(next, channel, axisId, {
193
+ syncLabels: Object.hasOwn(config, "rounding") || Object.hasOwn(previous, "rounding")
194
+ })) : next;
183
195
  }));
184
196
  }
185
197
 
@@ -198,6 +210,10 @@ function makeCreate(channel) {
198
210
  const id = axisGraphicIds(channel, axisId)[1];
199
211
  if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis ticks.`);
200
212
  const resolvedScale = this.resolvedScales[scale];
213
+ const labels = this.guideConfigs.axis?.[axisId]?.labels;
214
+ const inheritedSelection = labels?.rounding !== undefined && !Object.hasOwn(args, "values")
215
+ ? { count: labels.count, rounding: labels.rounding } : {};
216
+ args = { ...inheritedSelection, ...args };
201
217
  const inferredValues =
202
218
  Object.hasOwn(args, "count") || Object.hasOwn(args, "values")
203
219
  ? undefined
@@ -217,7 +233,9 @@ function makeCreate(channel) {
217
233
  ...resolvePlotGraphicPlacement(this)
218
234
  })
219
235
  ._withGuideConfig(axisId, config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
220
- return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId));
236
+ return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId, {
237
+ syncLabels: config.rounding !== undefined || labels?.rounding !== undefined
238
+ }));
221
239
  }));
222
240
  }
223
241
 
@@ -1,7 +1,7 @@
1
1
  import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
2
  import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
3
  import { materializeTextLines } from "../../../materialization/textLines.js";
4
- import { axisTitleTextLines as titleLines, validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
4
+ import { axisTitleTextLines as titleLines, validateAxisTextPlacement, axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
5
5
  import { axisThemeTypography } from "../../theme/state.js";
6
6
  import { canvasOverflowError } from "../../../layout/canvas.js";
7
7
  import { isSourceOwnedText } from "../../../grammar/text.js";
@@ -21,6 +21,7 @@ import {
21
21
  import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
22
22
  "../../../theme/defaults.js";
23
23
  import { findDataset } from "../../../selectors/datasets.js";
24
+ import { findUpstreamTransform } from "../../../materialization/dataProvenance.js";
24
25
  import { formatAggregateTitle } from "../../../grammar/aggregate.js";
25
26
  import { finiteMidpoint } from "../../../grammar/numeric.js";
26
27
  import {
@@ -132,7 +133,7 @@ function inferredTitleOffset(program, channel, config, axisId = channel) {
132
133
 
133
134
  const CREATE_OPTIONS = ["target",
134
135
  "text", "scale", "position", "at", "offset", "rotation", "color",
135
- "fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
136
+ "fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline", "overlap"
136
137
  ];
137
138
  const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
138
139
  const DEFAULTS = {
@@ -159,6 +160,8 @@ export function inferAxisTitleText(program, channel, scaleId) {
159
160
  const primaryTitles = new Set();
160
161
  for (const layer of program.semanticSpec.layers) {
161
162
  if (isSourceOwnedText(layer)) continue;
163
+ // Reference statistics reuse the source axis title.
164
+ if (program.markConfigs[layer.id]?.statisticalReference !== undefined) continue;
162
165
  const primary = layer.encoding?.[channel];
163
166
  const encoding = layer.mark?.type === "area" && Object.hasOwn(primary ?? {}, "datum")
164
167
  ? layer.encoding?.[`${channel}2`] : primary;
@@ -173,9 +176,8 @@ export function inferAxisTitleText(program, channel, scaleId) {
173
176
  encoding.field.length
174
177
  ) {
175
178
  const dataset = findDataset(program, layer.data);
176
- const transform = dataset?.transform?.length === 1
177
- ? dataset.transform[0]
178
- : undefined;
179
+ const transform = findUpstreamTransform(program, dataset,
180
+ ["density", "ecdf", "interval", "boxSummary", "gradientProfile", "summary"]);
179
181
  let derivedTitle;
180
182
  if (transform?.type === "density") {
181
183
  derivedTitle = encoding.field === transform.as?.[0]
@@ -183,12 +185,23 @@ export function inferAxisTitleText(program, channel, scaleId) {
183
185
  : encoding.field === transform.as?.[1]
184
186
  ? "Density"
185
187
  : undefined;
188
+ } else if (transform?.type === "ecdf") {
189
+ derivedTitle = encoding.field === transform.as.value ? transform.field
190
+ : encoding.field === transform.as.probability ? "Cumulative probability"
191
+ : encoding.field === transform.as.cumulative ? "Cumulative count" : undefined;
186
192
  } else if (transform?.type === "interval" &&
187
193
  Object.values(transform.as).includes(encoding.field)) {
188
194
  derivedTitle = `${transform.center}(${transform.field})`;
189
195
  } else if (transform?.type === "boxSummary" &&
190
196
  Object.values(transform.as).includes(encoding.field)) {
191
197
  derivedTitle = transform.field;
198
+ } else if (transform?.type === "gradientProfile" &&
199
+ [transform.as.lower, transform.as.upper, transform.as.center].includes(encoding.field)) {
200
+ // Profile bounds and the optional center share one source measure.
201
+ derivedTitle = transform.field;
202
+ } else if (transform?.type === "summary") {
203
+ const aggregate = transform.aggregates.find(value => value.as === encoding.field);
204
+ if (aggregate) derivedTitle = formatAggregateTitle(aggregate.op, aggregate.field);
192
205
  }
193
206
  const title = encoding.title ?? derivedTitle ?? (encoding.aggregate === undefined
194
207
  ? encoding.field
@@ -228,7 +241,7 @@ function resolveGeometry(program, channel, config, axisId = channel) {
228
241
  throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
229
242
  }
230
243
  const labelBounds = axisLabelBounds(program, channel, axisId);
231
- if (labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
244
+ if (!axisTitleAllowsLabelOverlap(config) && labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
232
245
  throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
233
246
  }
234
247
  return geometry;
@@ -91,6 +91,9 @@ function planAxis(program, channel, args, explicit) {
91
91
  if (explicit.offset !== undefined && explicit.offset !== (configs.frame?.offset ?? 0)) {
92
92
  guideConflict(`${channel} axis uses a different frame offset`);
93
93
  }
94
+ if (explicit.underlay !== undefined && explicit.underlay !== (configs.frame?.underlay === true)) {
95
+ guideConflict(`${channel} axis uses a different underlay policy`);
96
+ }
94
97
  const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
95
98
  if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
96
99
  const position = explicit.position ?? positions[0];
@@ -15,6 +15,7 @@ import {
15
15
  } from "./resolve.js";
16
16
  import { polarGuideNames } from "../polar/resolve.js";
17
17
  import { resolveAutomaticGridOptions } from "../applicability.js";
18
+ import { hasPlotUnderlays } from "../../../materialization/plotDrawOrder.js";
18
19
 
19
20
  const CARTESIAN_OPTIONS = Object.freeze(["horizontal", "vertical"]);
20
21
  const POLAR_OPTIONS = Object.freeze(["theta", "radial"]);
@@ -78,7 +79,7 @@ function makeRematerialize(direction) {
78
79
  value: geometry[property]
79
80
  });
80
81
  }
81
- return editGraphicProperties(next, operation.graphic, {
82
+ next = editGraphicProperties(next, operation.graphic, {
82
83
  stroke: config.color,
83
84
  strokeWidth: config.lineWidth
84
85
  })
@@ -87,6 +88,7 @@ function makeRematerialize(direction) {
87
88
  property: "strokeDash",
88
89
  value: geometry.strokeDash
89
90
  });
91
+ return hasPlotUnderlays(next) ? next.rematerializePlotDrawOrder() : next;
90
92
  }
91
93
  );
92
94
  }
@@ -1,4 +1,6 @@
1
1
  import { cartesianAxisOwners } from "../../../selectors/axes.js";
2
+ import { gridNames } from "../../../selectors/grids.js";
3
+ export { gridNames };
2
4
  import { axisGraphicIds } from "../../../materialization/guides/resources.js";
3
5
  import { isNominalValue } from "../../../grammar/scales/fields.js";
4
6
  import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
@@ -41,16 +43,6 @@ const DEFAULT_STYLE = Object.freeze({
41
43
  strokeDash: Object.freeze([])
42
44
  });
43
45
 
44
- export function gridNames(direction) {
45
- const prefix = direction === "horizontal" ? "Horizontal" : "Vertical";
46
- return {
47
- channel: direction === "horizontal" ? "y" : "x",
48
- create: `create${prefix}Grid`,
49
- rematerialize: `rematerialize${prefix}Grid`,
50
- graphic: `${direction}GridLines`
51
- };
52
- }
53
-
54
46
  function validateStrokeDash(value) {
55
47
  return validateGuideDash(value, "Grid strokeDash");
56
48
  }
@@ -215,10 +207,12 @@ export function resolveGridConfig(program, direction, args, resources) {
215
207
  export function refreshGridConfig(program, config) {
216
208
  if (config.inferredValues !== true) return config;
217
209
  const channel = config.direction === "horizontal" ? "y" : "x";
210
+ const inferred = inferGridTickConfig(program, channel, config.scale);
218
211
  const refreshed = {
219
212
  ...config,
220
- ...inferGridTickConfig(program, channel, config.scale)
213
+ ...inferred
221
214
  };
215
+ if (inferred.rounding === undefined) delete refreshed.rounding;
222
216
  if (refreshed.mode === "values") delete refreshed.count;
223
217
  else delete refreshed.values;
224
218
  return refreshed;
@@ -232,11 +226,13 @@ export function editGridConfig(previous, args) {
232
226
  )
233
227
  };
234
228
  if (Object.hasOwn(args, "count")) {
229
+ delete config.rounding;
235
230
  config.mode = "count";
236
231
  config.count = args.count;
237
232
  config.inferredValues = false;
238
233
  delete config.values;
239
234
  } else if (Array.isArray(args.values)) {
235
+ delete config.rounding;
240
236
  config.mode = "values";
241
237
  config.values = args.values;
242
238
  config.inferredValues = false;
@@ -37,6 +37,11 @@ function inferAxesOptions(program, applicability, layers) {
37
37
  ? { x: { ticksAndLabels: { count: 7 } } }
38
38
  : {};
39
39
  const directions = applicability.axes.directions;
40
+ for (const channel of ["x", "y"]) {
41
+ if (!directions[channel] && layers.some(layer => layer.encoding?.[channel] !== undefined)) {
42
+ inferred[channel] = false;
43
+ }
44
+ }
40
45
  const hasStoredY = layers.some(
41
46
  layer => layer.encoding?.y !== undefined
42
47
  );
@@ -126,11 +131,26 @@ export function resolveGuideOptions(program, args = {}, layers = program.semanti
126
131
  const createGuides = /* @__PURE__ */ action(
127
132
  {
128
133
  op: "createGuides",
129
- description: "Create applicable axes, grid, and legend."
134
+ description: "Create applicable axes, grid, and legend.",
135
+ scope: "any"
130
136
  },
131
137
  function (args = {}) {
138
+ if (this.compositionSpec?.facet) {
139
+ validateOptions(args);
140
+ if (Object.keys(args).length === 0) {
141
+ const selected = Object.entries(resolveGuideOptions(this, args))
142
+ .filter(([, options]) => options !== undefined);
143
+ if (selected.length > 0 && selected.every(([kind]) =>
144
+ hasStoredGuideCollection(this, kind === "axes" ? "axis" : kind)
145
+ )) return this;
146
+ }
147
+ return this.editFacetSource({ action: { method: "createGuides", options: args } });
148
+ }
149
+ this._assertUnitProgram("createGuides");
132
150
  const { axes, grid, legend } = resolveGuideOptions(this, args);
133
151
  if (axes === undefined && grid === undefined && legend === undefined) {
152
+ if (this.semanticSpec.layers.length > 0 &&
153
+ OPTIONS.some(key => args[key] === undefined)) return this;
134
154
  throw new Error("createGuides requires at least one selected guide.");
135
155
  }
136
156