ggaction 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +6 -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 +1 -1
  7. package/src/actions/canvas/fitting.js +6 -2
  8. package/src/actions/charts/heatmap.js +4 -3
  9. package/src/actions/data/derived.js +11 -1
  10. package/src/actions/data/edit.js +2 -0
  11. package/src/actions/data/index.js +4 -0
  12. package/src/actions/data/regression.js +24 -16
  13. package/src/actions/data/regressionStatistics.js +73 -0
  14. package/src/actions/data/shared.js +5 -2
  15. package/src/actions/distributions/revision.js +24 -18
  16. package/src/actions/encodings/remove.js +11 -6
  17. package/src/actions/encodings/shared.js +26 -24
  18. package/src/actions/facets/actions.js +18 -7
  19. package/src/actions/facets/guides.js +2 -1
  20. package/src/actions/guides/axes/axes.js +30 -11
  21. package/src/actions/guides/axes/axis.js +58 -8
  22. package/src/actions/guides/axes/components.js +13 -13
  23. package/src/actions/guides/axes/edit.js +36 -24
  24. package/src/actions/guides/axes/labels.js +37 -27
  25. package/src/actions/guides/axes/lines.js +22 -20
  26. package/src/actions/guides/axes/remove.js +10 -6
  27. package/src/actions/guides/axes/tickGroups.js +8 -7
  28. package/src/actions/guides/axes/ticks.js +31 -22
  29. package/src/actions/guides/axes/titles.js +53 -60
  30. package/src/actions/guides/facadeAxes.js +33 -24
  31. package/src/actions/guides/facadeLegend.js +3 -3
  32. package/src/actions/guides/grids/grid.js +1 -1
  33. package/src/actions/guides/grids/resolve.js +48 -51
  34. package/src/actions/guides/legends/blocks.js +43 -19
  35. package/src/actions/guides/legends/categorical/actions.js +30 -7
  36. package/src/actions/guides/legends/categorical/components.js +3 -10
  37. package/src/actions/guides/legends/categorical/index.js +2 -0
  38. package/src/actions/guides/legends/categorical/layout.js +9 -13
  39. package/src/actions/guides/legends/categorical/options.js +8 -2
  40. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  41. package/src/actions/guides/legends/categorical/resolve.js +9 -2
  42. package/src/actions/guides/legends/categorical/symbols.js +22 -8
  43. package/src/actions/guides/legends/colorSize.js +110 -0
  44. package/src/actions/guides/legends/combine.js +37 -0
  45. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
  46. package/src/actions/guides/legends/continuous/common.js +1 -1
  47. package/src/actions/guides/legends/continuous/gradient.js +27 -13
  48. package/src/actions/guides/legends/continuous/opacity.js +16 -11
  49. package/src/actions/guides/legends/creation.js +2 -0
  50. package/src/actions/guides/legends/edit.js +1 -1
  51. package/src/actions/guides/legends/focused.js +6 -5
  52. package/src/actions/guides/legends/lane.js +1 -1
  53. package/src/actions/guides/legends/lifecycle.js +2 -1
  54. package/src/actions/guides/legends/remove.js +6 -3
  55. package/src/actions/guides/legends/size.js +16 -6
  56. package/src/actions/guides/legends/strokeWidth.js +2 -0
  57. package/src/actions/guides/legends/target.js +2 -79
  58. package/src/actions/guides/legends/transition.js +24 -5
  59. package/src/actions/guides/tickValues.js +8 -5
  60. package/src/actions/marks/bar/edit.js +23 -12
  61. package/src/actions/marks/bar/materialize.js +6 -1
  62. package/src/actions/marks/point/materialize.js +5 -1
  63. package/src/actions/marks/remove.js +10 -10
  64. package/src/actions/primitives/createGraphics.js +3 -2
  65. package/src/actions/primitives/editGraphics.js +15 -11
  66. package/src/actions/primitives/graphicProperties.js +1 -6
  67. package/src/actions/primitives/semanticAction.js +27 -1
  68. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  69. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  70. package/src/actions/regression/edit.js +15 -3
  71. package/src/actions/scales/channels.js +2 -2
  72. package/src/actions/scales/create.js +3 -3
  73. package/src/actions/scales/definitions.js +1 -1
  74. package/src/actions/scales/edit.js +1 -1
  75. package/src/actions/scales/editPolicy.js +3 -0
  76. package/src/actions/scales/patch.js +1 -1
  77. package/src/actions/scales/quantitativeColor.js +8 -3
  78. package/src/actions/theme/reconcile.js +50 -23
  79. package/src/core/compositionState.js +2 -2
  80. package/src/core/graphicProperties.js +1 -0
  81. package/src/core/programState.js +4 -0
  82. package/src/core/resourceReferences.js +2 -2
  83. package/src/core/validation.js +9 -0
  84. package/src/core/vocabulary.js +3 -0
  85. package/src/grammar/axisTextPlacement.js +1 -0
  86. package/src/grammar/colorSizeLegend.js +39 -0
  87. package/src/grammar/computed.js +23 -1
  88. package/src/grammar/datasetSchema.js +10 -2
  89. package/src/grammar/displayLabels.js +8 -0
  90. package/src/grammar/facets/guides.js +54 -31
  91. package/src/grammar/facets/legendIntent.js +65 -0
  92. package/src/grammar/guideDash.js +1 -0
  93. package/src/grammar/legendBlocks.js +1 -0
  94. package/src/grammar/legendBorder.js +9 -0
  95. package/src/grammar/legendClip.js +19 -0
  96. package/src/grammar/legendCombinations.js +45 -0
  97. package/src/grammar/legendGradient.js +9 -0
  98. package/src/grammar/legendGroups.js +1 -1
  99. package/src/grammar/legendOpacity.js +6 -0
  100. package/src/grammar/pointShapes.js +16 -2
  101. package/src/grammar/regression/derive.js +9 -1
  102. package/src/grammar/regression/models.js +60 -0
  103. package/src/grammar/regression/parameters.js +9 -1
  104. package/src/grammar/regression/statistics.js +64 -0
  105. package/src/grammar/scales/color.js +1 -1
  106. package/src/grammar/scales/colorConsumers.js +1 -1
  107. package/src/grammar/scales/definition.js +1 -1
  108. package/src/grammar/scales/types.js +1 -1
  109. package/src/grammar/schemas/concreteGraphic.js +3 -0
  110. package/src/grammar/schemas/graphic.js +3 -0
  111. package/src/grammar/schemas/graphicBounds.js +31 -7
  112. package/src/grammar/schemas/semanticPath.js +17 -2
  113. package/src/grammar/summary.js +1 -1
  114. package/src/grammar/timeUnit.js +6 -4
  115. package/src/grammar/transformTopology.js +1 -0
  116. package/src/grammar/transforms.js +11 -1
  117. package/src/inspectionDescriptors.js +1 -1
  118. package/src/layout/guideCollisions.js +9 -1
  119. package/src/layout/legendGroup.js +1 -1
  120. package/src/materialization/compositionSnapshot.js +2 -7
  121. package/src/materialization/coordinateBounds.js +17 -4
  122. package/src/materialization/facetGuides/edit.js +30 -0
  123. package/src/materialization/facetGuides/layout.js +1 -1
  124. package/src/materialization/facetGuides/owner.js +125 -0
  125. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  126. package/src/materialization/facetGuides/placement.js +52 -6
  127. package/src/materialization/facetGuides/preparation.js +7 -0
  128. package/src/materialization/facets.js +1 -1
  129. package/src/materialization/graphicHierarchy.js +5 -1
  130. package/src/materialization/graphicTranslation.js +73 -0
  131. package/src/materialization/guides/layout.js +1 -1
  132. package/src/materialization/guides/resources.js +1 -1
  133. package/src/materialization/legendBorder.js +28 -0
  134. package/src/materialization/legendClip.js +76 -0
  135. package/src/materialization/legendGroups.js +1 -1
  136. package/src/materialization/legends.js +1 -1
  137. package/src/materialization/scaleGuideDependencies.js +13 -14
  138. package/src/materialization/scales/map.js +3 -0
  139. package/src/materialization/scales/resolve.js +2 -0
  140. package/src/materialization/typography.js +10 -2
  141. package/src/persistence/validation.js +174 -1
  142. package/src/persistence.js +1 -1
  143. package/src/renderers/canvas/index.js +20 -10
  144. package/src/renderers/canvas/isolated.js +166 -0
  145. package/src/renderers/canvas/limits.js +3 -0
  146. package/src/renderers/canvas/rect.js +1 -1
  147. package/src/renderers/pdf.js +2 -2
  148. package/src/renderers/png.js +2 -2
  149. package/src/renderers/svg.js +10 -3
  150. package/src/selectors/axes.js +1 -0
  151. package/src/selectors/graphicCapacity.js +38 -0
  152. package/src/version.js +1 -1
  153. package/types/basic.d.ts +2 -0
  154. package/types/index.d.ts +5 -0
  155. package/types/program.d.ts +206 -61
@@ -1,7 +1,9 @@
1
+ import { selectCartesianAxis, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
2
+ import { validateAxisPosition } from "./policy.js";
1
3
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
4
  import { action } from "../../../core/action.js";
3
5
  import { isPlainObject } from "../../../core/immutable.js";
4
- import { validateKeys, validateOptionObject } from "../../../core/validation.js";
6
+ import { validateKeys, validateOptionObject, validateNonNegativeFinite } from "../../../core/validation.js";
5
7
 
6
8
  import {
7
9
  AXIS_COMPONENTS,
@@ -12,21 +14,21 @@ import {
12
14
  } from "./components.js";
13
15
 
14
16
  const OPTIONS = Object.freeze([
15
- "position", "line", "ticks", "labels", "ticksAndLabels", "title"
17
+ "target", "position", "line", "ticks", "labels", "ticksAndLabels", "title", "offset"
16
18
  ]);
17
19
  const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
18
20
  const TICK_OPTIONS = Object.freeze([
19
- "count", "values", "length", "color", "lineWidth", "alongOffset"
21
+ "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"
20
22
  ]);
21
23
  const LABEL_OPTIONS = Object.freeze([
22
24
  "count", "values", "offset", "format", "color", "fontSize",
23
25
  "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
24
- "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap"
26
+ "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap", "align"
25
27
  ]);
26
28
  const GROUP_OPTIONS = Object.freeze(["count", "values", "ticks", "labels"]);
27
29
  const TITLE_OPTIONS = Object.freeze([
28
30
  "text", "at", "offset", "rotation", "color", "fontSize",
29
- "fontFamily", "fontWeight"
31
+ "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
30
32
  ]);
31
33
 
32
34
  function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
@@ -40,6 +42,7 @@ function validateNested(args, key, options, operation, { allowFalse = false } =
40
42
  }
41
43
 
42
44
  function validateArgs(args, operation) {
45
+ if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
43
46
  validateOptionObject(args, OPTIONS, operation, {
44
47
  allowEmpty: false,
45
48
  emptyMessage: `${operation} requires at least one axis change.`,
@@ -59,12 +62,12 @@ function validateArgs(args, operation) {
59
62
  );
60
63
  }
61
64
  if (args.ticksAndLabels !== false && args.ticksAndLabels?.ticks !== undefined) {
62
- validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth", "alongOffset"], `${operation}.ticksAndLabels`);
65
+ validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"], `${operation}.ticksAndLabels`);
63
66
  }
64
67
  if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
65
68
  validateNested(args.ticksAndLabels, "labels", [
66
69
  "offset", "format", "color", "fontSize", "fontFamily", "fontWeight",
67
- "rotation", "maxWidth", "wrap", "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap"
70
+ "rotation", "maxWidth", "wrap", "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap", "align"
68
71
  ], `${operation}.ticksAndLabels`);
69
72
  }
70
73
  }
@@ -82,27 +85,26 @@ function names(channel) {
82
85
  };
83
86
  }
84
87
 
85
- function buildPlan(program, channel, args, operation) {
86
- const shared = Object.hasOwn(args, "position")
87
- ? { position: args.position }
88
- : {};
88
+ function buildPlan(program, channel, args, operation, axisId = channel) {
89
+ const shared = { ...axisTargetArgs(axisId, channel),
90
+ ...(Object.hasOwn(args, "position") ? { position: args.position } : {}) };
89
91
  const plan = [];
90
92
  const addLeaf = (component, edit) => {
91
93
  const value = args[component];
92
94
  if (value === false) {
93
- assertRemovable(program, channel, component, operation.operation);
95
+ assertRemovable(program, channel, component, operation.operation, axisId);
94
96
  plan.push({ kind: "remove", component });
95
97
  } else if (value !== undefined) {
96
98
  plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
97
- } else if (args.position !== undefined && hasComponent(program, channel, component)) {
99
+ } else if ((args.position !== undefined || Object.hasOwn(args, "offset")) && hasComponent(program, channel, component, axisId)) {
98
100
  plan.push({ kind: "edit", operation: edit, args: shared });
99
101
  }
100
102
  };
101
103
 
102
104
  addLeaf("line", operation.line);
103
105
  if (args.ticksAndLabels === false) {
104
- assertRemovable(program, channel, "ticks", operation.operation);
105
- assertRemovable(program, channel, "labels", operation.operation);
106
+ assertRemovable(program, channel, "ticks", operation.operation, axisId);
107
+ assertRemovable(program, channel, "labels", operation.operation, axisId);
106
108
  plan.push(
107
109
  { kind: "remove", component: "ticks" },
108
110
  { kind: "remove", component: "labels" }
@@ -125,22 +127,22 @@ function buildPlan(program, channel, args, operation) {
125
127
  return plan;
126
128
  }
127
129
 
128
- function applyPlan(program, channel, plan, operation) {
130
+ function applyPlan(program, channel, plan, operation, axisId = channel) {
129
131
  let next = program;
130
132
  for (const step of plan) {
131
133
  next = step.kind === "remove"
132
- ? removeComponent(next, channel, step.component)
134
+ ? removeComponent(next, channel, step.component, axisId)
133
135
  : next[step.operation](step.args);
134
136
  }
135
137
 
136
138
  const hasRetainedGraphic = AXIS_COMPONENTS.some(
137
- component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
139
+ component => next.graphicSpec.objects[graphicId(channel, component, axisId)] !== undefined
138
140
  );
139
141
  if (!hasRetainedGraphic && (
140
- next.semanticSpec.guides.axis?.[channel] !== undefined ||
141
- next.guideConfigs.axis?.[channel] !== undefined
142
+ next.semanticSpec.guides.axis?.[axisId] !== undefined ||
143
+ next.guideConfigs.axis?.[axisId] !== undefined
142
144
  )) {
143
- next = next[operation.remove]();
145
+ next = next[operation.remove](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
144
146
  }
145
147
  return next;
146
148
  }
@@ -154,12 +156,22 @@ function makeEditAxis(channel) {
154
156
  },
155
157
  withGuideLayoutValidation(function (args = {}) {
156
158
  validateArgs(args, operation.operation);
157
- const plan = buildPlan(this, channel, args, operation);
159
+ const selected = selectCartesianAxis(this, channel, args, { whole: true });
160
+ const axisId = selected.id; args = selected.options;
161
+ let source = this;
162
+ if (selected.owner?.named && args.position !== undefined) {
163
+ const position = validateAxisPosition(channel, args.position);
164
+ assertCartesianAxisSideAvailable(this, { id: axisId, channel,
165
+ coordinate: selected.owner.coordinate, position });
166
+ source = source.editSemantic({ property: `guide.axis.${axisId}.position`, value: position });
167
+ }
168
+ if (Object.hasOwn(args, "offset")) source = source._withGuideConfig(axisId, "frame", { offset: args.offset });
169
+ const plan = buildPlan(source, channel, args, operation, axisId);
158
170
 
159
171
  // Run the complete proposal against an immutable speculative branch so a
160
172
  // later leaf failure cannot begin the returned action trace or state.
161
- applyPlan(this, channel, plan, operation);
162
- return applyPlan(this, channel, plan, operation);
173
+ applyPlan(source, channel, plan, operation, axisId);
174
+ return applyPlan(source, channel, plan, operation, axisId);
163
175
  })
164
176
  );
165
177
  }
@@ -1,4 +1,7 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
1
3
  import { axisThemeTypography } from "../../theme/state.js";
4
+ import { validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
2
5
  import { canvasOverflowError } from "../../../layout/canvas.js";
3
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
4
7
  import { action } from "../../../core/action.js";
@@ -11,7 +14,7 @@ import {
11
14
  validateNonNegativeFinite,
12
15
  validatePositiveFinite
13
16
  } from "../../../core/validation.js";
14
- import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
17
+ import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
15
18
  import {
16
19
  isTransformedScaleType,
17
20
  mapContinuousScaleValues,
@@ -48,9 +51,9 @@ import {
48
51
  } from "../../../grammar/displayLabels.js";
49
52
 
50
53
  const OPTIONS = [
51
- "scale", "position", "count", "values", "offset", "format", "color",
54
+ "target", "scale", "position", "count", "values", "offset", "format", "color",
52
55
  "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
53
- "lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap"
56
+ "lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
54
57
  ];
55
58
 
56
59
  const DEFAULTS = {
@@ -85,6 +88,7 @@ function normalizeLabelLayout(config, args, operation) {
85
88
  export { validateTextStyle as validateAxisTextStyle };
86
89
 
87
90
  function validateOptions(args, operation, create) {
91
+ validateAxisTextPlacement(args);
88
92
  validateOptionObject(
89
93
  args,
90
94
  create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
@@ -98,6 +102,7 @@ function validateOptions(args, operation, create) {
98
102
  }
99
103
 
100
104
  function validateConfig(channel, config) {
105
+ validateAxisTextPlacement(config);
101
106
  validateAxisPosition(channel, config.position);
102
107
  if (config.mode === "count") {
103
108
  if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
@@ -224,13 +229,9 @@ function unionLabelBounds(records) {
224
229
  return [...byGroup.values()];
225
230
  }
226
231
 
227
- function resolve(program, channel, config) {
232
+ function resolve(program, channel, config, axisId = channel) {
228
233
  const scale = program.resolvedScales[config.scale];
229
- const bounds = resolveGuideCoordinateBounds(program, {
230
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
231
- scale: config.scale,
232
- channel
233
- });
234
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
234
235
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
235
236
  validateAxisLabelMapScale(config, scale);
236
237
  if ((
@@ -292,7 +293,7 @@ function resolve(program, channel, config) {
292
293
  resolved.textBaseline = "middle";
293
294
  }
294
295
  const canvas = findCanvasGraphic(program)?.properties;
295
- let labelBounds = resolved.text.map((value, index) => resolveTextBounds({
296
+ const measuredBounds = () => resolved.text.map((value, index) => resolveTextBounds({
296
297
  x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
297
298
  y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
298
299
  text: value,
@@ -303,6 +304,7 @@ function resolve(program, channel, config) {
303
304
  textBaseline: resolved.textBaseline,
304
305
  rotation: config.rotation
305
306
  }, program.materializationConfigs.textMetrics));
307
+ let labelBounds = measuredBounds();
306
308
  if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
307
309
  const horizontal = channel === "x";
308
310
  const near = ["bottom", "right"].includes(config.position);
@@ -343,6 +345,10 @@ function resolve(program, channel, config) {
343
345
  return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
344
346
  });
345
347
  }
348
+ if (config.align !== undefined) {
349
+ resolved.textAlign = config.align;
350
+ labelBounds = measuredBounds();
351
+ }
346
352
  const rotatedOverlap = config.rotation !== 0;
347
353
  const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
348
354
  const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
@@ -369,11 +375,11 @@ function resolve(program, channel, config) {
369
375
  : index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
370
376
  throw new Error(`The ${channel}-axis labels overlap each other.`);
371
377
  }
372
- const title = program.graphicSpec.objects[`${channel}AxisTitle`]
373
- ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisTitle`, program.materializationConfigs.textMetrics)
378
+ const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
379
+ ? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
374
380
  : undefined;
375
381
  if (title &&
376
- program.guideConfigs.axis?.[channel]?.title?.inferredOffset !== true &&
382
+ program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
377
383
  labelBounds.some(item => textBoundsIntersect(item, title))) {
378
384
  throw new Error(`The ${channel}-axis labels overlap the axis title.`);
379
385
  }
@@ -384,12 +390,14 @@ function makeEdit(channel) {
384
390
  const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
385
391
  return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
386
392
  validateOptions(args, op, false);
387
- const id = `${channel}AxisLabels`;
393
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
394
+ const axisId = selected.id; args = selected.options;
395
+ const id = axisGraphicIds(channel, axisId)[2];
388
396
  if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
389
- const previous = this.guideConfigs.axis?.[channel]?.labels;
397
+ const previous = this.guideConfigs.axis?.[axisId]?.labels;
390
398
  if (!previous) throw new Error(`${op} requires label configuration.`);
391
399
  const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
392
- const ticks = this.guideConfigs.axis?.[channel]?.ticks;
400
+ const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
393
401
  const inferredValues = !explicitMode && previous.inferredValues === true &&
394
402
  ticks?.mode === "values"
395
403
  ? ticks.values
@@ -407,9 +415,9 @@ function makeEdit(channel) {
407
415
  config = normalizeAxisLabelMaps(this, config, args, op);
408
416
  if (mode === "values") delete config.count; else delete config.values;
409
417
  validateConfig(channel, config);
410
- assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
411
- const resolved = resolve(this, channel, config);
412
- let next = this._withGuideConfig(channel, "labels", config);
418
+ assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
419
+ const resolved = resolve(this, channel, config, axisId);
420
+ let next = this._withGuideConfig(axisId, "labels", config);
413
421
  const properties = {
414
422
  length: resolved.text.length,
415
423
  x: resolved.x,
@@ -430,10 +438,10 @@ function makeEdit(channel) {
430
438
  for (const [property, value] of Object.entries(properties)) {
431
439
  next = next.editGraphics({ target: id, property, value });
432
440
  }
433
- const titleConfig = next.guideConfigs.axis?.[channel]?.title;
441
+ const titleConfig = next.guideConfigs.axis?.[axisId]?.title;
434
442
  if (titleConfig?.inferredOffset === true) {
435
443
  const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
436
- next = next[editTitle]();
444
+ next = next[editTitle](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
437
445
  }
438
446
  return next;
439
447
  }));
@@ -447,12 +455,14 @@ function makeCreate(channel) {
447
455
  const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
448
456
  return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
449
457
  validateOptions(args, op, true);
458
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
459
+ const axisId = selected.id; args = selected.options;
450
460
  const scale = validateUserId(args.scale ?? channel, "Scale id");
451
- const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
461
+ const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
452
462
  if (guideScale && guideScale !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
453
- const id = `${channel}AxisLabels`;
463
+ const id = axisGraphicIds(channel, axisId)[2];
454
464
  if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis labels.`);
455
- const ticks = this.guideConfigs.axis?.[channel]?.ticks;
465
+ const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
456
466
  const hasValues = Object.hasOwn(args, "values");
457
467
  const hasCount = Object.hasOwn(args, "count");
458
468
  const resolvedScale = this.resolvedScales[scale];
@@ -488,15 +498,15 @@ function makeCreate(channel) {
488
498
  }
489
499
  validateConfig(channel, config);
490
500
  assertTickCompatibility(ticks, config, op);
491
- resolve(this, channel, config);
492
- return this.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
501
+ resolve(this, channel, config, axisId);
502
+ return this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
493
503
  .createGraphics({
494
504
  id,
495
505
  type: "text",
496
506
  length: 0,
497
507
  ...resolvePlotGraphicPlacement(this)
498
508
  })
499
- ._withGuideConfig(channel, "labels", config)[edit]();
509
+ ._withGuideConfig(axisId, "labels", config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
500
510
  }));
501
511
  }
502
512
 
@@ -1,3 +1,5 @@
1
+ import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
2
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
1
3
  import { requireSemanticScale } from "../../../selectors/scales.js";
2
4
  import { editGraphicProperties } from "../../primitives/graphicProperties.js";
3
5
  import { isSourceOwnedText } from "../../../grammar/text.js";
@@ -9,7 +11,7 @@ import {
9
11
  validateNonEmptyString,
10
12
  validateNonNegativeFinite
11
13
  } from "../../../core/validation.js";
12
- import { axisScaleBindings, resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
14
+ import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
13
15
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
14
16
  import {
15
17
  defaultAxisPosition,
@@ -20,8 +22,8 @@ import { resolvePlotGraphicPlacement } from
20
22
  "../../../materialization/graphicHierarchy.js";
21
23
 
22
24
  const DEFAULT_STYLE = Object.freeze({ color: DEFAULT_COLORS.text, lineWidth: 1 });
23
- const CREATE_OPTIONS = Object.freeze(["scale", "position", "color", "lineWidth"]);
24
- const EDIT_OPTIONS = Object.freeze(["position", "color", "lineWidth"]);
25
+ const CREATE_OPTIONS = Object.freeze(["target", "scale", "position", "color", "lineWidth"]);
26
+ const EDIT_OPTIONS = Object.freeze(["target", "position", "color", "lineWidth"]);
25
27
 
26
28
  function validateStyle({ color, lineWidth }) {
27
29
  validateNonEmptyString(color, "Axis line color");
@@ -42,7 +44,7 @@ function resolveLineRange(resolvedScale, bounds, channel, explicitRange) {
42
44
  : plotRange.reverse();
43
45
  }
44
46
 
45
- function resolveGeometry(program, channel, scaleId, position) {
47
+ function resolveGeometry(program, channel, scaleId, position, axisId = channel) {
46
48
  const hasConsumer = program.semanticSpec.layers.some(
47
49
  layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
48
50
  );
@@ -55,11 +57,7 @@ function resolveGeometry(program, channel, scaleId, position) {
55
57
 
56
58
  const resolvedScale = program.resolvedScales[scaleId];
57
59
  const range = resolvedScale?.range;
58
- const bounds = resolveGuideCoordinateBounds(program, {
59
- coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
60
- scale: scaleId,
61
- channel
62
- });
60
+ const bounds = resolveCartesianAxisFrameBounds(program, channel, position, scaleId, axisId);
63
61
 
64
62
  if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
65
63
  throw new Error(`Axis line requires a resolved numeric scale "${scaleId}".`);
@@ -80,10 +78,10 @@ function resolveGeometry(program, channel, scaleId, position) {
80
78
  );
81
79
  }
82
80
 
83
- function axisIds(channel) {
81
+ function axisIds(channel, axisId = channel) {
84
82
  return {
85
- graphic: `${channel}AxisLine`,
86
- guidePath: `guide.axis.${channel}.scale`
83
+ graphic: axisGraphicIds(channel, axisId)[0],
84
+ guidePath: `guide.axis.${axisId}.scale`
87
85
  };
88
86
  }
89
87
 
@@ -94,24 +92,26 @@ function createEditAxisLine(channel) {
94
92
  { op: operation, description: `Edit the concrete ${channel}-axis line.` },
95
93
  withGuideLayoutValidation(function (args = {}) {
96
94
  validateKeys(args, EDIT_OPTIONS, operation);
97
- const { graphic } = axisIds(channel);
95
+ const selected = selectCartesianAxis(this, channel, args, { create: false });
96
+ const axisId = selected.id; args = selected.options;
97
+ const { graphic } = axisIds(channel, axisId);
98
98
  const line = this.graphicSpec.objects[graphic];
99
99
 
100
100
  if (line?.type !== "line") {
101
101
  throw new Error(`${operation} requires an existing ${channel}-axis line.`);
102
102
  }
103
103
 
104
- const previous = this.guideConfigs.axis?.[channel]?.line;
104
+ const previous = this.guideConfigs.axis?.[axisId]?.line;
105
105
  const position = validateAxisPosition(
106
106
  channel,
107
107
  args.position ?? previous?.position ?? defaultAxisPosition(channel)
108
108
  );
109
- const scaleId = this.semanticSpec.guides.axis?.[channel]?.scale;
110
- const geometry = resolveGeometry(this, channel, scaleId, position);
109
+ const scaleId = this.semanticSpec.guides.axis?.[axisId]?.scale;
110
+ const geometry = resolveGeometry(this, channel, scaleId, position, axisId);
111
111
  const color = args.color ?? line.properties.stroke;
112
112
  const lineWidth = args.lineWidth ?? line.properties.strokeWidth;
113
113
  validateStyle({ color, lineWidth });
114
- let next = this._withGuideConfig(channel, "line", {
114
+ let next = this._withGuideConfig(axisId, "line", {
115
115
  position,
116
116
  color,
117
117
  lineWidth
@@ -140,6 +140,8 @@ function createAxisLine(channel) {
140
140
  { op: operation, description: `Create the concrete ${channel}-axis line.` },
141
141
  withGuideLayoutValidation(function (args = {}) {
142
142
  validateKeys(args, CREATE_OPTIONS, operation);
143
+ const selected = selectCartesianAxis(this, channel, args, { create: true });
144
+ const axisId = selected.id; args = selected.options;
143
145
  const scale = validateUserId(args.scale ?? channel, "Scale id");
144
146
  const position = validateAxisPosition(
145
147
  channel,
@@ -148,8 +150,8 @@ function createAxisLine(channel) {
148
150
  const color = args.color ?? DEFAULT_STYLE.color;
149
151
  const lineWidth = args.lineWidth ?? DEFAULT_STYLE.lineWidth;
150
152
  validateStyle({ color, lineWidth });
151
- resolveGeometry(this, channel, scale, position);
152
- const { graphic, guidePath } = axisIds(channel);
153
+ resolveGeometry(this, channel, scale, position, axisId);
154
+ const { graphic, guidePath } = axisIds(channel, axisId);
153
155
 
154
156
  if (this.graphicSpec.objects[graphic] !== undefined) {
155
157
  throw new Error(`${operation} requires a missing ${channel}-axis line.`);
@@ -162,7 +164,7 @@ function createAxisLine(channel) {
162
164
  type: "line",
163
165
  ...resolvePlotGraphicPlacement(this)
164
166
  })
165
- [editOperation]({ position, color, lineWidth });
167
+ [editOperation]({ ...axisTargetArgs(axisId, channel), position, color, lineWidth });
166
168
  })
167
169
  );
168
170
  }
@@ -1,3 +1,4 @@
1
+ import { selectCartesianAxis } from "../../../selectors/axes.js";
1
2
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
3
  import { action } from "../../../core/action.js";
3
4
  import { validateKeys } from "../../../core/validation.js";
@@ -15,16 +16,19 @@ function makeRemoveAxis(channel) {
15
16
  ? "Theta"
16
17
  : "Radial";
17
18
  const operation = `remove${prefix}Axis`;
18
- const graphicIds = axisGraphicIds(channel);
19
19
  return action(
20
20
  {
21
21
  op: operation,
22
22
  description: `Remove the complete ${channel}-axis resource.`
23
23
  },
24
24
  withGuideLayoutValidation(function (args = {}) {
25
- validateKeys(args, OPTIONS, operation);
26
- const semantic = this.semanticSpec.guides.axis?.[channel];
27
- const config = this.guideConfigs.axis?.[channel];
25
+ const cartesian = ["x", "y"].includes(channel);
26
+ validateKeys(args, cartesian ? [...OPTIONS, "target"] : OPTIONS, operation);
27
+ const selected = cartesian ? selectCartesianAxis(this, channel, args, { whole: true }) : { id: channel, options: args };
28
+ const axisId = selected.id; args = selected.options;
29
+ const graphicIds = axisGraphicIds(channel, axisId);
30
+ const semantic = this.semanticSpec.guides.axis?.[axisId];
31
+ const config = this.guideConfigs.axis?.[axisId];
28
32
  const hasGraphic = graphicIds.some(
29
33
  id => this.graphicSpec.objects[id] !== undefined
30
34
  );
@@ -42,7 +46,7 @@ function makeRemoveAxis(channel) {
42
46
  let next = semantic === undefined
43
47
  ? this
44
48
  : this.editSemantic({
45
- property: `guide.axis.${channel}`,
49
+ property: `guide.axis.${axisId}`,
46
50
  remove: true
47
51
  });
48
52
  for (const id of graphicIds) {
@@ -50,7 +54,7 @@ function makeRemoveAxis(channel) {
50
54
  next = next.editGraphics({ target: id, remove: true });
51
55
  }
52
56
  }
53
- return next._withoutMaterializationConfig(["guides", "axis", channel]);
57
+ return next._withoutMaterializationConfig(["guides", "axis", axisId]);
54
58
  })
55
59
  );
56
60
  }
@@ -3,7 +3,7 @@ import { action } from "../../../core/action.js";
3
3
  import { isPlainObject } from "../../../core/immutable.js";
4
4
  import { validateKeys, validateOptionObject } from "../../../core/validation.js";
5
5
 
6
- const CREATE_OPTIONS = Object.freeze([
6
+ const CREATE_OPTIONS = Object.freeze(["target",
7
7
  "scale",
8
8
  "position",
9
9
  "count",
@@ -11,14 +11,14 @@ const CREATE_OPTIONS = Object.freeze([
11
11
  "ticks",
12
12
  "labels"
13
13
  ]);
14
- const EDIT_OPTIONS = Object.freeze([
14
+ const EDIT_OPTIONS = Object.freeze(["target",
15
15
  "position",
16
16
  "count",
17
17
  "values",
18
18
  "ticks",
19
19
  "labels"
20
20
  ]);
21
- const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset"]);
21
+ const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
22
22
  const LABEL_OPTIONS = Object.freeze([
23
23
  "offset",
24
24
  "format",
@@ -34,10 +34,11 @@ const LABEL_OPTIONS = Object.freeze([
34
34
  "labelMap",
35
35
  "alongOffset",
36
36
  "flush",
37
- "colorMap"
37
+ "colorMap",
38
+ "align"
38
39
  ]);
39
- const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
40
- const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
40
+ const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
41
+ const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
41
42
 
42
43
  function validateNested(value, supported, label) {
43
44
  if (!isPlainObject(value)) {
@@ -104,7 +105,7 @@ function makeCreate(channel) {
104
105
  const shared = select(args, SHARED_CREATE);
105
106
  const tickArgs = { ...shared, ...(args.ticks ?? {}) };
106
107
  const labelArgs = {
107
- ...select(shared, ["scale", "position"]),
108
+ ...select(shared, ["target", "scale", "position"]),
108
109
  ...(args.labels ?? {})
109
110
  };
110
111