ggaction 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/actions/boxPlots/edit.js +455 -3
- package/src/actions/boxPlots/options.js +3 -3
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/data/bin2d.js +140 -17
- 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 +35 -1
- package/src/actions/encodings/density.js +215 -80
- package/src/actions/encodings/index.js +2 -0
- 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/legends/categorical/actions.js +19 -8
- package/src/actions/guides/legends/continuous/common.js +3 -3
- 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/point/edit.js +15 -3
- package/src/actions/marks/point/materialize.js +9 -3
- package/src/actions/marks/rule/actions.js +4 -1
- package/src/actions/regression/edit.js +235 -74
- package/src/actions/selection/actions.js +122 -2
- package/src/actions/selection/index.js +6 -0
- package/src/core/vocabulary.js +7 -0
- package/types/index.d.ts +1 -0
- package/types/program.d.ts +71 -7
|
@@ -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";
|
|
@@ -18,11 +19,43 @@ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
|
|
|
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,6 +92,74 @@ function preflight(program, sourceId, transform) {
|
|
|
59
92
|
deriveBin2DRows(source.values, transform);
|
|
60
93
|
}
|
|
61
94
|
|
|
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.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
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
|
+
}
|
|
162
|
+
|
|
62
163
|
export const materializeBin2DData = action(
|
|
63
164
|
{
|
|
64
165
|
op: "materializeBin2DData",
|
|
@@ -113,25 +214,47 @@ export const createBin2DData = action(
|
|
|
113
214
|
);
|
|
114
215
|
}
|
|
115
216
|
|
|
116
|
-
|
|
117
|
-
const consumers = directLayerConsumers(this, previous.id);
|
|
118
|
-
const revision = planDerivedDataRevision(this, {
|
|
217
|
+
return applyBin2DRevision(this, {
|
|
119
218
|
owner,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
219
|
+
previous,
|
|
220
|
+
source,
|
|
221
|
+
transform
|
|
123
222
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
export const editBin2DData = action(
|
|
227
|
+
{
|
|
228
|
+
op: "editBin2DData",
|
|
229
|
+
description: "Partially revise one logical rectangular 2D-bin owner."
|
|
230
|
+
},
|
|
231
|
+
function (args = {}) {
|
|
232
|
+
validateKeys(args, EDIT_OPTIONS, "editBin2DData");
|
|
233
|
+
if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
|
|
234
|
+
throw new Error("editBin2DData requires at least one transform or source option.");
|
|
130
235
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
236
|
+
const owner = resolveBin2DOwner(this, args.target);
|
|
237
|
+
const previous = requireCurrentBin2D(this, owner, ownerConfig(this, owner));
|
|
238
|
+
const source = validateUserId(
|
|
239
|
+
args.source ?? previous.source,
|
|
240
|
+
"2D bin source dataset id"
|
|
135
241
|
);
|
|
242
|
+
const transform = editedTransform(owner, previous, args);
|
|
243
|
+
if (
|
|
244
|
+
source === previous.source &&
|
|
245
|
+
sameRequestedTransform(
|
|
246
|
+
transform,
|
|
247
|
+
requestedBin2DTransform(previous.transform[0])
|
|
248
|
+
)
|
|
249
|
+
) {
|
|
250
|
+
throw new Error("editBin2DData requires an actual transform or source change.");
|
|
251
|
+
}
|
|
252
|
+
preflight(this, source, transform);
|
|
253
|
+
const revision = { owner, previous, source, transform };
|
|
254
|
+
|
|
255
|
+
// Execute one speculative immutable branch so every consumer plan is known
|
|
256
|
+
// to succeed before the returned action trace records its first child.
|
|
257
|
+
applyBin2DRevision(this, revision);
|
|
258
|
+
return applyBin2DRevision(this, revision);
|
|
136
259
|
}
|
|
137
260
|
);
|
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
resolveAppearanceScaleDefinition,
|
|
10
10
|
resolveOpacityScaleDefinition
|
|
11
11
|
} from "../scales/definitions.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
findLayer,
|
|
14
|
+
resolveEligibleLayer
|
|
15
|
+
} from "../../selectors/layers.js";
|
|
13
16
|
import { applyMaterializationPlan } from "../../materialization/dependencies.js";
|
|
14
17
|
import { planEncodingRematerialization } from "../../materialization/encodings.js";
|
|
15
18
|
import {
|
|
@@ -20,6 +23,7 @@ import {
|
|
|
20
23
|
} from "./shared.js";
|
|
21
24
|
|
|
22
25
|
const RADIUS_OPTIONS = Object.freeze(["value", "target"]);
|
|
26
|
+
const REMOVE_RADIUS_OPTIONS = Object.freeze(["target"]);
|
|
23
27
|
const OPACITY_OPTIONS = Object.freeze([
|
|
24
28
|
"value", "field", "target", "fieldType", "scale"
|
|
25
29
|
]);
|
|
@@ -129,6 +133,35 @@ const encodePointRadius = action(
|
|
|
129
133
|
}
|
|
130
134
|
);
|
|
131
135
|
|
|
136
|
+
const removePointRadius = action(
|
|
137
|
+
{
|
|
138
|
+
op: "removePointRadius",
|
|
139
|
+
description: "Remove a constant point radius and restore the theme default."
|
|
140
|
+
},
|
|
141
|
+
function (args = {}) {
|
|
142
|
+
validateOptions(args, REMOVE_RADIUS_OPTIONS, "removePointRadius");
|
|
143
|
+
const requested = args.target === undefined
|
|
144
|
+
? undefined
|
|
145
|
+
: args.target;
|
|
146
|
+
const layer = resolveEligibleLayer(this, {
|
|
147
|
+
target: requested,
|
|
148
|
+
predicate: candidate =>
|
|
149
|
+
candidate.mark?.type === "point" &&
|
|
150
|
+
this.markConfigs[candidate.id]?.radius !== undefined,
|
|
151
|
+
label: "point mark with an explicit radius"
|
|
152
|
+
});
|
|
153
|
+
const next = this
|
|
154
|
+
._withoutMaterializationConfig(["marks", layer.id, "radius"]);
|
|
155
|
+
const graphic = next.graphicSpec.objects[layer.id];
|
|
156
|
+
const baseline = graphic === undefined
|
|
157
|
+
? next
|
|
158
|
+
: graphic.type === "collection"
|
|
159
|
+
? next.editGraphics({ target: layer.id, property: "items", value: [] })
|
|
160
|
+
: next.editGraphics({ target: layer.id, property: "length", value: 0 });
|
|
161
|
+
return baseline.rematerializePointMark({ id: layer.id });
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
132
165
|
const encodeSize = action(
|
|
133
166
|
{
|
|
134
167
|
op: "encodeSize",
|
|
@@ -247,6 +280,7 @@ const encodeOpacity = action(
|
|
|
247
280
|
export function registerAppearanceEncodingAction(ProgramClass) {
|
|
248
281
|
ProgramClass.prototype.encodeRadius = encodeRadius;
|
|
249
282
|
ProgramClass.prototype.encodePointRadius = encodePointRadius;
|
|
283
|
+
ProgramClass.prototype.removePointRadius = removePointRadius;
|
|
250
284
|
ProgramClass.prototype.encodeSize = encodeSize;
|
|
251
285
|
ProgramClass.prototype.encodeShape = encodeShape;
|
|
252
286
|
ProgramClass.prototype.encodeOpacity = encodeOpacity;
|