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.
- package/CHANGELOG.md +48 -0
- package/README.md +77 -14
- package/package.json +19 -2
- package/src/BasicChartProgram.js +10 -0
- package/src/actions/basic.js +21 -0
- package/src/actions/boxPlots/edit.js +455 -3
- package/src/actions/boxPlots/options.js +3 -3
- package/src/actions/canvas/actions.js +42 -0
- package/src/actions/canvas/index.js +5 -1
- package/src/actions/charts/basic.js +13 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/data/basic.js +11 -0
- package/src/actions/data/basicBin2d.js +43 -0
- package/src/actions/data/bin2d.js +138 -40
- package/src/actions/data/bin2dMaterialize.js +29 -0
- package/src/actions/data/index.js +6 -1
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +41 -3
- package/src/actions/encodings/basic.js +21 -0
- package/src/actions/encodings/density.js +215 -80
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/index.js +5 -1
- package/src/actions/encodings/ranged.js +6 -2
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +136 -1
- package/src/actions/gradientPlots/edit.js +352 -4
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +10 -2
- package/src/actions/guides/basic.js +19 -0
- package/src/actions/guides/grids/grid.js +7 -3
- package/src/actions/guides/grids/index.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +19 -8
- package/src/actions/guides/legends/categorical/index.js +6 -2
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/edit.js +61 -3
- package/src/actions/guides/legends/remove.js +124 -23
- package/src/actions/guides/legends/strokeWidth.js +42 -14
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/bar/index.js +5 -1
- package/src/actions/marks/basic.js +11 -0
- package/src/actions/marks/line/actions.js +5 -1
- package/src/actions/marks/line/index.js +4 -1
- package/src/actions/marks/point/edit.js +15 -3
- package/src/actions/marks/point/index.js +5 -1
- package/src/actions/marks/point/materialize.js +9 -3
- package/src/actions/marks/rect/actions.js +5 -1
- package/src/actions/marks/rect/index.js +4 -1
- package/src/actions/marks/rule/actions.js +4 -1
- package/src/actions/regression/edit.js +235 -74
- package/src/actions/scales/index.js +5 -1
- package/src/actions/selection/actions.js +122 -2
- package/src/actions/selection/index.js +6 -0
- package/src/basic.js +2 -0
- package/src/core/vocabulary.js +7 -0
- package/src/renderers/canvas/index.js +96 -60
- package/src/renderers/canvas/text.js +3 -14
- package/src/renderers/pdf.js +112 -0
- package/src/renderers/svg.js +559 -0
- package/src/renderers/text.js +11 -0
- package/types/basic.d.ts +107 -0
- package/types/index.d.ts +1 -0
- package/types/pdf.d.ts +26 -0
- package/types/program.d.ts +71 -7
- 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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
117
|
-
const consumers = directLayerConsumers(this, previous.id);
|
|
118
|
-
const revision = planDerivedDataRevision(this, {
|
|
192
|
+
return applyBin2DRevision(this, {
|
|
119
193
|
owner,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
194
|
+
previous,
|
|
195
|
+
source,
|
|
196
|
+
transform
|
|
123
197
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 {
|
|
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
|
+
}
|