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
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import { validateKeys } from "../../core/validation.js";
|
|
4
5
|
import { planDerivedDataRevision } from "../../materialization/dataProvenance.js";
|
|
5
6
|
import { findDataset } from "../../selectors/datasets.js";
|
|
6
7
|
import { findLayer } from "../../selectors/layers.js";
|
|
8
|
+
import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
|
|
7
9
|
import { removeGradientPlotLegend } from "./components.js";
|
|
10
|
+
import {
|
|
11
|
+
assertDistributionScaleHandoff,
|
|
12
|
+
clearCartesianPositions,
|
|
13
|
+
rebindDistributionGuides,
|
|
14
|
+
resolveDistributionScalePlan,
|
|
15
|
+
setCartesianPosition,
|
|
16
|
+
setCartesianRange
|
|
17
|
+
} from "../distributions/revision.js";
|
|
8
18
|
import {
|
|
9
19
|
resolveGradientAppearance,
|
|
10
20
|
resolveGradientCenter,
|
|
11
21
|
resolveGradientDensity,
|
|
22
|
+
resolveGradientPosition,
|
|
12
23
|
resolveGradientWidth
|
|
13
24
|
} from "./options.js";
|
|
14
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
normalizeGradientPositionTypes,
|
|
27
|
+
resolveGradientOwner
|
|
28
|
+
} from "./resolve.js";
|
|
15
29
|
|
|
16
30
|
const OPTIONS = Object.freeze([
|
|
17
|
-
"target", "density", "width", "gradient", "center"
|
|
31
|
+
"target", "data", "x", "y", "density", "width", "gradient", "center"
|
|
18
32
|
]);
|
|
19
33
|
|
|
20
34
|
function patch(value, label) {
|
|
@@ -26,13 +40,224 @@ function patch(value, label) {
|
|
|
26
40
|
|
|
27
41
|
function removeCenter(program, id) {
|
|
28
42
|
let next = program;
|
|
43
|
+
const selections = Object.entries(
|
|
44
|
+
program.materializationConfigs.selections ?? {}
|
|
45
|
+
).filter(([, config]) => config.target === id).map(([selection]) => selection);
|
|
46
|
+
for (const [highlight, config] of Object.entries(
|
|
47
|
+
program.materializationConfigs.highlights ?? {}
|
|
48
|
+
)) {
|
|
49
|
+
if (config.target === id || selections.includes(config.selection)) {
|
|
50
|
+
next = next._withoutMaterializationConfig(["highlights", highlight]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const selection of selections) {
|
|
54
|
+
next = next._withoutMaterializationConfig(["selections", selection]);
|
|
55
|
+
}
|
|
29
56
|
if (findLayer(next, id) !== undefined) {
|
|
30
57
|
next = next.editSemantic({ property: `layer[${id}]`, remove: true });
|
|
31
58
|
}
|
|
32
59
|
if (next.graphicSpec.objects[id] !== undefined) {
|
|
33
60
|
next = next.editGraphics({ target: id, remove: true });
|
|
34
61
|
}
|
|
35
|
-
return next
|
|
62
|
+
return next
|
|
63
|
+
._withoutMaterializationConfig(["marks", id])
|
|
64
|
+
._withContext({
|
|
65
|
+
...(program.context.currentMark === id ? { currentMark: undefined } : {}),
|
|
66
|
+
...(selections.includes(program.context.currentSelection)
|
|
67
|
+
? { currentSelection: undefined }
|
|
68
|
+
: {})
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function currentGradientPositions(owner, current) {
|
|
73
|
+
const categoryChannel = current.orientation === "vertical" ? "x" : "y";
|
|
74
|
+
const measureChannel = current.orientation === "vertical" ? "y" : "x";
|
|
75
|
+
const categoryEncoding = owner.encoding[categoryChannel];
|
|
76
|
+
const measureEncoding = owner.encoding[measureChannel];
|
|
77
|
+
return {
|
|
78
|
+
x: current.orientation === "vertical"
|
|
79
|
+
? {
|
|
80
|
+
field: current.category,
|
|
81
|
+
fieldType: current.categoryType,
|
|
82
|
+
scale: categoryEncoding.scale
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
field: current.measure,
|
|
86
|
+
fieldType: "quantitative",
|
|
87
|
+
scale: measureEncoding.scale
|
|
88
|
+
},
|
|
89
|
+
y: current.orientation === "vertical"
|
|
90
|
+
? {
|
|
91
|
+
field: current.measure,
|
|
92
|
+
fieldType: "quantitative",
|
|
93
|
+
scale: measureEncoding.scale
|
|
94
|
+
}
|
|
95
|
+
: {
|
|
96
|
+
field: current.category,
|
|
97
|
+
fieldType: current.categoryType,
|
|
98
|
+
scale: categoryEncoding.scale
|
|
99
|
+
},
|
|
100
|
+
categoryScale: categoryEncoding.scale,
|
|
101
|
+
measureScale: measureEncoding.scale
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requirePositionField(position, label) {
|
|
106
|
+
if (typeof position?.field !== "string" || position.field.length === 0) {
|
|
107
|
+
throw new TypeError(`${label} field must be a non-empty string.`);
|
|
108
|
+
}
|
|
109
|
+
return position;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function roleCandidate(program, owner, current, args) {
|
|
113
|
+
const previous = currentGradientPositions(owner, current);
|
|
114
|
+
const positions = normalizeGradientPositionTypes(
|
|
115
|
+
requirePositionField(
|
|
116
|
+
Object.hasOwn(args, "x")
|
|
117
|
+
? resolveGradientPosition(args.x, "x", "editGradientPlot")
|
|
118
|
+
: previous.x,
|
|
119
|
+
"editGradientPlot x"
|
|
120
|
+
),
|
|
121
|
+
requirePositionField(
|
|
122
|
+
Object.hasOwn(args, "y")
|
|
123
|
+
? resolveGradientPosition(args.y, "y", "editGradientPlot")
|
|
124
|
+
: previous.y,
|
|
125
|
+
"editGradientPlot y"
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
if (positions.orientation === undefined) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"editGradientPlot requires one categorical axis and one quantitative axis."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const xRoleScale = positions.orientation === "vertical"
|
|
134
|
+
? previous.categoryScale
|
|
135
|
+
: previous.measureScale;
|
|
136
|
+
const yRoleScale = positions.orientation === "vertical"
|
|
137
|
+
? previous.measureScale
|
|
138
|
+
: previous.categoryScale;
|
|
139
|
+
const xScale = resolveDistributionScalePlan(program, {
|
|
140
|
+
channel: "x",
|
|
141
|
+
fieldType: positions.x.fieldType,
|
|
142
|
+
requested: positions.x.scale,
|
|
143
|
+
fallback: xRoleScale,
|
|
144
|
+
defaults: ["ordinal", "nominal"].includes(positions.x.fieldType)
|
|
145
|
+
? { discreteType: "band" }
|
|
146
|
+
: {}
|
|
147
|
+
});
|
|
148
|
+
const yScale = resolveDistributionScalePlan(program, {
|
|
149
|
+
channel: "y",
|
|
150
|
+
fieldType: positions.y.fieldType,
|
|
151
|
+
requested: positions.y.scale,
|
|
152
|
+
fallback: yRoleScale,
|
|
153
|
+
defaults: ["ordinal", "nominal"].includes(positions.y.fieldType)
|
|
154
|
+
? { discreteType: "band" }
|
|
155
|
+
: {}
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
source: Object.hasOwn(args, "data")
|
|
159
|
+
? validateUserId(args.data, "Gradient-plot data id")
|
|
160
|
+
: current.source,
|
|
161
|
+
orientation: positions.orientation,
|
|
162
|
+
x: { ...positions.x, scale: xScale.id },
|
|
163
|
+
y: { ...positions.y, scale: yScale.id },
|
|
164
|
+
xScale,
|
|
165
|
+
yScale,
|
|
166
|
+
category: positions.orientation === "vertical"
|
|
167
|
+
? positions.x.field
|
|
168
|
+
: positions.y.field,
|
|
169
|
+
categoryType: positions.orientation === "vertical"
|
|
170
|
+
? positions.x.fieldType
|
|
171
|
+
: positions.y.fieldType,
|
|
172
|
+
measure: positions.orientation === "vertical"
|
|
173
|
+
? positions.y.field
|
|
174
|
+
: positions.x.field,
|
|
175
|
+
previous
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sameRoleCandidate(current, candidate) {
|
|
180
|
+
return candidate.source === current.source &&
|
|
181
|
+
candidate.orientation === current.orientation &&
|
|
182
|
+
candidate.category === current.category &&
|
|
183
|
+
candidate.categoryType === current.categoryType &&
|
|
184
|
+
candidate.measure === current.measure &&
|
|
185
|
+
candidate.xScale.id === candidate.previous.x.scale &&
|
|
186
|
+
candidate.yScale.id === candidate.previous.y.scale &&
|
|
187
|
+
candidate.xScale.edit === undefined &&
|
|
188
|
+
candidate.yScale.edit === undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function updateGradientPositions(program, owner, current, candidate, hasCenter) {
|
|
192
|
+
const owned = [owner.id, ...(hasCenter ? [current.centerId] : [])];
|
|
193
|
+
assertDistributionScaleHandoff(program, {
|
|
194
|
+
owned,
|
|
195
|
+
oldXScale: candidate.previous.x.scale,
|
|
196
|
+
oldYScale: candidate.previous.y.scale,
|
|
197
|
+
newXScale: candidate.xScale.id,
|
|
198
|
+
newYScale: candidate.yScale.id
|
|
199
|
+
});
|
|
200
|
+
let next = program;
|
|
201
|
+
for (const id of owned) next = clearCartesianPositions(next, id);
|
|
202
|
+
for (const scale of [candidate.xScale, candidate.yScale]) {
|
|
203
|
+
if (scale.create) next = next.createScale(scale.definition);
|
|
204
|
+
}
|
|
205
|
+
const category = candidate.orientation === "vertical" ? candidate.x : candidate.y;
|
|
206
|
+
const measure = candidate.orientation === "vertical" ? candidate.y : candidate.x;
|
|
207
|
+
const categoryChannel = candidate.orientation === "vertical" ? "x" : "y";
|
|
208
|
+
const measureChannel = candidate.orientation === "vertical" ? "y" : "x";
|
|
209
|
+
next = setCartesianPosition(next, owner.id, categoryChannel, {
|
|
210
|
+
field: candidate.category,
|
|
211
|
+
fieldType: candidate.categoryType,
|
|
212
|
+
scale: category.scale
|
|
213
|
+
});
|
|
214
|
+
next = setCartesianRange(
|
|
215
|
+
next,
|
|
216
|
+
owner.id,
|
|
217
|
+
measureChannel,
|
|
218
|
+
GRADIENT_PROFILE_FIELDS.lower,
|
|
219
|
+
GRADIENT_PROFILE_FIELDS.upper,
|
|
220
|
+
measure.scale
|
|
221
|
+
);
|
|
222
|
+
if (hasCenter) {
|
|
223
|
+
next = setCartesianPosition(next, current.centerId, categoryChannel, {
|
|
224
|
+
field: candidate.category,
|
|
225
|
+
fieldType: candidate.categoryType,
|
|
226
|
+
scale: category.scale
|
|
227
|
+
});
|
|
228
|
+
next = setCartesianPosition(next, current.centerId, measureChannel, {
|
|
229
|
+
field: GRADIENT_PROFILE_FIELDS.center,
|
|
230
|
+
fieldType: "quantitative",
|
|
231
|
+
scale: measure.scale
|
|
232
|
+
})._withMarkConfig(current.centerId, {
|
|
233
|
+
...next.markConfigs[current.centerId],
|
|
234
|
+
fixedSpan: {
|
|
235
|
+
...next.markConfigs[current.centerId].fixedSpan,
|
|
236
|
+
orientation: candidate.orientation === "vertical"
|
|
237
|
+
? "horizontal"
|
|
238
|
+
: "vertical"
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
for (const id of new Set([candidate.xScale.id, candidate.yScale.id])) {
|
|
243
|
+
next = next.rematerializeScale({ id, marks: false, guides: false });
|
|
244
|
+
}
|
|
245
|
+
next = rebindDistributionGuides(next, {
|
|
246
|
+
oldXScale: candidate.previous.x.scale,
|
|
247
|
+
oldYScale: candidate.previous.y.scale,
|
|
248
|
+
newXScale: candidate.xScale.id,
|
|
249
|
+
newYScale: candidate.yScale.id,
|
|
250
|
+
oldXTitle: candidate.previous.x.field,
|
|
251
|
+
oldYTitle: candidate.previous.y.field,
|
|
252
|
+
newXTitle: candidate.x.field,
|
|
253
|
+
newYTitle: candidate.y.field,
|
|
254
|
+
oldMeasureChannel: current.orientation === "vertical" ? "y" : "x",
|
|
255
|
+
newMeasureChannel: candidate.orientation === "vertical" ? "y" : "x"
|
|
256
|
+
});
|
|
257
|
+
for (const scale of [candidate.xScale, candidate.yScale]) {
|
|
258
|
+
if (scale.edit !== undefined) next = next.editScale(scale.edit);
|
|
259
|
+
}
|
|
260
|
+
return next;
|
|
36
261
|
}
|
|
37
262
|
|
|
38
263
|
export const editGradientPlot = action(
|
|
@@ -67,6 +292,129 @@ export const editGradientPlot = action(
|
|
|
67
292
|
"editGradientPlot"
|
|
68
293
|
)
|
|
69
294
|
: current.center;
|
|
295
|
+
const candidate = roleCandidate(this, owner, current, args);
|
|
296
|
+
const sourceDataset = findDataset(this, candidate.source);
|
|
297
|
+
if (sourceDataset === undefined) {
|
|
298
|
+
throw new Error(`Unknown gradient-plot data "${candidate.source}".`);
|
|
299
|
+
}
|
|
300
|
+
const roleRequested = ["data", "x", "y"].some(
|
|
301
|
+
key => Object.hasOwn(args, key)
|
|
302
|
+
);
|
|
303
|
+
const changesRoles = !sameRoleCandidate(current, candidate);
|
|
304
|
+
if (changesRoles) {
|
|
305
|
+
const hadCenter = findLayer(this, current.centerId) !== undefined;
|
|
306
|
+
const revision = planDerivedDataRevision(this, {
|
|
307
|
+
owner: owner.id,
|
|
308
|
+
role: "ProfileData",
|
|
309
|
+
previous: current.profileId,
|
|
310
|
+
consumers: [owner.id, ...(hadCenter ? [current.centerId] : [])]
|
|
311
|
+
});
|
|
312
|
+
const applyEdit = program => {
|
|
313
|
+
let next = program.createGradientProfileData({
|
|
314
|
+
id: revision.id,
|
|
315
|
+
source: candidate.source,
|
|
316
|
+
category: candidate.category,
|
|
317
|
+
field: candidate.measure,
|
|
318
|
+
...density,
|
|
319
|
+
center: center === false ? "median" : center.type
|
|
320
|
+
});
|
|
321
|
+
for (const rebind of revision.rebinds) {
|
|
322
|
+
next = next.rebindLayerData(rebind);
|
|
323
|
+
}
|
|
324
|
+
const profile = findDataset(next, revision.id);
|
|
325
|
+
next = next._withMarkConfig(owner.id, {
|
|
326
|
+
...next.markConfigs[owner.id],
|
|
327
|
+
gradientPlot: {
|
|
328
|
+
...current,
|
|
329
|
+
source: candidate.source,
|
|
330
|
+
orientation: candidate.orientation,
|
|
331
|
+
category: candidate.category,
|
|
332
|
+
categoryType: candidate.categoryType,
|
|
333
|
+
measure: candidate.measure,
|
|
334
|
+
density,
|
|
335
|
+
width,
|
|
336
|
+
gradient,
|
|
337
|
+
center,
|
|
338
|
+
profileId: revision.id,
|
|
339
|
+
intensityDomain: profile.transform[0].resolved.intensityDomain
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
if (center === false && hadCenter) {
|
|
343
|
+
next = removeCenter(next, current.centerId);
|
|
344
|
+
}
|
|
345
|
+
const retainedCenter = center !== false && hadCenter;
|
|
346
|
+
next = updateGradientPositions(
|
|
347
|
+
next,
|
|
348
|
+
owner,
|
|
349
|
+
current,
|
|
350
|
+
candidate,
|
|
351
|
+
retainedCenter
|
|
352
|
+
).rematerializeRectMark({ id: owner.id });
|
|
353
|
+
const category = candidate.orientation === "vertical"
|
|
354
|
+
? candidate.x
|
|
355
|
+
: candidate.y;
|
|
356
|
+
const measure = candidate.orientation === "vertical"
|
|
357
|
+
? candidate.y
|
|
358
|
+
: candidate.x;
|
|
359
|
+
const categoryScale = next.resolvedScales[category.scale];
|
|
360
|
+
const spanSize = Math.max(
|
|
361
|
+
1,
|
|
362
|
+
categoryScale.bandwidth * width.band - 16
|
|
363
|
+
);
|
|
364
|
+
if (center !== false && !hadCenter) {
|
|
365
|
+
next = next.createGradientPlotCenter({
|
|
366
|
+
id: current.centerId,
|
|
367
|
+
owner: owner.id,
|
|
368
|
+
data: revision.id,
|
|
369
|
+
category: candidate.category,
|
|
370
|
+
categoryType: candidate.categoryType,
|
|
371
|
+
coordinate: owner.coordinate,
|
|
372
|
+
categoryScale: category.scale,
|
|
373
|
+
measureScale: measure.scale,
|
|
374
|
+
orientation: candidate.orientation,
|
|
375
|
+
size: spanSize,
|
|
376
|
+
stroke: center.stroke,
|
|
377
|
+
strokeWidth: center.strokeWidth
|
|
378
|
+
});
|
|
379
|
+
} else if (retainedCenter) {
|
|
380
|
+
next = next
|
|
381
|
+
.encodeStroke({ target: current.centerId, value: center.stroke })
|
|
382
|
+
.encodeStrokeWidth({
|
|
383
|
+
target: current.centerId,
|
|
384
|
+
value: center.strokeWidth
|
|
385
|
+
})
|
|
386
|
+
.materializeRuleSpan({
|
|
387
|
+
id: current.centerId,
|
|
388
|
+
orientation: candidate.orientation === "vertical"
|
|
389
|
+
? "horizontal"
|
|
390
|
+
: "vertical",
|
|
391
|
+
size: spanSize
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (current.guides?.legend !== false && current.guides?.legend !== undefined) {
|
|
395
|
+
next = removeGradientPlotLegend(next, owner.id)
|
|
396
|
+
.createGradientPlotLegend({
|
|
397
|
+
owner: owner.id,
|
|
398
|
+
...current.guides.legend
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
next = next.releaseDerivedData(revision.release);
|
|
402
|
+
return next._withContext({
|
|
403
|
+
currentMark: owner.id,
|
|
404
|
+
currentData: candidate.source
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
applyEdit(this);
|
|
408
|
+
return applyEdit(this);
|
|
409
|
+
}
|
|
410
|
+
if (roleRequested && !OPTIONS.slice(4).some(
|
|
411
|
+
key => Object.hasOwn(args, key)
|
|
412
|
+
)) {
|
|
413
|
+
return this._withContext({
|
|
414
|
+
currentMark: owner.id,
|
|
415
|
+
currentData: current.source
|
|
416
|
+
});
|
|
417
|
+
}
|
|
70
418
|
const densityChanged = JSON.stringify(density) !== JSON.stringify(current.density);
|
|
71
419
|
const centerTypeChanged = center !== false && current.center !== false &&
|
|
72
420
|
center.type !== current.center.type;
|
|
@@ -131,7 +479,7 @@ export const editGradientPlot = action(
|
|
|
131
479
|
.encodeStroke({ target: current.centerId, value: center.stroke })
|
|
132
480
|
.encodeStrokeWidth({ target: current.centerId, value: center.strokeWidth });
|
|
133
481
|
}
|
|
134
|
-
next = next.
|
|
482
|
+
next = next.rematerializeRectMark({ id: owner.id });
|
|
135
483
|
if (center !== false && findLayer(next, current.centerId) !== undefined) {
|
|
136
484
|
const categoryChannel = current.orientation === "vertical" ? "x" : "y";
|
|
137
485
|
const categoryScale = next.resolvedScales[owner.encoding[categoryChannel].scale];
|
|
@@ -18,11 +18,19 @@ const TITLE_OPTIONS = Object.freeze([
|
|
|
18
18
|
"text", "at", "offset", "rotation", "color", "fontSize",
|
|
19
19
|
"fontFamily", "fontWeight"
|
|
20
20
|
]);
|
|
21
|
+
const COMPONENTS = Object.freeze({
|
|
22
|
+
line: Object.freeze({ suffix: "Line", config: "line" }),
|
|
23
|
+
ticks: Object.freeze({ suffix: "Ticks", config: "ticks" }),
|
|
24
|
+
labels: Object.freeze({ suffix: "Labels", config: "labels" }),
|
|
25
|
+
title: Object.freeze({ suffix: "Title", config: "title" })
|
|
26
|
+
});
|
|
21
27
|
|
|
22
|
-
function validateNested(args, key, options, operation) {
|
|
28
|
+
function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
|
|
23
29
|
if (!Object.hasOwn(args, key)) return;
|
|
30
|
+
if (allowFalse && args[key] === false) return;
|
|
24
31
|
if (!isPlainObject(args[key])) {
|
|
25
|
-
|
|
32
|
+
const accepted = allowFalse ? "false or a plain object" : "a plain object";
|
|
33
|
+
throw new TypeError(`${operation}.${key} must be ${accepted}.`);
|
|
26
34
|
}
|
|
27
35
|
validateKeys(args[key], options, `${operation}.${key}`);
|
|
28
36
|
}
|
|
@@ -33,11 +41,12 @@ function validateArgs(args, operation) {
|
|
|
33
41
|
emptyMessage: `${operation} requires at least one axis change.`,
|
|
34
42
|
emptyError: Error
|
|
35
43
|
});
|
|
36
|
-
|
|
37
|
-
validateNested(args, "
|
|
38
|
-
validateNested(args, "
|
|
39
|
-
validateNested(args, "
|
|
40
|
-
validateNested(args, "
|
|
44
|
+
const removable = { allowFalse: true };
|
|
45
|
+
validateNested(args, "line", LINE_OPTIONS, operation, removable);
|
|
46
|
+
validateNested(args, "ticks", TICK_OPTIONS, operation, removable);
|
|
47
|
+
validateNested(args, "labels", LABEL_OPTIONS, operation, removable);
|
|
48
|
+
validateNested(args, "ticksAndLabels", GROUP_OPTIONS, operation, removable);
|
|
49
|
+
validateNested(args, "title", TITLE_OPTIONS, operation, removable);
|
|
41
50
|
if (args.ticksAndLabels !== undefined && (
|
|
42
51
|
args.ticks !== undefined || args.labels !== undefined
|
|
43
52
|
)) {
|
|
@@ -45,10 +54,10 @@ function validateArgs(args, operation) {
|
|
|
45
54
|
`${operation} cannot combine ticksAndLabels with ticks or labels.`
|
|
46
55
|
);
|
|
47
56
|
}
|
|
48
|
-
if (args.ticksAndLabels?.ticks !== undefined) {
|
|
57
|
+
if (args.ticksAndLabels !== false && args.ticksAndLabels?.ticks !== undefined) {
|
|
49
58
|
validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth"], `${operation}.ticksAndLabels`);
|
|
50
59
|
}
|
|
51
|
-
if (args.ticksAndLabels?.labels !== undefined) {
|
|
60
|
+
if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
|
|
52
61
|
validateNested(args.ticksAndLabels, "labels", [
|
|
53
62
|
"offset", "format", "color", "fontSize", "fontFamily", "fontWeight"
|
|
54
63
|
], `${operation}.ticksAndLabels`);
|
|
@@ -63,10 +72,114 @@ function names(channel) {
|
|
|
63
72
|
ticks: `edit${prefix}AxisTicks`,
|
|
64
73
|
labels: `edit${prefix}AxisLabels`,
|
|
65
74
|
group: `edit${prefix}AxisTicksAndLabels`,
|
|
66
|
-
title: `edit${prefix}AxisTitle
|
|
75
|
+
title: `edit${prefix}AxisTitle`,
|
|
76
|
+
remove: `remove${prefix}Axis`
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
function graphicId(channel, component) {
|
|
81
|
+
return `${channel}Axis${COMPONENTS[component].suffix}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasComponent(program, channel, component) {
|
|
85
|
+
return program.graphicSpec.objects[graphicId(channel, component)] !== undefined ||
|
|
86
|
+
program.guideConfigs.axis?.[channel]?.[COMPONENTS[component].config] !== undefined ||
|
|
87
|
+
(component === "title" &&
|
|
88
|
+
program.semanticSpec.guides.axis?.[channel]?.title !== undefined);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertRemovable(program, channel, component, operation) {
|
|
92
|
+
if (!hasComponent(program, channel, component)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${operation}.${component} requires an existing ${channel}-axis ${component}.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function removeComponent(program, channel, component) {
|
|
100
|
+
const id = graphicId(channel, component);
|
|
101
|
+
let next = program;
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
component === "title" &&
|
|
105
|
+
next.semanticSpec.guides.axis?.[channel]?.title !== undefined
|
|
106
|
+
) {
|
|
107
|
+
next = next.editSemantic({
|
|
108
|
+
property: `guide.axis.${channel}.title`,
|
|
109
|
+
remove: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (next.graphicSpec.objects[id] !== undefined) {
|
|
113
|
+
next = next.editGraphics({ target: id, remove: true });
|
|
114
|
+
}
|
|
115
|
+
return next._withoutMaterializationConfig([
|
|
116
|
+
"guides", "axis", channel, COMPONENTS[component].config
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildPlan(program, channel, args, operation) {
|
|
121
|
+
const shared = Object.hasOwn(args, "position")
|
|
122
|
+
? { position: args.position }
|
|
123
|
+
: {};
|
|
124
|
+
const plan = [];
|
|
125
|
+
const addLeaf = (component, edit) => {
|
|
126
|
+
const value = args[component];
|
|
127
|
+
if (value === false) {
|
|
128
|
+
assertRemovable(program, channel, component, operation.operation);
|
|
129
|
+
plan.push({ kind: "remove", component });
|
|
130
|
+
} else if (value !== undefined) {
|
|
131
|
+
plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
|
|
132
|
+
} else if (args.position !== undefined && hasComponent(program, channel, component)) {
|
|
133
|
+
plan.push({ kind: "edit", operation: edit, args: shared });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
addLeaf("line", operation.line);
|
|
138
|
+
if (args.ticksAndLabels === false) {
|
|
139
|
+
assertRemovable(program, channel, "ticks", operation.operation);
|
|
140
|
+
assertRemovable(program, channel, "labels", operation.operation);
|
|
141
|
+
plan.push(
|
|
142
|
+
{ kind: "remove", component: "ticks" },
|
|
143
|
+
{ kind: "remove", component: "labels" }
|
|
144
|
+
);
|
|
145
|
+
} else if (args.ticksAndLabels !== undefined) {
|
|
146
|
+
plan.push({
|
|
147
|
+
kind: "edit",
|
|
148
|
+
operation: operation.group,
|
|
149
|
+
args: { ...shared, ...args.ticksAndLabels }
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
addLeaf("ticks", operation.ticks);
|
|
153
|
+
addLeaf("labels", operation.labels);
|
|
154
|
+
}
|
|
155
|
+
addLeaf("title", operation.title);
|
|
156
|
+
|
|
157
|
+
if (plan.length === 0) {
|
|
158
|
+
throw new Error(`${operation.operation} requires an existing ${channel}-axis component.`);
|
|
159
|
+
}
|
|
160
|
+
return plan;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applyPlan(program, channel, plan, operation) {
|
|
164
|
+
let next = program;
|
|
165
|
+
for (const step of plan) {
|
|
166
|
+
next = step.kind === "remove"
|
|
167
|
+
? removeComponent(next, channel, step.component)
|
|
168
|
+
: next[step.operation](step.args);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const hasRetainedGraphic = Object.keys(COMPONENTS).some(
|
|
172
|
+
component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
|
|
173
|
+
);
|
|
174
|
+
if (!hasRetainedGraphic && (
|
|
175
|
+
next.semanticSpec.guides.axis?.[channel] !== undefined ||
|
|
176
|
+
next.guideConfigs.axis?.[channel] !== undefined
|
|
177
|
+
)) {
|
|
178
|
+
next = next[operation.remove]();
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
|
|
70
183
|
function makeEditAxis(channel) {
|
|
71
184
|
const operation = names(channel);
|
|
72
185
|
return action(
|
|
@@ -76,43 +189,12 @@ function makeEditAxis(channel) {
|
|
|
76
189
|
},
|
|
77
190
|
function (args = {}) {
|
|
78
191
|
validateArgs(args, operation.operation);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (args.line !== undefined || (args.position !== undefined && has(`${channel}AxisLine`))) {
|
|
86
|
-
next = next[operation.line]({ ...shared, ...(args.line ?? {}) });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (args.ticksAndLabels !== undefined) {
|
|
90
|
-
next = next[operation.group]({
|
|
91
|
-
...shared,
|
|
92
|
-
...args.ticksAndLabels
|
|
93
|
-
});
|
|
94
|
-
} else if (args.ticks !== undefined || args.labels !== undefined) {
|
|
95
|
-
if (args.ticks !== undefined) {
|
|
96
|
-
next = next[operation.ticks]({ ...shared, ...args.ticks });
|
|
97
|
-
}
|
|
98
|
-
if (args.labels !== undefined) {
|
|
99
|
-
next = next[operation.labels]({ ...shared, ...args.labels });
|
|
100
|
-
}
|
|
101
|
-
} else if (args.position !== undefined) {
|
|
102
|
-
const hasTicks = has(`${channel}AxisTicks`);
|
|
103
|
-
const hasLabels = has(`${channel}AxisLabels`);
|
|
104
|
-
if (hasTicks && hasLabels) {
|
|
105
|
-
next = next[operation.group](shared);
|
|
106
|
-
} else {
|
|
107
|
-
if (hasTicks) next = next[operation.ticks](shared);
|
|
108
|
-
if (hasLabels) next = next[operation.labels](shared);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (args.title !== undefined || (args.position !== undefined && has(`${channel}AxisTitle`))) {
|
|
113
|
-
next = next[operation.title]({ ...shared, ...(args.title ?? {}) });
|
|
114
|
-
}
|
|
115
|
-
return next;
|
|
192
|
+
const plan = buildPlan(this, channel, args, operation);
|
|
193
|
+
|
|
194
|
+
// Run the complete proposal against an immutable speculative branch so a
|
|
195
|
+
// later leaf failure cannot begin the returned action trace or state.
|
|
196
|
+
applyPlan(this, channel, plan, operation);
|
|
197
|
+
return applyPlan(this, channel, plan, operation);
|
|
116
198
|
}
|
|
117
199
|
);
|
|
118
200
|
}
|
|
@@ -360,14 +360,25 @@ export const createLegend = action(
|
|
|
360
360
|
symbol
|
|
361
361
|
});
|
|
362
362
|
if (requestedPoint.encoding?.size?.scale !== undefined) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
363
|
+
const existingSize = next.guideConfigs.legend?.size;
|
|
364
|
+
if (existingSize === undefined) {
|
|
365
|
+
next = next.createSizeLegend({
|
|
366
|
+
target: requestedPoint.id,
|
|
367
|
+
...(count === undefined ? {} : { count }),
|
|
368
|
+
inheritAppearance:
|
|
369
|
+
categoricalArgs.position === "left" ||
|
|
370
|
+
categoricalArgs.labels !== undefined ||
|
|
371
|
+
categoricalArgs.titleStyle !== undefined
|
|
372
|
+
});
|
|
373
|
+
} else if (existingSize.target !== requestedPoint.id) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"Combined point series legend requires the active size legend to share its target."
|
|
376
|
+
);
|
|
377
|
+
} else if (count !== undefined && count !== existingSize.count) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"Existing size legend count must be edited before recreating the categorical block."
|
|
380
|
+
);
|
|
381
|
+
}
|
|
371
382
|
next = next.rematerializeLegend();
|
|
372
383
|
}
|
|
373
384
|
return next;
|
|
@@ -49,7 +49,7 @@ const DEFAULT_BORDER = Object.freeze({
|
|
|
49
49
|
export const validatePositive = validatePositiveFinite;
|
|
50
50
|
export const validateNonNegative = validateNonNegativeFinite;
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
export function normalizeLegendTextOptions(value, label, defaults) {
|
|
53
53
|
if (value === undefined) return { ...defaults };
|
|
54
54
|
if (!isPlainObject(value)) {
|
|
55
55
|
throw new TypeError(`${label} must be a plain object.`);
|
|
@@ -132,12 +132,12 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
132
132
|
count,
|
|
133
133
|
title: args.title,
|
|
134
134
|
inferredTitle: args.title === undefined,
|
|
135
|
-
labels:
|
|
135
|
+
labels: normalizeLegendTextOptions(
|
|
136
136
|
args.labels,
|
|
137
137
|
"createLegend.labels",
|
|
138
138
|
DEFAULT_LABELS
|
|
139
139
|
),
|
|
140
|
-
titleStyle:
|
|
140
|
+
titleStyle: normalizeLegendTextOptions(
|
|
141
141
|
args.titleStyle,
|
|
142
142
|
"createLegend.titleStyle",
|
|
143
143
|
DEFAULT_TITLE
|