ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -7,9 +7,21 @@ import {
7
7
  POLAR_POSITION_CHANNELS
8
8
  } from "../../core/vocabulary.js";
9
9
  import { findSemanticScale } from "../../selectors/scales.js";
10
+ import { findDataset } from "../../selectors/datasets.js";
11
+ import { findUpstreamTransform } from
12
+ "../../materialization/dataProvenance.js";
13
+
14
+ function isHorizonLayer(program, layer) {
15
+ return findUpstreamTransform(
16
+ program,
17
+ findDataset(program, layer.data),
18
+ "horizon"
19
+ ) !== undefined;
20
+ }
10
21
 
11
22
  function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
12
23
  return program.semanticSpec.layers.some(layer => {
24
+ if (isHorizonLayer(program, layer) && channel !== "x") return false;
13
25
  const encoding = layer.encoding?.[channel];
14
26
  if (!scaled && !grid) return encoding !== undefined;
15
27
  if (encoding?.scale === undefined) return false;
@@ -33,22 +45,24 @@ function positionalApplicability(program, channels) {
33
45
 
34
46
  export function hasInferableLegend(program) {
35
47
  return program.semanticSpec.layers.some(layer =>
36
- (layer.mark?.type === "point" &&
37
- layer.encoding?.opacity?.scale !== undefined) ||
38
- (layer.mark?.type === "point" &&
39
- layer.encoding?.size?.scale !== undefined) ||
40
- (layer.mark?.type === "point" &&
41
- layer.encoding?.color?.scale !== undefined &&
42
- (layer.encoding?.shape?.scale !== undefined ||
43
- ["sequential", "quantize", "quantile", "threshold"].includes(
44
- findSemanticScale(program, layer.encoding.color.scale)?.type
45
- ))) ||
46
- (layer.mark?.type === "line" &&
47
- ["color", "strokeDash"].some(
48
- channel => layer.encoding?.[channel]?.scale !== undefined
49
- )) ||
50
- (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
51
- layer.encoding?.color?.scale !== undefined)
48
+ !isHorizonLayer(program, layer) && (
49
+ (layer.mark?.type === "point" &&
50
+ layer.encoding?.opacity?.scale !== undefined) ||
51
+ (layer.mark?.type === "point" &&
52
+ layer.encoding?.size?.scale !== undefined) ||
53
+ (layer.mark?.type === "point" &&
54
+ layer.encoding?.color?.scale !== undefined &&
55
+ (layer.encoding?.shape?.scale !== undefined ||
56
+ ["sequential", "quantize", "quantile", "threshold"].includes(
57
+ findSemanticScale(program, layer.encoding.color.scale)?.type
58
+ ))) ||
59
+ (layer.mark?.type === "line" &&
60
+ ["color", "strokeDash"].some(
61
+ channel => layer.encoding?.[channel]?.scale !== undefined
62
+ )) ||
63
+ (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
64
+ layer.encoding?.color?.scale !== undefined)
65
+ )
52
66
  );
53
67
  }
54
68
 
@@ -58,12 +72,22 @@ export function resolveGuideApplicability(program) {
58
72
  CARTESIAN_POSITION_CHANNELS
59
73
  );
60
74
  const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
75
+ const parallel = program.semanticSpec.layers.some(layer =>
76
+ layer.encoding?.parallel?.dimensions?.length >= 2
77
+ );
61
78
  return Object.freeze({
62
79
  axes: Object.freeze({
63
80
  cartesian: CARTESIAN_POSITION_CHANNELS.some(
64
81
  channel => cartesian[channel].axis
65
82
  ),
66
- polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis)
83
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis),
84
+ parallel,
85
+ directions: Object.freeze({
86
+ x: cartesian.x.axis,
87
+ y: cartesian.y.axis,
88
+ theta: polar.theta.axis,
89
+ radius: polar.radius.axis
90
+ })
67
91
  }),
68
92
  grid: Object.freeze({
69
93
  cartesian: CARTESIAN_POSITION_CHANNELS.some(
@@ -90,6 +114,9 @@ export function resolveAutomaticGridOptions(program) {
90
114
  ...(directions.radial ? { radial: {} } : {})
91
115
  });
92
116
  }
117
+ if (directions.vertical && !directions.horizontal) {
118
+ return Object.freeze({ horizontal: false, vertical: {} });
119
+ }
93
120
 
94
121
  const barOrientations = program.semanticSpec.layers
95
122
  .map(resolveBarOrientation)
@@ -9,7 +9,9 @@ const TOP_OPTIONS = Object.freeze([
9
9
  "coordinate", "x", "y", "theta", "radius"
10
10
  ]);
11
11
  const COORDINATE_OPTIONS = Object.freeze(["id", "type"]);
12
- const COORDINATE_API_TYPES = new Set(["auto", "cartesian", "polar"]);
12
+ const COORDINATE_API_TYPES = new Set([
13
+ "auto", "cartesian", "polar", "parallel"
14
+ ]);
13
15
 
14
16
  function validateAxisOption(value, channel) {
15
17
  if (value !== undefined && value !== false && !isPlainObject(value)) {
@@ -47,6 +49,9 @@ function inspectChannels(layers) {
47
49
  layer =>
48
50
  layer.encoding?.theta !== undefined || layer.encoding?.radius !== undefined
49
51
  );
52
+ const parallelLayers = layers.filter(
53
+ layer => layer.encoding?.parallel !== undefined
54
+ );
50
55
  const hasMixedLayer = layers.some(layer => {
51
56
  const hasCartesian =
52
57
  layer.encoding?.x !== undefined || layer.encoding?.y !== undefined;
@@ -61,7 +66,7 @@ function inspectChannels(layers) {
61
66
  );
62
67
  }
63
68
 
64
- return { cartesianLayers, polarLayers };
69
+ return { cartesianLayers, polarLayers, parallelLayers };
65
70
  }
66
71
 
67
72
  function resolveCoordinate(program, descriptor, cartesianLayers) {
@@ -185,7 +190,7 @@ const createAxes = action(
185
190
  },
186
191
  function (args = {}) {
187
192
  const coordinateDescriptor = validateArgs(args);
188
- const { cartesianLayers, polarLayers } = inspectChannels(
193
+ const { cartesianLayers, polarLayers, parallelLayers } = inspectChannels(
189
194
  this.semanticSpec.layers
190
195
  );
191
196
  const requestedCoordinateType = coordinateDescriptor.id === undefined
@@ -194,6 +199,35 @@ const createAxes = action(
194
199
  const explicitlyPolar = coordinateDescriptor.type === "polar" ||
195
200
  requestedCoordinateType === "polar" ||
196
201
  args.theta !== undefined || args.radius !== undefined;
202
+ const explicitlyParallel = coordinateDescriptor.type === "parallel" ||
203
+ requestedCoordinateType === "parallel";
204
+ if (parallelLayers.length > 0 && (
205
+ (cartesianLayers.length === 0 && polarLayers.length === 0) ||
206
+ explicitlyParallel
207
+ )) {
208
+ if (
209
+ args.x !== undefined || args.y !== undefined ||
210
+ args.theta !== undefined || args.radius !== undefined
211
+ ) {
212
+ throw new Error(
213
+ "createAxes channel options are not supported for Parallel axes."
214
+ );
215
+ }
216
+ const candidates = coordinateDescriptor.id === undefined
217
+ ? parallelLayers
218
+ : parallelLayers.filter(layer =>
219
+ layer.coordinate === coordinateDescriptor.id
220
+ );
221
+ if (candidates.length !== 1) {
222
+ throw new Error(
223
+ "createAxes requires coordinate.id when one Parallel layer cannot be inferred."
224
+ );
225
+ }
226
+ return this.createParallelAxes({
227
+ target: candidates[0].id,
228
+ coordinate: candidates[0].coordinate
229
+ });
230
+ }
197
231
  if (polarLayers.length > 0 &&
198
232
  (cartesianLayers.length === 0 || explicitlyPolar)) {
199
233
  if (coordinateDescriptor.type === "cartesian") {
@@ -18,11 +18,19 @@ const TITLE_OPTIONS = Object.freeze([
18
18
  "text", "at", "offset", "rotation", "color", "fontSize",
19
19
  "fontFamily", "fontWeight"
20
20
  ]);
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
+ });
21
27
 
22
- function validateNested(args, key, options, operation) {
28
+ function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
23
29
  if (!Object.hasOwn(args, key)) return;
30
+ if (allowFalse && args[key] === false) return;
24
31
  if (!isPlainObject(args[key])) {
25
- throw new TypeError(`${operation}.${key} must be a plain object.`);
32
+ const accepted = allowFalse ? "false or a plain object" : "a plain object";
33
+ throw new TypeError(`${operation}.${key} must be ${accepted}.`);
26
34
  }
27
35
  validateKeys(args[key], options, `${operation}.${key}`);
28
36
  }
@@ -33,11 +41,12 @@ function validateArgs(args, operation) {
33
41
  emptyMessage: `${operation} requires at least one axis change.`,
34
42
  emptyError: Error
35
43
  });
36
- validateNested(args, "line", LINE_OPTIONS, operation);
37
- validateNested(args, "ticks", TICK_OPTIONS, operation);
38
- validateNested(args, "labels", LABEL_OPTIONS, operation);
39
- validateNested(args, "ticksAndLabels", GROUP_OPTIONS, operation);
40
- validateNested(args, "title", TITLE_OPTIONS, operation);
44
+ const removable = { allowFalse: true };
45
+ validateNested(args, "line", LINE_OPTIONS, operation, removable);
46
+ validateNested(args, "ticks", TICK_OPTIONS, operation, removable);
47
+ validateNested(args, "labels", LABEL_OPTIONS, operation, removable);
48
+ validateNested(args, "ticksAndLabels", GROUP_OPTIONS, operation, removable);
49
+ validateNested(args, "title", TITLE_OPTIONS, operation, removable);
41
50
  if (args.ticksAndLabels !== undefined && (
42
51
  args.ticks !== undefined || args.labels !== undefined
43
52
  )) {
@@ -45,10 +54,10 @@ function validateArgs(args, operation) {
45
54
  `${operation} cannot combine ticksAndLabels with ticks or labels.`
46
55
  );
47
56
  }
48
- if (args.ticksAndLabels?.ticks !== undefined) {
57
+ if (args.ticksAndLabels !== false && args.ticksAndLabels?.ticks !== undefined) {
49
58
  validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth"], `${operation}.ticksAndLabels`);
50
59
  }
51
- if (args.ticksAndLabels?.labels !== undefined) {
60
+ if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
52
61
  validateNested(args.ticksAndLabels, "labels", [
53
62
  "offset", "format", "color", "fontSize", "fontFamily", "fontWeight"
54
63
  ], `${operation}.ticksAndLabels`);
@@ -63,10 +72,114 @@ function names(channel) {
63
72
  ticks: `edit${prefix}AxisTicks`,
64
73
  labels: `edit${prefix}AxisLabels`,
65
74
  group: `edit${prefix}AxisTicksAndLabels`,
66
- title: `edit${prefix}AxisTitle`
75
+ title: `edit${prefix}AxisTitle`,
76
+ remove: `remove${prefix}Axis`
67
77
  };
68
78
  }
69
79
 
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
+ function buildPlan(program, channel, args, operation) {
121
+ const shared = Object.hasOwn(args, "position")
122
+ ? { position: args.position }
123
+ : {};
124
+ const plan = [];
125
+ const addLeaf = (component, edit) => {
126
+ const value = args[component];
127
+ if (value === false) {
128
+ assertRemovable(program, channel, component, operation.operation);
129
+ plan.push({ kind: "remove", component });
130
+ } else if (value !== undefined) {
131
+ plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
132
+ } else if (args.position !== undefined && hasComponent(program, channel, component)) {
133
+ plan.push({ kind: "edit", operation: edit, args: shared });
134
+ }
135
+ };
136
+
137
+ addLeaf("line", operation.line);
138
+ if (args.ticksAndLabels === false) {
139
+ assertRemovable(program, channel, "ticks", operation.operation);
140
+ assertRemovable(program, channel, "labels", operation.operation);
141
+ plan.push(
142
+ { kind: "remove", component: "ticks" },
143
+ { kind: "remove", component: "labels" }
144
+ );
145
+ } else if (args.ticksAndLabels !== undefined) {
146
+ plan.push({
147
+ kind: "edit",
148
+ operation: operation.group,
149
+ args: { ...shared, ...args.ticksAndLabels }
150
+ });
151
+ } else {
152
+ addLeaf("ticks", operation.ticks);
153
+ addLeaf("labels", operation.labels);
154
+ }
155
+ addLeaf("title", operation.title);
156
+
157
+ if (plan.length === 0) {
158
+ throw new Error(`${operation.operation} requires an existing ${channel}-axis component.`);
159
+ }
160
+ return plan;
161
+ }
162
+
163
+ function applyPlan(program, channel, plan, operation) {
164
+ let next = program;
165
+ for (const step of plan) {
166
+ next = step.kind === "remove"
167
+ ? removeComponent(next, channel, step.component)
168
+ : next[step.operation](step.args);
169
+ }
170
+
171
+ const hasRetainedGraphic = Object.keys(COMPONENTS).some(
172
+ component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
173
+ );
174
+ if (!hasRetainedGraphic && (
175
+ next.semanticSpec.guides.axis?.[channel] !== undefined ||
176
+ next.guideConfigs.axis?.[channel] !== undefined
177
+ )) {
178
+ next = next[operation.remove]();
179
+ }
180
+ return next;
181
+ }
182
+
70
183
  function makeEditAxis(channel) {
71
184
  const operation = names(channel);
72
185
  return action(
@@ -76,43 +189,12 @@ function makeEditAxis(channel) {
76
189
  },
77
190
  function (args = {}) {
78
191
  validateArgs(args, operation.operation);
79
- const shared = Object.hasOwn(args, "position")
80
- ? { position: args.position }
81
- : {};
82
- const has = id => this.graphicSpec.objects[id] !== undefined;
83
- let next = this;
84
-
85
- if (args.line !== undefined || (args.position !== undefined && has(`${channel}AxisLine`))) {
86
- next = next[operation.line]({ ...shared, ...(args.line ?? {}) });
87
- }
88
-
89
- if (args.ticksAndLabels !== undefined) {
90
- next = next[operation.group]({
91
- ...shared,
92
- ...args.ticksAndLabels
93
- });
94
- } else if (args.ticks !== undefined || args.labels !== undefined) {
95
- if (args.ticks !== undefined) {
96
- next = next[operation.ticks]({ ...shared, ...args.ticks });
97
- }
98
- if (args.labels !== undefined) {
99
- next = next[operation.labels]({ ...shared, ...args.labels });
100
- }
101
- } else if (args.position !== undefined) {
102
- const hasTicks = has(`${channel}AxisTicks`);
103
- const hasLabels = has(`${channel}AxisLabels`);
104
- if (hasTicks && hasLabels) {
105
- next = next[operation.group](shared);
106
- } else {
107
- if (hasTicks) next = next[operation.ticks](shared);
108
- if (hasLabels) next = next[operation.labels](shared);
109
- }
110
- }
111
-
112
- if (args.title !== undefined || (args.position !== undefined && has(`${channel}AxisTitle`))) {
113
- next = next[operation.title]({ ...shared, ...(args.title ?? {}) });
114
- }
115
- return next;
192
+ const plan = buildPlan(this, channel, args, operation);
193
+
194
+ // Run the complete proposal against an immutable speculative branch so a
195
+ // later leaf failure cannot begin the returned action trace or state.
196
+ applyPlan(this, channel, plan, operation);
197
+ return applyPlan(this, channel, plan, operation);
116
198
  }
117
199
  );
118
200
  }
@@ -12,6 +12,7 @@ import {
12
12
  removeXAxis,
13
13
  removeYAxis
14
14
  } from "./remove.js";
15
+ import { registerParallelAxisActions } from "./parallel.js";
15
16
 
16
17
  export function registerGuideAxisActions(ProgramClass) {
17
18
  registerAxisLineActions(ProgramClass);
@@ -22,6 +23,7 @@ export function registerGuideAxisActions(ProgramClass) {
22
23
  registerAxisActions(ProgramClass);
23
24
  registerAxisCollectionActions(ProgramClass);
24
25
  registerCompleteAxisEditActions(ProgramClass);
26
+ registerParallelAxisActions(ProgramClass);
25
27
  ProgramClass.prototype.removeXAxis = removeXAxis;
26
28
  ProgramClass.prototype.removeYAxis = removeYAxis;
27
29
  ProgramClass.prototype.removeThetaAxis = removeThetaAxis;
@@ -27,6 +27,7 @@ import {
27
27
  } from "./policy.js";
28
28
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
29
29
  "../../../materialization/graphicHierarchy.js";
30
+ import { resolveTextBounds } from "../../../core/textMetrics.js";
30
31
 
31
32
  const OPTIONS = Object.freeze([
32
33
  "scale", "position", "count", "values", "offset", "format", "color",
@@ -122,14 +123,18 @@ function resolve(program, channel, config) {
122
123
  })
123
124
  };
124
125
  const canvas = findCanvasGraphic(program)?.properties;
125
- const maximumTextWidth = Math.max(
126
- 0,
127
- ...text.map(value => [...value].length * config.fontSize * 0.6)
128
- );
126
+ const labelBounds = text.map((value, index) => resolveTextBounds({
127
+ x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
128
+ y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
129
+ text: value,
130
+ fontSize: config.fontSize,
131
+ textAlign: resolved.textAlign,
132
+ textBaseline: resolved.textBaseline
133
+ }));
129
134
  const fits = config.position === "top"
130
- ? resolved.y - config.fontSize >= 0
135
+ ? labelBounds.every(item => item.top >= 0)
131
136
  : config.position === "right"
132
- ? resolved.x + maximumTextWidth <= canvas?.width
137
+ ? labelBounds.every(item => item.right <= canvas?.width)
133
138
  : true;
134
139
  if (!canvas || !fits) {
135
140
  throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
@@ -0,0 +1,86 @@
1
+ import { validateUserId } from "../../../../core/identifiers.js";
2
+ import {
3
+ isTransformedScaleType,
4
+ mapContinuousScaleValues,
5
+ mapOrdinalPositionValues,
6
+ transformedTicks
7
+ } from "../../../../grammar/scales/index.js";
8
+ import { niceTicks } from "../../../../grammar/ticks.js";
9
+ import { resolveGraphicBounds } from "../../../../layout/canvas.js";
10
+ import { findCoordinate } from "../../../../selectors/coordinates.js";
11
+ import { findLayer } from "../../../../selectors/layers.js";
12
+
13
+ export function requireParallelAxisLayer(program, target) {
14
+ const layer = findLayer(program, target);
15
+ const coordinate = layer === undefined
16
+ ? undefined
17
+ : findCoordinate(program, layer.coordinate);
18
+ if (
19
+ layer?.mark?.type !== "line" ||
20
+ coordinate?.type !== "parallel" ||
21
+ layer.encoding?.parallel?.dimensions?.length < 2
22
+ ) {
23
+ throw new Error(`Parallel axes require an encoded Parallel line "${target}".`);
24
+ }
25
+ return { coordinate, dimensions: layer.encoding.parallel.dimensions, layer };
26
+ }
27
+
28
+ export function resolveParallelAxisTarget(program, requested) {
29
+ if (requested !== undefined) {
30
+ const target = validateUserId(requested, "Parallel axes target");
31
+ requireParallelAxisLayer(program, target);
32
+ return target;
33
+ }
34
+ const candidates = program.semanticSpec.layers.filter(layer => {
35
+ const coordinate = findCoordinate(program, layer.coordinate);
36
+ return layer.encoding?.parallel !== undefined && coordinate?.type === "parallel";
37
+ });
38
+ if (candidates.length !== 1) {
39
+ throw new Error(
40
+ "Parallel axes require target when one Parallel layer cannot be inferred."
41
+ );
42
+ }
43
+ return candidates[0].id;
44
+ }
45
+
46
+ function ticksForScale(scale) {
47
+ if (["ordinal", "band", "point"].includes(scale.type)) return scale.domain;
48
+ if (isTransformedScaleType(scale.type)) {
49
+ return transformedTicks(scale.type, scale.domain, 5, {
50
+ ...(scale.base === undefined ? {} : { base: scale.base }),
51
+ ...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
52
+ ...(scale.constant === undefined ? {} : { constant: scale.constant })
53
+ });
54
+ }
55
+ return niceTicks(scale.domain, 5);
56
+ }
57
+
58
+ function formatValue(value) {
59
+ if (Number.isFinite(value) && Math.abs(value) >= 1000 && value % 1000 === 0) {
60
+ return `${value / 1000}k`;
61
+ }
62
+ return String(value);
63
+ }
64
+
65
+ export function resolveParallelAxisValues(program, dimensions) {
66
+ const bounds = resolveGraphicBounds(program);
67
+ const step = bounds.width / (dimensions.length - 1);
68
+ const axes = dimensions.map((dimension, index) => {
69
+ const scale = program.resolvedScales[dimension.scale];
70
+ if (scale === undefined) {
71
+ throw new Error(`Parallel axis requires resolved scale "${dimension.scale}".`);
72
+ }
73
+ const values = ticksForScale(scale);
74
+ const y = ["ordinal", "band", "point"].includes(scale.type)
75
+ ? mapOrdinalPositionValues(values, scale)
76
+ : mapContinuousScaleValues(values, scale);
77
+ return {
78
+ ...dimension,
79
+ x: bounds.x + step * index,
80
+ values,
81
+ y,
82
+ labels: values.map(formatValue)
83
+ };
84
+ });
85
+ return { axes, bounds };
86
+ }