ggaction 0.0.5 → 0.0.7

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 (69) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +77 -14
  3. package/package.json +19 -2
  4. package/src/BasicChartProgram.js +10 -0
  5. package/src/actions/basic.js +21 -0
  6. package/src/actions/boxPlots/edit.js +455 -3
  7. package/src/actions/boxPlots/options.js +3 -3
  8. package/src/actions/canvas/actions.js +42 -0
  9. package/src/actions/canvas/index.js +5 -1
  10. package/src/actions/charts/basic.js +13 -0
  11. package/src/actions/composition/actions.js +13 -3
  12. package/src/actions/data/basic.js +11 -0
  13. package/src/actions/data/basicBin2d.js +43 -0
  14. package/src/actions/data/bin2d.js +138 -40
  15. package/src/actions/data/bin2dMaterialize.js +29 -0
  16. package/src/actions/data/index.js +6 -1
  17. package/src/actions/data/intervalEdit.js +76 -0
  18. package/src/actions/distributions/revision.js +326 -0
  19. package/src/actions/encodings/appearance.js +41 -3
  20. package/src/actions/encodings/basic.js +21 -0
  21. package/src/actions/encodings/density.js +215 -80
  22. package/src/actions/encodings/index.js +2 -0
  23. package/src/actions/encodings/position/index.js +5 -1
  24. package/src/actions/encodings/ranged.js +6 -2
  25. package/src/actions/encodings/remove.js +268 -0
  26. package/src/actions/errorBands/edit.js +121 -13
  27. package/src/actions/errorBars/edit.js +45 -8
  28. package/src/actions/errorBars/options.js +7 -1
  29. package/src/actions/facets/actions.js +136 -1
  30. package/src/actions/gradientPlots/edit.js +352 -4
  31. package/src/actions/guides/axes/edit.js +129 -47
  32. package/src/actions/guides/axes/index.js +10 -2
  33. package/src/actions/guides/basic.js +19 -0
  34. package/src/actions/guides/grids/grid.js +7 -3
  35. package/src/actions/guides/grids/index.js +1 -1
  36. package/src/actions/guides/legends/categorical/actions.js +19 -8
  37. package/src/actions/guides/legends/categorical/index.js +6 -2
  38. package/src/actions/guides/legends/continuous/common.js +3 -3
  39. package/src/actions/guides/legends/continuous/index.js +6 -2
  40. package/src/actions/guides/legends/edit.js +61 -3
  41. package/src/actions/guides/legends/remove.js +124 -23
  42. package/src/actions/guides/legends/strokeWidth.js +42 -14
  43. package/src/actions/marks/arc/actions.js +46 -14
  44. package/src/actions/marks/bar/index.js +5 -1
  45. package/src/actions/marks/basic.js +11 -0
  46. package/src/actions/marks/line/actions.js +5 -1
  47. package/src/actions/marks/line/index.js +4 -1
  48. package/src/actions/marks/point/edit.js +15 -3
  49. package/src/actions/marks/point/index.js +5 -1
  50. package/src/actions/marks/point/materialize.js +9 -3
  51. package/src/actions/marks/rect/actions.js +5 -1
  52. package/src/actions/marks/rect/index.js +4 -1
  53. package/src/actions/marks/rule/actions.js +4 -1
  54. package/src/actions/regression/edit.js +235 -74
  55. package/src/actions/scales/index.js +5 -1
  56. package/src/actions/selection/actions.js +122 -2
  57. package/src/actions/selection/index.js +6 -0
  58. package/src/basic.js +2 -0
  59. package/src/core/vocabulary.js +7 -0
  60. package/src/renderers/canvas/index.js +96 -60
  61. package/src/renderers/canvas/text.js +3 -14
  62. package/src/renderers/pdf.js +112 -0
  63. package/src/renderers/svg.js +559 -0
  64. package/src/renderers/text.js +11 -0
  65. package/types/basic.d.ts +107 -0
  66. package/types/index.d.ts +1 -0
  67. package/types/pdf.d.ts +26 -0
  68. package/types/program.d.ts +71 -7
  69. package/types/svg.d.ts +11 -0
@@ -3,7 +3,8 @@ import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
4
  import {
5
5
  deriveBin2DRows,
6
- normalizeBin2DTransform
6
+ normalizeBin2DTransform,
7
+ requestedBin2DTransform
7
8
  } from "../../grammar/bin2d.js";
8
9
  import { applyLayerDataRematerialization } from
9
10
  "../../materialization/dependencies.js";
@@ -13,16 +14,48 @@ import {
13
14
  findDataset,
14
15
  findDatasetConsumer
15
16
  } from "../../selectors/datasets.js";
16
- import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
17
+ export { materializeBin2DData } from "./bin2dMaterialize.js";
17
18
 
18
19
  const OPTIONS = Object.freeze([
19
20
  "id", "source", "x", "y", "bins", "extent", "includeEmpty", "members", "as"
20
21
  ]);
22
+ const EDIT_OPTIONS = Object.freeze([
23
+ "target", "source", "x", "y", "bins", "extent", "includeEmpty", "members", "as"
24
+ ]);
25
+ const EDITABLE = Object.freeze(EDIT_OPTIONS.slice(1));
26
+ const OUTPUT_FIELDS = Object.freeze([
27
+ "x0", "x1", "y0", "y1", "count"
28
+ ]);
21
29
 
22
30
  function ownerConfig(program, id) {
23
31
  return program.materializationConfigs.data?.bin2d?.[id];
24
32
  }
25
33
 
34
+ function ownerEntries(program) {
35
+ return Object.entries(program.materializationConfigs.data?.bin2d ?? {});
36
+ }
37
+
38
+ function resolveBin2DOwner(program, requested) {
39
+ if (requested !== undefined) {
40
+ const owner = validateUserId(requested, "2D bin owner id");
41
+ if (ownerConfig(program, owner) === undefined) {
42
+ throw new Error(`Unknown 2D bin owner "${owner}".`);
43
+ }
44
+ return owner;
45
+ }
46
+ const owners = ownerEntries(program);
47
+ const current = owners.filter(([, config]) =>
48
+ config?.current === program.context.currentData
49
+ );
50
+ if (current.length === 1) return current[0][0];
51
+ if (current.length > 1) {
52
+ throw new Error("2D bin owner is ambiguous; provide target.");
53
+ }
54
+ if (owners.length === 1) return owners[0][0];
55
+ if (owners.length === 0) throw new Error("No 2D bin owner is available.");
56
+ throw new Error("2D bin owner is ambiguous; provide target.");
57
+ }
58
+
26
59
  function requireCurrentBin2D(program, id, config) {
27
60
  const current = findDataset(program, config.current);
28
61
  if (
@@ -59,30 +92,73 @@ function preflight(program, sourceId, transform) {
59
92
  deriveBin2DRows(source.values, transform);
60
93
  }
61
94
 
62
- export const materializeBin2DData = action(
63
- {
64
- op: "materializeBin2DData",
65
- description: "Materialize one immutable rectangular 2D-bin dataset."
66
- },
67
- function (args = {}) {
68
- validateKeys(args, MATERIALIZE_OPTIONS, "materializeBin2DData");
69
- const { id, source, transform } = requireDerivedDataset(
70
- this,
71
- args.id,
72
- "bin2d"
95
+ function requireCompleteEditOutputFields(value, members) {
96
+ const required = [...OUTPUT_FIELDS, ...(members ? ["members"] : [])];
97
+ const missing = required.find(field => !Object.hasOwn(value, field));
98
+ if (missing !== undefined) {
99
+ throw new Error(
100
+ `editBin2DData as requires the complete "${missing}" output field.`
73
101
  );
74
- const result = deriveBin2DRows(source.values, transform);
75
- return this
76
- .editSemantic({
77
- property: `dataset[${id}].transform`,
78
- value: [{ ...transform, resolved: result.resolved }]
79
- })
80
- .editSemantic({
81
- property: `dataset[${id}].values`,
82
- value: result.values
83
- });
84
102
  }
85
- );
103
+ }
104
+
105
+ function editedTransform(owner, previous, args) {
106
+ const prior = requestedBin2DTransform(previous.transform[0]);
107
+ const option = property => Object.hasOwn(args, property)
108
+ ? args[property]
109
+ : prior[property];
110
+ const members = option("members");
111
+ let as = option("as");
112
+ if (!Object.hasOwn(args, "as")) {
113
+ as = { ...as };
114
+ if (members && as.members === undefined) {
115
+ as.members = `__${owner}_members`;
116
+ } else if (!members) {
117
+ delete as.members;
118
+ }
119
+ }
120
+ const transform = normalizeBin2DTransform({
121
+ id: owner,
122
+ x: option("x"),
123
+ y: option("y"),
124
+ bins: option("bins"),
125
+ extent: option("extent"),
126
+ includeEmpty: option("includeEmpty"),
127
+ members,
128
+ as
129
+ });
130
+ if (Object.hasOwn(args, "as")) {
131
+ requireCompleteEditOutputFields(args.as, members);
132
+ }
133
+ return transform;
134
+ }
135
+
136
+ function sameRequestedTransform(left, right) {
137
+ return JSON.stringify(left) === JSON.stringify(right);
138
+ }
139
+
140
+ function applyBin2DRevision(program, { owner, previous, source, transform }) {
141
+ rejectDerivedConsumers(program, previous.id);
142
+ const consumers = directLayerConsumers(program, previous.id);
143
+ const revision = planDerivedDataRevision(program, {
144
+ owner,
145
+ role: "Bin2DData",
146
+ previous: previous.id,
147
+ consumers
148
+ });
149
+ let next = program
150
+ .createDerivedData({ id: revision.id, source, transform: [transform] })
151
+ .materializeBin2DData({ id: revision.id });
152
+ for (const rebind of revision.rebinds) {
153
+ next = next.rebindLayerData(rebind);
154
+ next = applyLayerDataRematerialization(next, rebind.id);
155
+ }
156
+ next = next.releaseDerivedData(revision.release);
157
+ return next._withMaterializationConfig(
158
+ ["data", "bin2d", owner],
159
+ { current: revision.id }
160
+ );
161
+ }
86
162
 
87
163
  export const createBin2DData = action(
88
164
  {
@@ -113,25 +189,47 @@ export const createBin2DData = action(
113
189
  );
114
190
  }
115
191
 
116
- rejectDerivedConsumers(this, previous.id);
117
- const consumers = directLayerConsumers(this, previous.id);
118
- const revision = planDerivedDataRevision(this, {
192
+ return applyBin2DRevision(this, {
119
193
  owner,
120
- role: "Bin2DData",
121
- previous: previous.id,
122
- consumers
194
+ previous,
195
+ source,
196
+ transform
123
197
  });
124
- let next = this
125
- .createDerivedData({ id: revision.id, source, transform: [transform] })
126
- .materializeBin2DData({ id: revision.id });
127
- for (const rebind of revision.rebinds) {
128
- next = next.rebindLayerData(rebind);
129
- next = applyLayerDataRematerialization(next, rebind.id);
198
+ }
199
+ );
200
+
201
+ export const editBin2DData = action(
202
+ {
203
+ op: "editBin2DData",
204
+ description: "Partially revise one logical rectangular 2D-bin owner."
205
+ },
206
+ function (args = {}) {
207
+ validateKeys(args, EDIT_OPTIONS, "editBin2DData");
208
+ if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
209
+ throw new Error("editBin2DData requires at least one transform or source option.");
130
210
  }
131
- next = next.releaseDerivedData(revision.release);
132
- return next._withMaterializationConfig(
133
- ["data", "bin2d", owner],
134
- { current: revision.id }
211
+ const owner = resolveBin2DOwner(this, args.target);
212
+ const previous = requireCurrentBin2D(this, owner, ownerConfig(this, owner));
213
+ const source = validateUserId(
214
+ args.source ?? previous.source,
215
+ "2D bin source dataset id"
135
216
  );
217
+ const transform = editedTransform(owner, previous, args);
218
+ if (
219
+ source === previous.source &&
220
+ sameRequestedTransform(
221
+ transform,
222
+ requestedBin2DTransform(previous.transform[0])
223
+ )
224
+ ) {
225
+ throw new Error("editBin2DData requires an actual transform or source change.");
226
+ }
227
+ preflight(this, source, transform);
228
+ const revision = { owner, previous, source, transform };
229
+
230
+ // Execute one speculative immutable branch so every consumer plan is known
231
+ // to succeed before the returned action trace records its first child.
232
+ applyBin2DRevision(this, revision);
233
+ return applyBin2DRevision(this, revision);
136
234
  }
137
235
  );
@@ -0,0 +1,29 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { deriveBin2DRows } from "../../grammar/bin2d.js";
4
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
5
+
6
+ export const materializeBin2DData = action(
7
+ {
8
+ op: "materializeBin2DData",
9
+ description: "Materialize one immutable rectangular 2D-bin dataset."
10
+ },
11
+ function (args = {}) {
12
+ validateKeys(args, MATERIALIZE_OPTIONS, "materializeBin2DData");
13
+ const { id, source, transform } = requireDerivedDataset(
14
+ this,
15
+ args.id,
16
+ "bin2d"
17
+ );
18
+ const result = deriveBin2DRows(source.values, transform);
19
+ return this
20
+ .editSemantic({
21
+ property: `dataset[${id}].transform`,
22
+ value: [{ ...transform, resolved: result.resolved }]
23
+ })
24
+ .editSemantic({
25
+ property: `dataset[${id}].values`,
26
+ value: result.values
27
+ });
28
+ }
29
+ );
@@ -23,7 +23,11 @@ import {
23
23
  import { createIntervalData, materializeIntervalData } from "./interval.js";
24
24
  import { createHorizonData, materializeHorizonData } from "./horizon.js";
25
25
  import { createWindowData, materializeWindowData } from "./window.js";
26
- import { createBin2DData, materializeBin2DData } from "./bin2d.js";
26
+ import {
27
+ createBin2DData,
28
+ editBin2DData,
29
+ materializeBin2DData
30
+ } from "./bin2d.js";
27
31
  import { createBoxSummaryData, createBoxOutlierData, materializeBoxSummaryData, materializeBoxOutlierData } from "./box.js";
28
32
 
29
33
  export function registerDataActions(ProgramClass) {
@@ -51,6 +55,7 @@ export function registerDataActions(ProgramClass) {
51
55
  ProgramClass.prototype.createWindowData = createWindowData;
52
56
  ProgramClass.prototype.materializeWindowData = materializeWindowData;
53
57
  ProgramClass.prototype.createBin2DData = createBin2DData;
58
+ ProgramClass.prototype.editBin2DData = editBin2DData;
54
59
  ProgramClass.prototype.materializeBin2DData = materializeBin2DData;
55
60
  ProgramClass.prototype.createBoxSummaryData = createBoxSummaryData;
56
61
  ProgramClass.prototype.createBoxOutlierData = createBoxOutlierData;
@@ -0,0 +1,76 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { normalizeIntervalParameters } from "../../grammar/interval.js";
4
+ import { planDerivedDataRevision } from
5
+ "../../materialization/dataProvenance.js";
6
+ import { findDataset } from "../../selectors/datasets.js";
7
+
8
+ const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "level"]);
9
+
10
+ export function planIntervalEdit(program, {
11
+ owner,
12
+ data,
13
+ consumers,
14
+ statistics,
15
+ operation
16
+ }) {
17
+ if (!isPlainObject(statistics)) {
18
+ throw new TypeError(`${operation} statistics must be a plain object.`);
19
+ }
20
+ validateKeys(statistics, STATISTICS_OPTIONS, `${operation} statistics`);
21
+ if (!STATISTICS_OPTIONS.some(key => Object.hasOwn(statistics, key))) {
22
+ throw new Error(
23
+ `${operation} statistics requires center, extent, or level.`
24
+ );
25
+ }
26
+ const previous = findDataset(program, data);
27
+ const transform = previous?.transform?.length === 1
28
+ ? previous.transform[0]
29
+ : undefined;
30
+ if (transform?.type !== "interval") {
31
+ throw new Error(
32
+ `${operation} statistics requires a statistical interval owner; ` +
33
+ "explicit interval fields cannot be converted by edit."
34
+ );
35
+ }
36
+ const center = Object.hasOwn(statistics, "center")
37
+ ? statistics.center
38
+ : transform.center;
39
+ const extent = Object.hasOwn(statistics, "extent")
40
+ ? statistics.extent
41
+ : transform.extent;
42
+ const raw = { center, extent };
43
+ if (Object.hasOwn(statistics, "level")) {
44
+ raw.level = statistics.level;
45
+ } else if (extent === "ci" && transform.extent === "ci") {
46
+ raw.level = transform.level;
47
+ }
48
+ const parameters = normalizeIntervalParameters(raw);
49
+ const current = {
50
+ center: transform.center,
51
+ extent: transform.extent,
52
+ ...(transform.level === undefined ? {} : { level: transform.level })
53
+ };
54
+ const changed = JSON.stringify(parameters) !== JSON.stringify(current);
55
+ if (!changed) return { changed, parameters };
56
+
57
+ const revision = planDerivedDataRevision(program, {
58
+ owner,
59
+ role: "IntervalData",
60
+ previous: previous.id,
61
+ consumers
62
+ });
63
+ return {
64
+ changed,
65
+ parameters,
66
+ revision,
67
+ dataArgs: {
68
+ id: revision.id,
69
+ source: previous.source,
70
+ field: transform.field,
71
+ groupBy: transform.groupBy,
72
+ ...parameters,
73
+ as: transform.as
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,326 @@
1
+ import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
2
+ import { findLayer } from "../../selectors/layers.js";
3
+ import { findSemanticScale } from "../../selectors/scales.js";
4
+ import { resolvePositionScaleDefinition } from "../scales/definitions.js";
5
+
6
+ const POSITION_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
7
+
8
+ export function resolveDistributionScalePlan(program, {
9
+ channel,
10
+ fieldType,
11
+ requested,
12
+ fallback,
13
+ defaults
14
+ }) {
15
+ const options = requested === undefined
16
+ ? { id: fallback }
17
+ : typeof requested === "string"
18
+ ? { id: requested }
19
+ : { ...requested, id: requested.id ?? fallback };
20
+ const existing = findSemanticScale(program, options.id);
21
+ return {
22
+ id: options.id,
23
+ definition: resolvePositionScaleDefinition(
24
+ program,
25
+ channel,
26
+ fieldType,
27
+ options,
28
+ defaults
29
+ ),
30
+ create: existing === undefined,
31
+ edit: existing !== undefined && typeof requested === "object" &&
32
+ Object.keys(requested).some(key => key !== "id")
33
+ ? options
34
+ : undefined
35
+ };
36
+ }
37
+
38
+ export function setCartesianPosition(program, id, channel, {
39
+ field,
40
+ fieldType,
41
+ scale,
42
+ title
43
+ }) {
44
+ let next = program
45
+ .editSemantic({
46
+ property: `layer[${id}].encoding.${channel}.field`,
47
+ value: field
48
+ })
49
+ .editSemantic({
50
+ property: `layer[${id}].encoding.${channel}.fieldType`,
51
+ value: fieldType
52
+ })
53
+ .editSemantic({
54
+ property: `layer[${id}].encoding.${channel}.scale`,
55
+ value: scale
56
+ });
57
+ if (title !== undefined) {
58
+ next = next.editSemantic({
59
+ property: `layer[${id}].encoding.${channel}.title`,
60
+ value: title
61
+ });
62
+ }
63
+ return next;
64
+ }
65
+
66
+ export function setCartesianRange(
67
+ program,
68
+ id,
69
+ channel,
70
+ lower,
71
+ upper,
72
+ scale,
73
+ title
74
+ ) {
75
+ return setCartesianPosition(program, id, channel, {
76
+ field: lower,
77
+ fieldType: "quantitative",
78
+ scale,
79
+ title
80
+ })
81
+ .editSemantic({
82
+ property: `layer[${id}].encoding.${channel}2.field`,
83
+ value: upper
84
+ })
85
+ .editSemantic({
86
+ property: `layer[${id}].encoding.${channel}2.fieldType`,
87
+ value: "quantitative"
88
+ })
89
+ .editSemantic({
90
+ property: `layer[${id}].encoding.${channel}2.scale`,
91
+ value: scale
92
+ });
93
+ }
94
+
95
+ function axisMethods(channel) {
96
+ const prefix = channel === "x" ? "X" : "Y";
97
+ return {
98
+ ticks: `edit${prefix}AxisTicks`,
99
+ labels: `edit${prefix}AxisLabels`,
100
+ title: `edit${prefix}AxisTitle`
101
+ };
102
+ }
103
+
104
+ function isDiscretePosition(scale) {
105
+ return ["ordinal", "band", "point"].includes(scale?.type);
106
+ }
107
+
108
+ function normalizeTickConfig(config, scale) {
109
+ if (config === undefined) return undefined;
110
+ if (isDiscretePosition(scale)) {
111
+ if (config.mode === "count" || config.inferredValues === true) {
112
+ const next = {
113
+ ...config,
114
+ mode: "values",
115
+ values: scale.domain,
116
+ inferredValues: true
117
+ };
118
+ delete next.count;
119
+ return next;
120
+ }
121
+ return config;
122
+ }
123
+ if (config.mode === "values" && config.inferredValues === true) {
124
+ const next = {
125
+ ...config,
126
+ mode: "count",
127
+ count: DEFAULT_TICK_COUNT,
128
+ inferredValues: true
129
+ };
130
+ delete next.values;
131
+ return next;
132
+ }
133
+ return config;
134
+ }
135
+
136
+ function outsideConsumer(program, owned, channel, scale) {
137
+ return program.semanticSpec.layers.some(layer =>
138
+ !owned.has(layer.id) && layer.encoding?.[channel]?.scale === scale
139
+ );
140
+ }
141
+
142
+ export function assertDistributionScaleHandoff(program, {
143
+ owned,
144
+ oldXScale,
145
+ oldYScale,
146
+ newXScale,
147
+ newYScale
148
+ }) {
149
+ const ids = new Set(owned);
150
+ for (const [channel, previous, next] of [
151
+ ["x", oldXScale, newXScale],
152
+ ["y", oldYScale, newYScale]
153
+ ]) {
154
+ if (
155
+ previous !== next &&
156
+ outsideConsumer(program, ids, channel, previous)
157
+ ) {
158
+ throw new Error(
159
+ `Cannot hand off ${channel} scale "${previous}" while unrelated consumers remain.`
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ export function clearCartesianPositions(program, id) {
166
+ const layer = findLayer(program, id);
167
+ if (layer === undefined) return program;
168
+ let next = program;
169
+ for (const channel of POSITION_CHANNELS) {
170
+ if (layer.encoding?.[channel] === undefined) continue;
171
+ next = next.editSemantic({
172
+ property: `layer[${id}].encoding.${channel}`,
173
+ remove: true
174
+ });
175
+ }
176
+ return next;
177
+ }
178
+
179
+ function rebindAxis(program, channel, {
180
+ previousScale,
181
+ nextScale,
182
+ previousTitle,
183
+ nextTitle
184
+ }) {
185
+ const semantic = program.semanticSpec.guides.axis?.[channel];
186
+ if (semantic?.scale !== previousScale) return program;
187
+ let next = program.editSemantic({
188
+ property: `guide.axis.${channel}.scale`,
189
+ value: nextScale
190
+ });
191
+ if (semantic.title === previousTitle) {
192
+ next = next.editSemantic({
193
+ property: `guide.axis.${channel}.title`,
194
+ value: nextTitle
195
+ });
196
+ }
197
+ const scale = next.resolvedScales[nextScale];
198
+ const ticks = normalizeTickConfig(
199
+ next.guideConfigs.axis?.[channel]?.ticks,
200
+ scale
201
+ );
202
+ if (ticks !== undefined) {
203
+ next = next._withGuideConfig(channel, "ticks", {
204
+ ...ticks,
205
+ scale: nextScale
206
+ });
207
+ }
208
+ const labels = next.guideConfigs.axis?.[channel]?.labels;
209
+ if (labels !== undefined) {
210
+ const tickConfig = next.guideConfigs.axis[channel].ticks;
211
+ next = next._withGuideConfig(channel, "labels", {
212
+ ...labels,
213
+ scale: nextScale,
214
+ mode: tickConfig.mode,
215
+ inferredValues: tickConfig.inferredValues,
216
+ ...(tickConfig.mode === "values"
217
+ ? { values: tickConfig.values }
218
+ : { count: tickConfig.count })
219
+ });
220
+ if (tickConfig.mode === "values") {
221
+ const config = next.guideConfigs.axis[channel].labels;
222
+ if (Object.hasOwn(config, "count")) {
223
+ const { count: _count, ...withoutCount } = config;
224
+ next = next._withGuideConfig(channel, "labels", withoutCount);
225
+ }
226
+ } else {
227
+ const config = next.guideConfigs.axis[channel].labels;
228
+ if (Object.hasOwn(config, "values")) {
229
+ const { values: _values, ...withoutValues } = config;
230
+ next = next._withGuideConfig(channel, "labels", withoutValues);
231
+ }
232
+ }
233
+ }
234
+ const title = next.guideConfigs.axis?.[channel]?.title;
235
+ if (title !== undefined) {
236
+ next = next._withGuideConfig(channel, "title", {
237
+ ...title,
238
+ scale: nextScale
239
+ });
240
+ }
241
+ const methods = axisMethods(channel);
242
+ if (ticks !== undefined) next = next[methods.ticks]();
243
+ if (labels !== undefined) next = next[methods.labels]();
244
+ if (title !== undefined) next = next[methods.title]();
245
+ return next;
246
+ }
247
+
248
+ function gridDirection(channel) {
249
+ return channel === "x" ? "vertical" : "horizontal";
250
+ }
251
+
252
+ function rebindGrid(program, {
253
+ previousMeasureChannel,
254
+ nextMeasureChannel,
255
+ previousScale,
256
+ nextScale
257
+ }) {
258
+ const previousDirection = gridDirection(previousMeasureChannel);
259
+ const nextDirection = gridDirection(nextMeasureChannel);
260
+ const stored = program.guideConfigs.grid?.[previousDirection];
261
+ const semantic = program.semanticSpec.guides.grid?.[previousDirection];
262
+ if (stored === undefined || semantic?.scale !== previousScale) return program;
263
+ if (previousDirection === nextDirection) {
264
+ return program
265
+ .editSemantic({
266
+ property: `guide.grid.${previousDirection}.scale`,
267
+ value: nextScale
268
+ })
269
+ ._withGridConfig(previousDirection, {
270
+ ...stored,
271
+ scale: nextScale
272
+ })[`rematerialize${previousDirection === "horizontal" ? "Horizontal" : "Vertical"}Grid`]();
273
+ }
274
+ if (program.semanticSpec.guides.grid?.[nextDirection] !== undefined) {
275
+ throw new Error(
276
+ `Cannot hand off the ${previousDirection} grid because a ${nextDirection} grid already exists.`
277
+ );
278
+ }
279
+ const options = {
280
+ scale: nextScale,
281
+ coordinate: stored.coordinate,
282
+ color: stored.color,
283
+ lineWidth: stored.lineWidth,
284
+ strokeDash: stored.strokeDash,
285
+ ...(stored.inferredValues === true
286
+ ? {}
287
+ : stored.mode === "values"
288
+ ? { values: stored.values }
289
+ : { count: stored.count })
290
+ };
291
+ return program
292
+ .removeGrid({ [previousDirection]: true })
293
+ [`create${nextDirection === "horizontal" ? "Horizontal" : "Vertical"}Grid`](options);
294
+ }
295
+
296
+ export function rebindDistributionGuides(program, {
297
+ oldXScale,
298
+ oldYScale,
299
+ newXScale,
300
+ newYScale,
301
+ oldXTitle,
302
+ oldYTitle,
303
+ newXTitle,
304
+ newYTitle,
305
+ oldMeasureChannel,
306
+ newMeasureChannel
307
+ }) {
308
+ let next = rebindAxis(program, "x", {
309
+ previousScale: oldXScale,
310
+ nextScale: newXScale,
311
+ previousTitle: oldXTitle,
312
+ nextTitle: newXTitle
313
+ });
314
+ next = rebindAxis(next, "y", {
315
+ previousScale: oldYScale,
316
+ nextScale: newYScale,
317
+ previousTitle: oldYTitle,
318
+ nextTitle: newYTitle
319
+ });
320
+ return rebindGrid(next, {
321
+ previousMeasureChannel: oldMeasureChannel,
322
+ nextMeasureChannel: newMeasureChannel,
323
+ previousScale: oldMeasureChannel === "x" ? oldXScale : oldYScale,
324
+ nextScale: newMeasureChannel === "x" ? newXScale : newYScale
325
+ });
326
+ }