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
|
@@ -15,6 +15,7 @@ import { planDerivedDataRevision } from
|
|
|
15
15
|
import { validateOptions } from "./shared.js";
|
|
16
16
|
import { normalizeDensityPlacement } from "../../grammar/density.js";
|
|
17
17
|
import { scaleEditPatch } from "../scales/patch.js";
|
|
18
|
+
import { removeLegendKinds } from "../guides/legends/remove.js";
|
|
18
19
|
import {
|
|
19
20
|
requireDensityField,
|
|
20
21
|
resolveDensityCategoryScaleOptions,
|
|
@@ -29,8 +30,8 @@ const OPTIONS = Object.freeze([
|
|
|
29
30
|
"valueScale", "densityScale", "placement"
|
|
30
31
|
]);
|
|
31
32
|
const EDIT_OPTIONS = Object.freeze([
|
|
32
|
-
"target", "
|
|
33
|
-
"placement"
|
|
33
|
+
"target", "source", "field", "groupBy", "bandwidth", "extent", "steps",
|
|
34
|
+
"kernel", "normalization", "placement"
|
|
34
35
|
]);
|
|
35
36
|
const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
|
|
36
37
|
|
|
@@ -190,6 +191,43 @@ function findDensityArea(program, requested) {
|
|
|
190
191
|
});
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
function densityCategoryField(layer, transform, groupBy) {
|
|
195
|
+
if (transform.placement === undefined) return undefined;
|
|
196
|
+
if (transform.placement.categoryField !== transform.groupBy) {
|
|
197
|
+
return transform.placement.categoryField;
|
|
198
|
+
}
|
|
199
|
+
return groupBy ?? `${layer.id}DensityCategory`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function assertGroupingSelectionCompatibility(program, layer, changesGroup) {
|
|
203
|
+
if (!changesGroup) return;
|
|
204
|
+
const referenced = Object.entries(
|
|
205
|
+
program.materializationConfigs.selections ?? {}
|
|
206
|
+
).find(([, selection]) =>
|
|
207
|
+
selection.target === layer.id &&
|
|
208
|
+
["group", "color"].includes(selection.selector?.channel)
|
|
209
|
+
);
|
|
210
|
+
if (referenced !== undefined) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`editDensity cannot change groupBy while selection "${referenced[0]}" ` +
|
|
213
|
+
`references ${referenced[1].selector.channel}.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function removeOwnedColorLegends(program, target) {
|
|
219
|
+
const kinds = Object.entries(program.guideConfigs.legend ?? {})
|
|
220
|
+
.filter(([kind, config]) =>
|
|
221
|
+
config?.target === target &&
|
|
222
|
+
(
|
|
223
|
+
config.channels?.includes("color") ||
|
|
224
|
+
["gradient", "interval"].includes(kind)
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
.map(([kind]) => kind);
|
|
228
|
+
return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
|
|
229
|
+
}
|
|
230
|
+
|
|
193
231
|
const editDensity = action(
|
|
194
232
|
{
|
|
195
233
|
op: "editDensity",
|
|
@@ -203,21 +241,55 @@ const editDensity = action(
|
|
|
203
241
|
const layer = findDensityArea(this, args.target);
|
|
204
242
|
const previous = findDataset(this, layer.data);
|
|
205
243
|
const transform = previous.transform[0];
|
|
244
|
+
const source = Object.hasOwn(args, "source")
|
|
245
|
+
? validateUserId(args.source, "Density source dataset id")
|
|
246
|
+
: previous.source;
|
|
247
|
+
const field = Object.hasOwn(args, "field")
|
|
248
|
+
? requireDensityField(args.field, "Density field")
|
|
249
|
+
: transform.field;
|
|
250
|
+
const groupBy = Object.hasOwn(args, "groupBy")
|
|
251
|
+
? args.groupBy === false
|
|
252
|
+
? undefined
|
|
253
|
+
: requireDensityField(args.groupBy, "Density groupBy")
|
|
254
|
+
: transform.groupBy;
|
|
255
|
+
const changesGroup = groupBy !== transform.groupBy;
|
|
256
|
+
assertGroupingSelectionCompatibility(this, layer, changesGroup);
|
|
257
|
+
const categoryField = densityCategoryField(layer, transform, groupBy);
|
|
206
258
|
const requestedPlacement = Object.hasOwn(args, "placement")
|
|
207
259
|
? normalizeDensityPlacement(args.placement, {
|
|
208
260
|
densityChannel: transform.placement?.channel ?? "x",
|
|
209
|
-
groupBy
|
|
210
|
-
categoryField:
|
|
211
|
-
transform.placement?.categoryField ??
|
|
261
|
+
groupBy,
|
|
262
|
+
categoryField: categoryField ?? groupBy ??
|
|
212
263
|
`${layer.id}DensityCategory`
|
|
213
264
|
})
|
|
214
|
-
: transform.placement
|
|
265
|
+
: transform.placement === undefined || !changesGroup
|
|
266
|
+
? transform.placement
|
|
267
|
+
: normalizeDensityPlacement({
|
|
268
|
+
type: "category",
|
|
269
|
+
...(transform.placement.side === undefined
|
|
270
|
+
? {}
|
|
271
|
+
: { side: transform.placement.side }),
|
|
272
|
+
width: transform.placement.width,
|
|
273
|
+
...(transform.placement.split === undefined
|
|
274
|
+
? {}
|
|
275
|
+
: { split: transform.placement.split })
|
|
276
|
+
}, {
|
|
277
|
+
densityChannel: transform.placement.channel,
|
|
278
|
+
groupBy,
|
|
279
|
+
categoryField
|
|
280
|
+
});
|
|
215
281
|
const colorField = layer.encoding?.color?.field;
|
|
282
|
+
const colorFollowsGroup = transform.groupBy !== undefined &&
|
|
283
|
+
colorField === transform.groupBy;
|
|
216
284
|
const availableColorFields = [
|
|
217
|
-
|
|
285
|
+
groupBy,
|
|
218
286
|
requestedPlacement?.split?.field
|
|
219
287
|
].filter(Boolean);
|
|
220
|
-
if (
|
|
288
|
+
if (
|
|
289
|
+
colorField !== undefined &&
|
|
290
|
+
!colorFollowsGroup &&
|
|
291
|
+
!availableColorFields.includes(colorField)
|
|
292
|
+
) {
|
|
221
293
|
throw new Error(
|
|
222
294
|
`editDensity placement would remove color field "${colorField}" from the density series.`
|
|
223
295
|
);
|
|
@@ -234,11 +306,11 @@ const editDensity = action(
|
|
|
234
306
|
|
|
235
307
|
const dataArgs = {
|
|
236
308
|
id: revision.id,
|
|
237
|
-
source
|
|
238
|
-
field
|
|
239
|
-
...(
|
|
309
|
+
source,
|
|
310
|
+
field,
|
|
311
|
+
...(groupBy === undefined
|
|
240
312
|
? {}
|
|
241
|
-
: { groupBy
|
|
313
|
+
: { groupBy }),
|
|
242
314
|
bandwidth: option("bandwidth"),
|
|
243
315
|
extent: option("extent"),
|
|
244
316
|
steps: option("steps"),
|
|
@@ -249,78 +321,141 @@ const editDensity = action(
|
|
|
249
321
|
? {}
|
|
250
322
|
: { placement: requestedPlacement })
|
|
251
323
|
};
|
|
252
|
-
let next = requestedPlacement === undefined
|
|
253
|
-
? this.createDensityData(dataArgs)
|
|
254
|
-
: this.createCategoricalDensityData(dataArgs);
|
|
255
|
-
next = next.rebindLayerData(revision.rebinds[0])
|
|
256
|
-
.releaseDerivedData(revision.release);
|
|
257
|
-
|
|
258
324
|
const changesPlacementMode = Object.hasOwn(args, "placement") &&
|
|
259
325
|
(transform.placement === undefined) !== (requestedPlacement === undefined);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const scaleId = layer.encoding[channel].scale;
|
|
309
|
-
const requestedScale = resolveDensityCategoryScaleOptions(
|
|
310
|
-
args.placement.scale,
|
|
311
|
-
scaleId
|
|
312
|
-
);
|
|
313
|
-
if (requestedScale.id !== scaleId) {
|
|
314
|
-
throw new Error("editDensity placement scale cannot change its id.");
|
|
326
|
+
const applyEdit = program => {
|
|
327
|
+
let next = requestedPlacement === undefined
|
|
328
|
+
? program.createDensityData(dataArgs)
|
|
329
|
+
: program.createCategoricalDensityData(dataArgs);
|
|
330
|
+
next = next.rebindLayerData(revision.rebinds[0])
|
|
331
|
+
.releaseDerivedData(revision.release);
|
|
332
|
+
|
|
333
|
+
if (changesGroup) {
|
|
334
|
+
if (groupBy === undefined) {
|
|
335
|
+
if (findLayer(next, layer.id).encoding?.group !== undefined) {
|
|
336
|
+
next = next.editSemantic({
|
|
337
|
+
property: `layer[${layer.id}].encoding.group`,
|
|
338
|
+
remove: true
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (colorFollowsGroup) {
|
|
342
|
+
next = next.editSemantic({
|
|
343
|
+
property: `layer[${layer.id}].encoding.color`,
|
|
344
|
+
remove: true
|
|
345
|
+
});
|
|
346
|
+
next = removeOwnedColorLegends(next, layer.id);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
next = next
|
|
350
|
+
.editSemantic({
|
|
351
|
+
property: `layer[${layer.id}].encoding.group.field`,
|
|
352
|
+
value: groupBy
|
|
353
|
+
})
|
|
354
|
+
.editSemantic({
|
|
355
|
+
property: `layer[${layer.id}].encoding.group.fieldType`,
|
|
356
|
+
value: "nominal"
|
|
357
|
+
});
|
|
358
|
+
if (colorFollowsGroup) {
|
|
359
|
+
next = next.editSemantic({
|
|
360
|
+
property: `layer[${layer.id}].encoding.color.field`,
|
|
361
|
+
value: groupBy
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (
|
|
366
|
+
requestedPlacement !== undefined &&
|
|
367
|
+
transform.placement?.categoryField !== requestedPlacement.categoryField
|
|
368
|
+
) {
|
|
369
|
+
next = next.editSemantic({
|
|
370
|
+
property: `layer[${layer.id}].encoding.${requestedPlacement.channel}.field`,
|
|
371
|
+
value: requestedPlacement.categoryField
|
|
372
|
+
});
|
|
373
|
+
}
|
|
315
374
|
}
|
|
316
|
-
next = next.editScale(scaleEditPatch(scaleId, requestedScale));
|
|
317
|
-
}
|
|
318
375
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
376
|
+
if (changesPlacementMode) {
|
|
377
|
+
const xScaleId = layer.encoding?.x?.scale;
|
|
378
|
+
const yScaleId = layer.encoding?.y?.scale;
|
|
379
|
+
const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
|
|
380
|
+
program,
|
|
381
|
+
layer,
|
|
382
|
+
transform,
|
|
383
|
+
requestedPlacement,
|
|
384
|
+
args.placement
|
|
385
|
+
);
|
|
386
|
+
next = next
|
|
387
|
+
.editSemantic({
|
|
388
|
+
property: `layer[${layer.id}].encoding.x`,
|
|
389
|
+
remove: true
|
|
390
|
+
})
|
|
391
|
+
.editSemantic({
|
|
392
|
+
property: `layer[${layer.id}].encoding.y`,
|
|
393
|
+
remove: true
|
|
394
|
+
});
|
|
395
|
+
next = next
|
|
396
|
+
.editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
|
|
397
|
+
.editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
|
|
398
|
+
const densityChannel = requestedPlacement?.channel ?? (
|
|
399
|
+
layer.encoding.x.field === transform.as[1] ? "x" : "y"
|
|
400
|
+
);
|
|
401
|
+
const valueChannel = requestedPlacement?.channel === "x" ? "y" : "x";
|
|
402
|
+
const definition = resolveDensityPositionDefinition({
|
|
403
|
+
layer: { densityChannel },
|
|
404
|
+
output: transform.as,
|
|
405
|
+
groupBy,
|
|
406
|
+
placement: requestedPlacement,
|
|
407
|
+
coordinate: layer.coordinate,
|
|
408
|
+
valueScale: {
|
|
409
|
+
id: valueChannel === "y" ? yScaleId : xScaleId,
|
|
410
|
+
...scaleDefinitions[valueChannel]
|
|
411
|
+
},
|
|
412
|
+
densityScale: requestedPlacement === undefined
|
|
413
|
+
? {
|
|
414
|
+
id: densityChannel === "x" ? xScaleId : yScaleId,
|
|
415
|
+
...scaleDefinitions[densityChannel]
|
|
416
|
+
}
|
|
417
|
+
: undefined,
|
|
418
|
+
placementScale: requestedPlacement === undefined
|
|
419
|
+
? undefined
|
|
420
|
+
: {
|
|
421
|
+
id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
|
|
422
|
+
...scaleDefinitions[requestedPlacement.channel]
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
next = applyDensityPosition(next, layer.id, definition, groupBy);
|
|
426
|
+
} else if (
|
|
427
|
+
requestedPlacement !== undefined &&
|
|
428
|
+
Object.hasOwn(args.placement ?? {}, "scale")
|
|
429
|
+
) {
|
|
430
|
+
const channel = requestedPlacement.channel;
|
|
431
|
+
const scaleId = layer.encoding[channel].scale;
|
|
432
|
+
const requestedScale = resolveDensityCategoryScaleOptions(
|
|
433
|
+
args.placement.scale,
|
|
434
|
+
scaleId
|
|
435
|
+
);
|
|
436
|
+
if (requestedScale.id !== scaleId) {
|
|
437
|
+
throw new Error("editDensity placement scale cannot change its id.");
|
|
438
|
+
}
|
|
439
|
+
next = next.editScale(scaleEditPatch(scaleId, requestedScale));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
next = applyMaterializationPlan(
|
|
443
|
+
next,
|
|
444
|
+
planDensityRematerialization(next, layer.id)
|
|
445
|
+
);
|
|
446
|
+
if (
|
|
447
|
+
changesGroup &&
|
|
448
|
+
colorFollowsGroup &&
|
|
449
|
+
groupBy !== undefined &&
|
|
450
|
+
Object.values(next.guideConfigs.legend ?? {})
|
|
451
|
+
.some(config => config?.target === layer.id)
|
|
452
|
+
) {
|
|
453
|
+
next = next.rematerializeLegend();
|
|
454
|
+
}
|
|
455
|
+
return next;
|
|
456
|
+
};
|
|
457
|
+
applyEdit(this);
|
|
458
|
+
return applyEdit(this);
|
|
324
459
|
}
|
|
325
460
|
);
|
|
326
461
|
|
|
@@ -12,6 +12,7 @@ import { registerStrokeDashEncodingActions } from "./strokeDash.js";
|
|
|
12
12
|
import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
|
|
13
13
|
import { registerTextEncodingAction } from "./text.js";
|
|
14
14
|
import { registerParallelEncodingAction } from "./parallel.js";
|
|
15
|
+
import { registerEncodingRemovalAction } from "./remove.js";
|
|
15
16
|
|
|
16
17
|
export function registerEncodingActions(ProgramClass) {
|
|
17
18
|
registerPositionEncodingActions(ProgramClass);
|
|
@@ -28,4 +29,5 @@ export function registerEncodingActions(ProgramClass) {
|
|
|
28
29
|
registerRuleAppearanceEncodingActions(ProgramClass);
|
|
29
30
|
registerTextEncodingAction(ProgramClass);
|
|
30
31
|
registerParallelEncodingAction(ProgramClass);
|
|
32
|
+
registerEncodingRemovalAction(ProgramClass);
|
|
31
33
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
getPositionChannelDefinition,
|
|
6
|
+
POSITION_ENCODING_CHANNELS
|
|
7
|
+
} from "../../core/vocabulary.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveBarChannels,
|
|
10
|
+
resolveBarColorLayout,
|
|
11
|
+
resolveBarOffsetChannel
|
|
12
|
+
} from "../../grammar/bars/policy.js";
|
|
13
|
+
import {
|
|
14
|
+
getMarkMaterializationStep,
|
|
15
|
+
getSourceDependentMarkSteps
|
|
16
|
+
} from "../../materialization/marks/index.js";
|
|
17
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
+
import { removeLegendKinds } from "../guides/legends/remove.js";
|
|
19
|
+
|
|
20
|
+
const OPTIONS = Object.freeze(["target", "channel"]);
|
|
21
|
+
const REMOVABLE_CHANNELS = Object.freeze([
|
|
22
|
+
"x", "y", "x2", "y2", "xOffset", "yOffset", "theta", "radius",
|
|
23
|
+
"color", "strokeDash", "strokeWidth", "size", "shape", "group",
|
|
24
|
+
"opacity", "text"
|
|
25
|
+
]);
|
|
26
|
+
const REMOVE_AXIS = Object.freeze({
|
|
27
|
+
x: "removeXAxis",
|
|
28
|
+
y: "removeYAxis",
|
|
29
|
+
theta: "removeThetaAxis",
|
|
30
|
+
radius: "removeRadialAxis"
|
|
31
|
+
});
|
|
32
|
+
const SPECIALIZED_LEGEND_KIND = Object.freeze({
|
|
33
|
+
color: Object.freeze(["gradient", "interval"]),
|
|
34
|
+
size: Object.freeze(["size"]),
|
|
35
|
+
opacity: Object.freeze(["opacity"]),
|
|
36
|
+
strokeWidth: Object.freeze(["strokeWidth"])
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function resolveTarget(program, requested, channel) {
|
|
40
|
+
const candidates = program.semanticSpec.layers.filter(
|
|
41
|
+
layer => layer.encoding?.[channel] !== undefined
|
|
42
|
+
);
|
|
43
|
+
if (requested !== undefined) {
|
|
44
|
+
const id = validateUserId(requested, "Encoding target id");
|
|
45
|
+
const layer = findLayer(program, id);
|
|
46
|
+
if (layer === undefined) {
|
|
47
|
+
throw new Error(`Unknown encoding target "${id}".`);
|
|
48
|
+
}
|
|
49
|
+
if (layer.encoding?.[channel] === undefined) {
|
|
50
|
+
throw new Error(`Mark "${id}" has no ${channel} encoding.`);
|
|
51
|
+
}
|
|
52
|
+
return layer;
|
|
53
|
+
}
|
|
54
|
+
const current = candidates.find(
|
|
55
|
+
layer => layer.id === program.context.currentMark
|
|
56
|
+
);
|
|
57
|
+
if (current !== undefined) return current;
|
|
58
|
+
if (candidates.length === 1) return candidates[0];
|
|
59
|
+
if (candidates.length === 0) {
|
|
60
|
+
throw new Error(`removeEncoding requires an active ${channel} encoding.`);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`removeEncoding ${channel} target is ambiguous; provide target.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function activeCascade(layer, channel) {
|
|
66
|
+
const channels = new Set([channel]);
|
|
67
|
+
if (channel === "x") {
|
|
68
|
+
channels.add("x2");
|
|
69
|
+
channels.add("xOffset");
|
|
70
|
+
}
|
|
71
|
+
if (channel === "y") {
|
|
72
|
+
channels.add("y2");
|
|
73
|
+
channels.add("yOffset");
|
|
74
|
+
}
|
|
75
|
+
if (channel === "color" && layer.mark?.type === "bar") {
|
|
76
|
+
if (resolveBarColorLayout(layer) === "group") {
|
|
77
|
+
channels.add(resolveBarOffsetChannel(layer));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
channel === "group" &&
|
|
82
|
+
layer.mark?.type === "area" &&
|
|
83
|
+
layer.encoding?.color?.field !== undefined &&
|
|
84
|
+
layer.encoding.color.field === layer.encoding.group?.field
|
|
85
|
+
) {
|
|
86
|
+
channels.add("color");
|
|
87
|
+
}
|
|
88
|
+
return [...channels].filter(active => layer.encoding?.[active] !== undefined);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertSelectionCompatibility(program, target, channels) {
|
|
92
|
+
for (const [id, selection] of Object.entries(
|
|
93
|
+
program.materializationConfigs.selections ?? {}
|
|
94
|
+
)) {
|
|
95
|
+
if (
|
|
96
|
+
selection.target === target &&
|
|
97
|
+
channels.includes(selection.selector?.channel)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function removeChannelConfigs(program, target, channels) {
|
|
107
|
+
let next = program;
|
|
108
|
+
for (const channel of channels) {
|
|
109
|
+
if (["xOffset", "yOffset"].includes(channel)) {
|
|
110
|
+
next = next._withoutMaterializationConfig(["marks", target, channel]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function restoreBarBaseline(program, layer, channels) {
|
|
117
|
+
if (layer.mark?.type !== "bar" || !channels.includes("color")) {
|
|
118
|
+
return program;
|
|
119
|
+
}
|
|
120
|
+
const measureChannel = resolveBarChannels(layer)?.measure;
|
|
121
|
+
if (
|
|
122
|
+
measureChannel === undefined ||
|
|
123
|
+
layer.encoding?.[measureChannel]?.stack !== "normalize"
|
|
124
|
+
) {
|
|
125
|
+
return program;
|
|
126
|
+
}
|
|
127
|
+
return program.editSemantic({
|
|
128
|
+
property: `layer[${layer.id}].encoding.${measureChannel}.stack`,
|
|
129
|
+
value: "zero"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasPrimaryScaleConsumer(program, channel, scale) {
|
|
134
|
+
return program.semanticSpec.layers.some(
|
|
135
|
+
layer => layer.encoding?.[channel]?.scale === scale
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cleanupPositionGuides(program, removed) {
|
|
140
|
+
let next = program;
|
|
141
|
+
for (const { channel, scale } of removed) {
|
|
142
|
+
const definition = getPositionChannelDefinition(channel);
|
|
143
|
+
if (
|
|
144
|
+
definition?.guideChannel === undefined ||
|
|
145
|
+
hasPrimaryScaleConsumer(next, channel, scale)
|
|
146
|
+
) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const axis = next.semanticSpec.guides.axis?.[definition.guideChannel];
|
|
150
|
+
if (axis?.scale === scale) {
|
|
151
|
+
next = next[REMOVE_AXIS[definition.guideChannel]]({
|
|
152
|
+
scale,
|
|
153
|
+
...(axis.coordinate === undefined ? {} : { coordinate: axis.coordinate })
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const grid = next.semanticSpec.guides.grid?.[definition.gridDirection];
|
|
157
|
+
if (grid?.scale === scale) {
|
|
158
|
+
next = next.removeGrid({ [definition.gridDirection]: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function reconcileCategoricalLegend(program, target, channels) {
|
|
165
|
+
const entry = ["series", "color"]
|
|
166
|
+
.map(kind => [kind, program.guideConfigs.legend?.[kind]])
|
|
167
|
+
.find(([, config]) =>
|
|
168
|
+
config?.target === target &&
|
|
169
|
+
config.channels.some(channel => channels.includes(channel))
|
|
170
|
+
);
|
|
171
|
+
if (entry === undefined) return program;
|
|
172
|
+
const [kind, config] = entry;
|
|
173
|
+
const layer = findLayer(program, target);
|
|
174
|
+
const remaining = config.channels.filter(
|
|
175
|
+
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
176
|
+
);
|
|
177
|
+
if (remaining.length === 0) {
|
|
178
|
+
return removeLegendKinds(program, [kind]);
|
|
179
|
+
}
|
|
180
|
+
const next = removeLegendKinds(program, [kind]);
|
|
181
|
+
return next.createLegend({
|
|
182
|
+
target,
|
|
183
|
+
channels: remaining,
|
|
184
|
+
position: config.position,
|
|
185
|
+
align: config.align,
|
|
186
|
+
direction: config.direction,
|
|
187
|
+
...(config.columns === undefined ? {} : { columns: config.columns }),
|
|
188
|
+
offset: config.offset,
|
|
189
|
+
titlePosition: config.titlePosition,
|
|
190
|
+
...(config.inferredTitle ? {} : { title: config.title }),
|
|
191
|
+
symbol: config.symbol,
|
|
192
|
+
labels: config.labels,
|
|
193
|
+
titleStyle: config.titleStyle,
|
|
194
|
+
itemGap: config.itemGap,
|
|
195
|
+
border: config.border
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cleanupLegends(program, target, channels) {
|
|
200
|
+
let next = reconcileCategoricalLegend(program, target, channels);
|
|
201
|
+
const kinds = [...new Set(channels.flatMap(
|
|
202
|
+
channel => SPECIALIZED_LEGEND_KIND[channel] ?? []
|
|
203
|
+
))].filter(kind => next.guideConfigs.legend?.[kind]?.target === target);
|
|
204
|
+
return kinds.length === 0 ? next : removeLegendKinds(next, kinds);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function clearGraphic(program, target) {
|
|
208
|
+
const graphic = program.graphicSpec.objects[target];
|
|
209
|
+
if (graphic === undefined) return program;
|
|
210
|
+
return graphic.type === "collection"
|
|
211
|
+
? program.editGraphics({ target, property: "items", value: [] })
|
|
212
|
+
: program.editGraphics({ target, property: "length", value: 0 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function rematerializeTarget(program, target) {
|
|
216
|
+
const layer = findLayer(program, target);
|
|
217
|
+
const step = getMarkMaterializationStep(program, layer);
|
|
218
|
+
const baseline = clearGraphic(program, target);
|
|
219
|
+
let next = step === undefined
|
|
220
|
+
? baseline
|
|
221
|
+
: baseline[step.op](step.args);
|
|
222
|
+
for (const dependent of getSourceDependentMarkSteps(next, target)) {
|
|
223
|
+
next = next[dependent.op](dependent.args);
|
|
224
|
+
}
|
|
225
|
+
return next;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const removeEncoding = action(
|
|
229
|
+
{
|
|
230
|
+
op: "removeEncoding",
|
|
231
|
+
description: "Remove one semantic encoding and its owned materialized state."
|
|
232
|
+
},
|
|
233
|
+
function (args = {}) {
|
|
234
|
+
validateOptionObject(args, OPTIONS, "removeEncoding");
|
|
235
|
+
if (!REMOVABLE_CHANNELS.includes(args.channel)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Unsupported removable encoding channel "${args.channel}".`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const layer = resolveTarget(this, args.target, args.channel);
|
|
241
|
+
const channels = activeCascade(layer, args.channel);
|
|
242
|
+
assertSelectionCompatibility(this, layer.id, channels);
|
|
243
|
+
const removedPositions = channels
|
|
244
|
+
.filter(channel => POSITION_ENCODING_CHANNELS.includes(channel))
|
|
245
|
+
.map(channel => ({
|
|
246
|
+
channel,
|
|
247
|
+
scale: layer.encoding[channel].scale
|
|
248
|
+
}))
|
|
249
|
+
.filter(item => item.scale !== undefined);
|
|
250
|
+
|
|
251
|
+
let next = this;
|
|
252
|
+
for (const channel of channels) {
|
|
253
|
+
next = next.editSemantic({
|
|
254
|
+
property: `layer[${layer.id}].encoding.${channel}`,
|
|
255
|
+
remove: true
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
next = removeChannelConfigs(next, layer.id, channels);
|
|
259
|
+
next = restoreBarBaseline(next, layer, channels);
|
|
260
|
+
next = cleanupLegends(next, layer.id, channels);
|
|
261
|
+
next = cleanupPositionGuides(next, removedPositions);
|
|
262
|
+
return rematerializeTarget(next, layer.id);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
export function registerEncodingRemovalAction(ProgramClass) {
|
|
267
|
+
ProgramClass.prototype.removeEncoding = removeEncoding;
|
|
268
|
+
}
|