ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -0,0 +1 @@
1
+ export { registerFacetActions } from "./actions.js";
@@ -0,0 +1,69 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { hasDataset } from "../../selectors/datasets.js";
6
+ import { requireLayer } from "../../selectors/layers.js";
7
+
8
+ const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
9
+ const REBIND_OPTIONS = Object.freeze(["id", "data"]);
10
+ const MATERIALIZERS = Object.freeze({
11
+ filter: "materializeFilteredData",
12
+ regression: "materializeRegressionData",
13
+ density: "materializeDensityData",
14
+ interval: "materializeIntervalData",
15
+ boxSummary: "materializeBoxSummaryData",
16
+ boxOutlier: "materializeBoxOutlierData"
17
+ });
18
+
19
+ function requestedTransform(transform) {
20
+ if (!isPlainObject(transform)) {
21
+ throw new TypeError("replayDerivedData transform must be a plain object.");
22
+ }
23
+ const materialize = MATERIALIZERS[transform.type];
24
+ if (materialize === undefined) {
25
+ throw new Error(
26
+ `replayDerivedData does not support transform "${transform.type ?? "unknown"}".`
27
+ );
28
+ }
29
+ if (transform.type !== "density") return { transform, materialize };
30
+ const { resolved: _resolved, ...requested } = transform;
31
+ return { transform: requested, materialize };
32
+ }
33
+
34
+ export const replayDerivedData = action(
35
+ {
36
+ op: "replayDerivedData",
37
+ description: "Replay one stored derived-data transform for a facet cell."
38
+ },
39
+ function (args = {}) {
40
+ validateKeys(args, REPLAY_OPTIONS, "replayDerivedData");
41
+ const id = validateUserId(args.id, "Facet replay dataset id");
42
+ const source = validateUserId(args.source, "Facet replay source id");
43
+ const resolved = requestedTransform(args.transform);
44
+ return this
45
+ .createDerivedData({ id, source, transform: [resolved.transform] })
46
+ [resolved.materialize]({ id });
47
+ }
48
+ );
49
+
50
+ export const rebindLayerData = action(
51
+ {
52
+ op: "rebindLayerData",
53
+ description: "Rebind one facet-cell layer to its replayed dataset."
54
+ },
55
+ function (args = {}) {
56
+ validateKeys(args, REBIND_OPTIONS, "rebindLayerData");
57
+ const id = validateUserId(args.id, "Facet layer id");
58
+ const data = validateUserId(args.data, "Facet layer dataset id");
59
+ requireLayer(this, id);
60
+ if (!hasDataset(this, data)) {
61
+ throw new Error(`Facet layer dataset "${data}" does not exist.`);
62
+ }
63
+ return this.editSemantic({
64
+ property: `layer[${id}].data`,
65
+ value: data
66
+ });
67
+ }
68
+ );
69
+
@@ -0,0 +1,113 @@
1
+ import {
2
+ BAR_ORIENTATIONS,
3
+ resolveBarOrientation
4
+ } from "../../grammar/bars/policy.js";
5
+ import {
6
+ CARTESIAN_POSITION_CHANNELS,
7
+ POLAR_POSITION_CHANNELS
8
+ } from "../../core/vocabulary.js";
9
+ import { findSemanticScale } from "../../selectors/scales.js";
10
+
11
+ function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
12
+ return program.semanticSpec.layers.some(layer => {
13
+ const encoding = layer.encoding?.[channel];
14
+ if (!scaled && !grid) return encoding !== undefined;
15
+ if (encoding?.scale === undefined) return false;
16
+ if (!grid || POLAR_POSITION_CHANNELS.includes(channel)) return true;
17
+ const type = findSemanticScale(program, encoding.scale)?.type;
18
+ return type !== undefined && !["ordinal", "band", "point"].includes(type);
19
+ });
20
+ }
21
+
22
+ function positionalApplicability(program, channels) {
23
+ return Object.freeze(Object.fromEntries(
24
+ channels.map(channel => [
25
+ channel,
26
+ Object.freeze({
27
+ axis: hasEncoding(program, channel),
28
+ grid: hasEncoding(program, channel, { grid: true })
29
+ })
30
+ ])
31
+ ));
32
+ }
33
+
34
+ export function hasInferableLegend(program) {
35
+ 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)
52
+ );
53
+ }
54
+
55
+ export function resolveGuideApplicability(program) {
56
+ const cartesian = positionalApplicability(
57
+ program,
58
+ CARTESIAN_POSITION_CHANNELS
59
+ );
60
+ const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
61
+ return Object.freeze({
62
+ axes: Object.freeze({
63
+ cartesian: CARTESIAN_POSITION_CHANNELS.some(
64
+ channel => cartesian[channel].axis
65
+ ),
66
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis)
67
+ }),
68
+ grid: Object.freeze({
69
+ cartesian: CARTESIAN_POSITION_CHANNELS.some(
70
+ channel => cartesian[channel].grid
71
+ ),
72
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].grid),
73
+ directions: Object.freeze({
74
+ horizontal: cartesian.y.grid,
75
+ vertical: cartesian.x.grid,
76
+ theta: polar.theta.grid,
77
+ radial: polar.radius.grid
78
+ })
79
+ }),
80
+ legend: hasInferableLegend(program)
81
+ });
82
+ }
83
+
84
+ export function resolveAutomaticGridOptions(program) {
85
+ const applicability = resolveGuideApplicability(program);
86
+ const directions = applicability.grid.directions;
87
+ if (applicability.grid.polar && !applicability.grid.cartesian) {
88
+ return Object.freeze({
89
+ ...(directions.theta ? { theta: {} } : {}),
90
+ ...(directions.radial ? { radial: {} } : {})
91
+ });
92
+ }
93
+
94
+ const barOrientations = program.semanticSpec.layers
95
+ .map(resolveBarOrientation)
96
+ .filter(Boolean);
97
+ const positionedLayers = program.semanticSpec.layers.filter(
98
+ layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
99
+ );
100
+ const horizontalQuantitative = positionedLayers.length > 0 &&
101
+ positionedLayers.every(layer =>
102
+ layer.encoding.x.fieldType === "quantitative" &&
103
+ ["nominal", "ordinal", "temporal"].includes(layer.encoding.y.fieldType)
104
+ );
105
+ if (
106
+ horizontalQuantitative ||
107
+ (barOrientations.length > 0 &&
108
+ barOrientations.every(value => value === BAR_ORIENTATIONS.horizontal))
109
+ ) {
110
+ return Object.freeze({ horizontal: false, vertical: {} });
111
+ }
112
+ return Object.freeze({ horizontal: {}, vertical: false });
113
+ }
@@ -1,10 +1,13 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import { isPlainObject } from "../../../core/immutable.js";
3
3
  import { validateUserId } from "../../../core/identifiers.js";
4
- import { validateKeys } from "../../../core/validation.js";
4
+ import { validateKeys, validateOptionObject } from "../../../core/validation.js";
5
5
  import { findCoordinate } from "../../../selectors/coordinates.js";
6
+ import { resolvePolarGuideResources } from "../polar/resolve.js";
6
7
 
7
- const TOP_OPTIONS = Object.freeze(["coordinate", "x", "y"]);
8
+ const TOP_OPTIONS = Object.freeze([
9
+ "coordinate", "x", "y", "theta", "radius"
10
+ ]);
8
11
  const COORDINATE_OPTIONS = Object.freeze(["id", "type"]);
9
12
  const COORDINATE_API_TYPES = new Set(["auto", "cartesian", "polar"]);
10
13
 
@@ -15,13 +18,11 @@ function validateAxisOption(value, channel) {
15
18
  }
16
19
 
17
20
  function validateArgs(args) {
18
- if (!isPlainObject(args)) {
19
- throw new TypeError("createAxes options must be a plain object.");
20
- }
21
-
22
- validateKeys(args, TOP_OPTIONS, "createAxes");
21
+ validateOptionObject(args, TOP_OPTIONS, "createAxes");
23
22
  validateAxisOption(args.x, "x");
24
23
  validateAxisOption(args.y, "y");
24
+ validateAxisOption(args.theta, "theta");
25
+ validateAxisOption(args.radius, "radius");
25
26
 
26
27
  const coordinate = args.coordinate ?? {};
27
28
  if (!isPlainObject(coordinate)) {
@@ -60,14 +61,10 @@ function inspectChannels(layers) {
60
61
  );
61
62
  }
62
63
 
63
- return { cartesianLayers, hasPolar: polarLayers.length > 0 };
64
+ return { cartesianLayers, polarLayers };
64
65
  }
65
66
 
66
- function resolveCoordinate(program, descriptor, cartesianLayers, hasPolar) {
67
- if (hasPolar && cartesianLayers.length === 0) {
68
- throw new Error("createAxes does not yet support Polar axes.");
69
- }
70
-
67
+ function resolveCoordinate(program, descriptor, cartesianLayers) {
71
68
  if (cartesianLayers.length === 0) {
72
69
  throw new Error("createAxes requires an x or y encoding.");
73
70
  }
@@ -124,6 +121,24 @@ function resolveCoordinate(program, descriptor, cartesianLayers, hasPolar) {
124
121
  return { id, layers };
125
122
  }
126
123
 
124
+ function resolvePolarAxisArgs(program, layers, channel, option, descriptor) {
125
+ const selected = option === false
126
+ ? false
127
+ : option !== undefined || hasChannel(layers, channel);
128
+ if (!selected) return undefined;
129
+ const args = option ?? {};
130
+ const kind = channel === "theta" ? "theta" : "radius";
131
+ const resource = resolvePolarGuideResources(program, kind, {
132
+ ...args,
133
+ ...(descriptor.id === undefined ? {} : { coordinate: descriptor.id })
134
+ }, "createAxes");
135
+ return {
136
+ ...args,
137
+ scale: resource.scale,
138
+ coordinate: resource.coordinate
139
+ };
140
+ }
141
+
127
142
  function hasChannel(layers, channel) {
128
143
  return layers.some(layer => layer.encoding?.[channel] !== undefined);
129
144
  }
@@ -170,14 +185,53 @@ const createAxes = action(
170
185
  },
171
186
  function (args = {}) {
172
187
  const coordinateDescriptor = validateArgs(args);
173
- const { cartesianLayers, hasPolar } = inspectChannels(
188
+ const { cartesianLayers, polarLayers } = inspectChannels(
174
189
  this.semanticSpec.layers
175
190
  );
191
+ const requestedCoordinateType = coordinateDescriptor.id === undefined
192
+ ? undefined
193
+ : findCoordinate(this, coordinateDescriptor.id)?.type;
194
+ const explicitlyPolar = coordinateDescriptor.type === "polar" ||
195
+ requestedCoordinateType === "polar" ||
196
+ args.theta !== undefined || args.radius !== undefined;
197
+ if (polarLayers.length > 0 &&
198
+ (cartesianLayers.length === 0 || explicitlyPolar)) {
199
+ if (coordinateDescriptor.type === "cartesian") {
200
+ throw new Error("createAxes coordinate type conflicts with Polar encodings.");
201
+ }
202
+ if (args.x !== undefined || args.y !== undefined) {
203
+ throw new Error("createAxes x/y options require Cartesian encodings.");
204
+ }
205
+ const theta = resolvePolarAxisArgs(
206
+ this,
207
+ polarLayers,
208
+ "theta",
209
+ args.theta,
210
+ coordinateDescriptor
211
+ );
212
+ const radius = resolvePolarAxisArgs(
213
+ this,
214
+ polarLayers,
215
+ "radius",
216
+ args.radius,
217
+ coordinateDescriptor
218
+ );
219
+ if (theta === undefined && radius === undefined) {
220
+ throw new Error("createAxes requires at least one selected axis.");
221
+ }
222
+ let polarProgram = this;
223
+ if (theta !== undefined) {
224
+ polarProgram = polarProgram.createThetaAxis(theta);
225
+ }
226
+ if (radius !== undefined) {
227
+ polarProgram = polarProgram.createRadialAxis(radius);
228
+ }
229
+ return polarProgram;
230
+ }
176
231
  const coordinate = resolveCoordinate(
177
232
  this,
178
233
  coordinateDescriptor,
179
- cartesianLayers,
180
- hasPolar
234
+ cartesianLayers
181
235
  );
182
236
  const x = resolveAxisArgs(coordinate.layers, "x", args.x);
183
237
  const y = resolveAxisArgs(coordinate.layers, "y", args.y);
@@ -0,0 +1,126 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { isPlainObject } from "../../../core/immutable.js";
3
+ import { validateKeys, validateOptionObject } from "../../../core/validation.js";
4
+
5
+ const OPTIONS = Object.freeze([
6
+ "position", "line", "ticks", "labels", "ticksAndLabels", "title"
7
+ ]);
8
+ const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
9
+ const TICK_OPTIONS = Object.freeze([
10
+ "count", "values", "length", "color", "lineWidth"
11
+ ]);
12
+ const LABEL_OPTIONS = Object.freeze([
13
+ "count", "values", "offset", "format", "color", "fontSize",
14
+ "fontFamily", "fontWeight"
15
+ ]);
16
+ const GROUP_OPTIONS = Object.freeze(["count", "values", "ticks", "labels"]);
17
+ const TITLE_OPTIONS = Object.freeze([
18
+ "text", "at", "offset", "rotation", "color", "fontSize",
19
+ "fontFamily", "fontWeight"
20
+ ]);
21
+
22
+ function validateNested(args, key, options, operation) {
23
+ if (!Object.hasOwn(args, key)) return;
24
+ if (!isPlainObject(args[key])) {
25
+ throw new TypeError(`${operation}.${key} must be a plain object.`);
26
+ }
27
+ validateKeys(args[key], options, `${operation}.${key}`);
28
+ }
29
+
30
+ function validateArgs(args, operation) {
31
+ validateOptionObject(args, OPTIONS, operation, {
32
+ allowEmpty: false,
33
+ emptyMessage: `${operation} requires at least one axis change.`,
34
+ emptyError: Error
35
+ });
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);
41
+ if (args.ticksAndLabels !== undefined && (
42
+ args.ticks !== undefined || args.labels !== undefined
43
+ )) {
44
+ throw new Error(
45
+ `${operation} cannot combine ticksAndLabels with ticks or labels.`
46
+ );
47
+ }
48
+ if (args.ticksAndLabels?.ticks !== undefined) {
49
+ validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth"], `${operation}.ticksAndLabels`);
50
+ }
51
+ if (args.ticksAndLabels?.labels !== undefined) {
52
+ validateNested(args.ticksAndLabels, "labels", [
53
+ "offset", "format", "color", "fontSize", "fontFamily", "fontWeight"
54
+ ], `${operation}.ticksAndLabels`);
55
+ }
56
+ }
57
+
58
+ function names(channel) {
59
+ const prefix = channel === "x" ? "X" : "Y";
60
+ return {
61
+ operation: `edit${prefix}Axis`,
62
+ line: `edit${prefix}AxisLine`,
63
+ ticks: `edit${prefix}AxisTicks`,
64
+ labels: `edit${prefix}AxisLabels`,
65
+ group: `edit${prefix}AxisTicksAndLabels`,
66
+ title: `edit${prefix}AxisTitle`
67
+ };
68
+ }
69
+
70
+ function makeEditAxis(channel) {
71
+ const operation = names(channel);
72
+ return action(
73
+ {
74
+ op: operation.operation,
75
+ description: `Edit selected existing ${channel}-axis components.`
76
+ },
77
+ function (args = {}) {
78
+ 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;
116
+ }
117
+ );
118
+ }
119
+
120
+ const editXAxis = makeEditAxis("x");
121
+ const editYAxis = makeEditAxis("y");
122
+
123
+ export function registerCompleteAxisEditActions(ProgramClass) {
124
+ ProgramClass.prototype.editXAxis = editXAxis;
125
+ ProgramClass.prototype.editYAxis = editYAxis;
126
+ }
@@ -5,6 +5,13 @@ import { registerAxisTickGroupActions } from "./tickGroups.js";
5
5
  import { registerAxisTickActions } from "./ticks.js";
6
6
  import { registerAxisTitleActions } from "./titles.js";
7
7
  import { registerAxisActions } from "./axis.js";
8
+ import { registerCompleteAxisEditActions } from "./edit.js";
9
+ import {
10
+ removeRadialAxis,
11
+ removeThetaAxis,
12
+ removeXAxis,
13
+ removeYAxis
14
+ } from "./remove.js";
8
15
 
9
16
  export function registerGuideAxisActions(ProgramClass) {
10
17
  registerAxisLineActions(ProgramClass);
@@ -14,4 +21,9 @@ export function registerGuideAxisActions(ProgramClass) {
14
21
  registerAxisTitleActions(ProgramClass);
15
22
  registerAxisActions(ProgramClass);
16
23
  registerAxisCollectionActions(ProgramClass);
24
+ registerCompleteAxisEditActions(ProgramClass);
25
+ ProgramClass.prototype.removeXAxis = removeXAxis;
26
+ ProgramClass.prototype.removeYAxis = removeYAxis;
27
+ ProgramClass.prototype.removeThetaAxis = removeThetaAxis;
28
+ ProgramClass.prototype.removeRadialAxis = removeRadialAxis;
17
29
  }
@@ -2,6 +2,7 @@ import { action } from "../../../core/action.js";
2
2
  import { validateUserId } from "../../../core/identifiers.js";
3
3
  import {
4
4
  sameOrderedValues,
5
+ validateOptionObject,
5
6
  validateNonEmptyString,
6
7
  validateNonNegativeFinite,
7
8
  validatePositiveFinite
@@ -13,7 +14,7 @@ import {
13
14
  formatTransformedTick,
14
15
  mapOrdinalPositionValues
15
16
  } from "../../../grammar/scales.js";
16
- import { formatTimeTick } from "../../../grammar/ticks.js";
17
+ import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
17
18
  import { valuesFromTickConfig } from "../tickValues.js";
18
19
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
19
20
  "../../../theme/defaults.js";
@@ -24,7 +25,7 @@ import {
24
25
  validateAxisFormat,
25
26
  validateAxisPosition
26
27
  } from "./policy.js";
27
- import { resolvePlotGraphicPlacement } from
28
+ import { findCanvasGraphic, resolvePlotGraphicPlacement } from
28
29
  "../../../materialization/graphicHierarchy.js";
29
30
 
30
31
  const OPTIONS = Object.freeze([
@@ -41,11 +42,11 @@ const DEFAULTS = Object.freeze({
41
42
  });
42
43
 
43
44
  function validateOptions(args, operation, create) {
44
- for (const key of Object.keys(args)) {
45
- if (!OPTIONS.includes(key) || (!create && key === "scale")) {
46
- throw new Error(`Unknown ${operation} option "${key}".`);
47
- }
48
- }
45
+ validateOptionObject(
46
+ args,
47
+ create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
48
+ operation
49
+ );
49
50
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
50
51
  throw new Error(`${operation} cannot use count and values together.`);
51
52
  }
@@ -97,16 +98,18 @@ function resolve(program, channel, config) {
97
98
  const positions = discrete
98
99
  ? mapOrdinalPositionValues(values, scale)
99
100
  : mapContinuousScaleValues(values, scale);
100
- const text = values.map(value => formatAxisValue(
101
- value,
102
- scale.type,
103
- config.format,
104
- item => scale.type === "time"
105
- ? formatTimeTick(item, scale.domain)
106
- : isTransformedScaleType(scale.type)
107
- ? formatTransformedTick(scale.type, item)
108
- : String(item)
109
- ));
101
+ const text = scale.type === "time" && config.format === "auto"
102
+ ? formatTimeTicks(values, scale.domain)
103
+ : values.map(value => formatAxisValue(
104
+ value,
105
+ scale.type,
106
+ config.format,
107
+ item => scale.type === "time"
108
+ ? formatTimeTick(item, scale.domain)
109
+ : isTransformedScaleType(scale.type)
110
+ ? formatTransformedTick(scale.type, item)
111
+ : String(item)
112
+ ));
110
113
  const resolved = {
111
114
  values,
112
115
  text,
@@ -118,7 +121,7 @@ function resolve(program, channel, config) {
118
121
  offset: config.offset
119
122
  })
120
123
  };
121
- const canvas = program.graphicSpec.objects.canvas?.properties;
124
+ const canvas = findCanvasGraphic(program)?.properties;
122
125
  const maximumTextWidth = Math.max(
123
126
  0,
124
127
  ...text.map(value => [...value].length * config.fontSize * 0.6)
@@ -0,0 +1,60 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateKeys } from "../../../core/validation.js";
3
+ import { axisGraphicIds } from
4
+ "../../../materialization/guides/resources.js";
5
+
6
+ const OPTIONS = Object.freeze(["coordinate", "scale"]);
7
+
8
+ function makeRemoveAxis(channel) {
9
+ const prefix = channel === "x"
10
+ ? "X"
11
+ : channel === "y"
12
+ ? "Y"
13
+ : channel === "theta"
14
+ ? "Theta"
15
+ : "Radial";
16
+ const operation = `remove${prefix}Axis`;
17
+ const graphicIds = axisGraphicIds(channel);
18
+ return action(
19
+ {
20
+ op: operation,
21
+ description: `Remove the complete ${channel}-axis resource.`
22
+ },
23
+ function (args = {}) {
24
+ validateKeys(args, OPTIONS, operation);
25
+ const semantic = this.semanticSpec.guides.axis?.[channel];
26
+ const config = this.guideConfigs.axis?.[channel];
27
+ const hasGraphic = graphicIds.some(
28
+ id => this.graphicSpec.objects[id] !== undefined
29
+ );
30
+ if (semantic === undefined && config === undefined && !hasGraphic) {
31
+ throw new Error(`${operation} requires an existing ${channel}-axis.`);
32
+ }
33
+ if (args.scale !== undefined && semantic?.scale !== args.scale) {
34
+ throw new Error(`${operation} found no axis for scale "${args.scale}".`);
35
+ }
36
+ if (args.coordinate !== undefined && semantic?.coordinate !== args.coordinate) {
37
+ throw new Error(
38
+ `${operation} found no axis for coordinate "${args.coordinate}".`
39
+ );
40
+ }
41
+ let next = semantic === undefined
42
+ ? this
43
+ : this.editSemantic({
44
+ property: `guide.axis.${channel}`,
45
+ remove: true
46
+ });
47
+ for (const id of graphicIds) {
48
+ if (next.graphicSpec.objects[id] !== undefined) {
49
+ next = next.editGraphics({ target: id, remove: true });
50
+ }
51
+ }
52
+ return next._withoutMaterializationConfig(["guides", "axis", channel]);
53
+ }
54
+ );
55
+ }
56
+
57
+ export const removeXAxis = makeRemoveAxis("x");
58
+ export const removeYAxis = makeRemoveAxis("y");
59
+ export const removeThetaAxis = makeRemoveAxis("theta");
60
+ export const removeRadialAxis = makeRemoveAxis("radius");
@@ -1,6 +1,6 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import { isPlainObject } from "../../../core/immutable.js";
3
- import { validateKeys } from "../../../core/validation.js";
3
+ import { validateKeys, validateOptionObject } from "../../../core/validation.js";
4
4
 
5
5
  const CREATE_OPTIONS = Object.freeze([
6
6
  "scale",
@@ -38,7 +38,12 @@ function validateNested(value, supported, label) {
38
38
  }
39
39
 
40
40
  function validateArgs(args, operation, create) {
41
- validateKeys(args, create ? CREATE_OPTIONS : EDIT_OPTIONS, operation);
41
+ validateOptionObject(
42
+ args,
43
+ create ? CREATE_OPTIONS : EDIT_OPTIONS,
44
+ operation,
45
+ { allowEmpty: create }
46
+ );
42
47
 
43
48
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
44
49
  throw new Error(`${operation} cannot use count and values together.`);
@@ -52,9 +57,6 @@ function validateArgs(args, operation, create) {
52
57
  validateNested(args.labels, LABEL_OPTIONS, `${operation}.labels`);
53
58
  }
54
59
 
55
- if (!create && Object.keys(args).length === 0) {
56
- throw new TypeError(`${operation} requires at least one option.`);
57
- }
58
60
  }
59
61
 
60
62
  function select(source, keys) {
@@ -2,7 +2,8 @@ import { action } from "../../../core/action.js";
2
2
  import { validateUserId } from "../../../core/identifiers.js";
3
3
  import {
4
4
  validateNonEmptyString,
5
- validateNonNegativeFinite
5
+ validateNonNegativeFinite,
6
+ validateOptionObject
6
7
  } from "../../../core/validation.js";
7
8
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
8
9
  import {
@@ -21,7 +22,7 @@ import {
21
22
  resolveAxisTickGeometry,
22
23
  validateAxisPosition
23
24
  } from "./policy.js";
24
- import { resolvePlotGraphicPlacement } from
25
+ import { findCanvasGraphic, resolvePlotGraphicPlacement } from
25
26
  "../../../materialization/graphicHierarchy.js";
26
27
 
27
28
  const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth"]);
@@ -33,7 +34,11 @@ const DEFAULTS = Object.freeze({
33
34
  });
34
35
 
35
36
  function validateOptions(args, operation, create) {
36
- for (const key of Object.keys(args)) if (!OPTIONS.includes(key) || (!create && key === "scale")) throw new Error(`Unknown ${operation} option "${key}".`);
37
+ validateOptionObject(
38
+ args,
39
+ create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
40
+ operation
41
+ );
37
42
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
38
43
  }
39
44
 
@@ -84,7 +89,7 @@ function geometry(program, channel, config) {
84
89
  length: config.length
85
90
  })
86
91
  };
87
- const canvas = program.graphicSpec.objects.canvas?.properties;
92
+ const canvas = findCanvasGraphic(program)?.properties;
88
93
  const coordinates = channel === "x"
89
94
  ? [...resolved.x1, ...resolved.x2, resolved.y1, resolved.y2]
90
95
  : [resolved.x1, resolved.x2, ...resolved.y1, ...resolved.y2];