ggaction 0.0.4 → 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 +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- 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 +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -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
|
+
}
|
|
@@ -3,9 +3,22 @@ import {
|
|
|
3
3
|
validateRuleStroke,
|
|
4
4
|
validateRuleStrokeWidth
|
|
5
5
|
} from "../../grammar/ruleAppearance.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readQuantitativeField } from "../../grammar/scales/index.js";
|
|
7
|
+
import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
|
|
8
|
+
import {
|
|
9
|
+
applyEncodingScale,
|
|
10
|
+
resolveReassignmentScaleOptions,
|
|
11
|
+
resolveTarget,
|
|
12
|
+
validateOptions
|
|
13
|
+
} from "./shared.js";
|
|
14
|
+
import { applyMaterializationPlan } from "../../materialization/dependencies.js";
|
|
15
|
+
import { planEncodingRematerialization } from "../../materialization/encodings.js";
|
|
16
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
7
17
|
|
|
8
|
-
const
|
|
18
|
+
const STROKE_OPTIONS = Object.freeze(["target", "value"]);
|
|
19
|
+
const WIDTH_OPTIONS = Object.freeze([
|
|
20
|
+
"target", "value", "field", "fieldType", "scale"
|
|
21
|
+
]);
|
|
9
22
|
|
|
10
23
|
const encodeStroke = action(
|
|
11
24
|
{
|
|
@@ -13,7 +26,7 @@ const encodeStroke = action(
|
|
|
13
26
|
description: "Set a constant graphical stroke on a rule mark."
|
|
14
27
|
},
|
|
15
28
|
function (args = {}) {
|
|
16
|
-
validateOptions(args,
|
|
29
|
+
validateOptions(args, STROKE_OPTIONS, "encodeStroke");
|
|
17
30
|
const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
|
|
18
31
|
return this
|
|
19
32
|
._withMarkConfig(id, {
|
|
@@ -30,14 +43,79 @@ const encodeStrokeWidth = action(
|
|
|
30
43
|
description: "Set a constant graphical stroke width on a rule mark."
|
|
31
44
|
},
|
|
32
45
|
function (args = {}) {
|
|
33
|
-
validateOptions(args,
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
|
|
47
|
+
const hasValue = Object.hasOwn(args, "value");
|
|
48
|
+
const hasField = Object.hasOwn(args, "field");
|
|
49
|
+
if (hasValue === hasField) {
|
|
50
|
+
throw new Error("encodeStrokeWidth requires exactly one of value or field.");
|
|
51
|
+
}
|
|
52
|
+
if (hasValue) {
|
|
53
|
+
const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
|
|
54
|
+
let next = this.guideConfigs.legend?.strokeWidth?.target === id
|
|
55
|
+
? this.removeLegend({ target: id })
|
|
56
|
+
: this;
|
|
57
|
+
if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
|
|
58
|
+
next = next.editSemantic({
|
|
59
|
+
property: `layer[${id}].encoding.strokeWidth`,
|
|
60
|
+
remove: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return next
|
|
64
|
+
._withMarkConfig(id, {
|
|
65
|
+
...next.markConfigs[id],
|
|
66
|
+
strokeWidth: validateRuleStrokeWidth(args.value)
|
|
67
|
+
})
|
|
68
|
+
.rematerializeRuleMark({ id });
|
|
69
|
+
}
|
|
70
|
+
const { id, dataset, layer } = resolveTarget(
|
|
71
|
+
this,
|
|
72
|
+
args.target,
|
|
73
|
+
["rule", "line"],
|
|
74
|
+
"rule or line mark"
|
|
75
|
+
);
|
|
76
|
+
const fieldType = args.fieldType ?? "quantitative";
|
|
77
|
+
if (fieldType !== "quantitative") {
|
|
78
|
+
throw new Error("encodeStrokeWidth requires a quantitative field.");
|
|
79
|
+
}
|
|
80
|
+
const values = readQuantitativeField(dataset.values, args.field);
|
|
81
|
+
if (values.some(value => value < 0)) {
|
|
82
|
+
throw new RangeError(
|
|
83
|
+
`encodeStrokeWidth field "${args.field}" cannot contain negative values.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const previous = layer.encoding?.strokeWidth;
|
|
87
|
+
const requestedScale = resolveReassignmentScaleOptions(
|
|
88
|
+
previous,
|
|
89
|
+
args.scale ?? {}
|
|
90
|
+
);
|
|
91
|
+
const scale = resolveStrokeWidthScaleDefinition(this, requestedScale);
|
|
92
|
+
const { strokeWidth, ...config } = this.markConfigs[id] ?? {};
|
|
93
|
+
void strokeWidth;
|
|
94
|
+
let next = this
|
|
95
|
+
._withMarkConfig(id, config)
|
|
96
|
+
.editSemantic({
|
|
97
|
+
property: `layer[${id}].encoding.strokeWidth.field`,
|
|
98
|
+
value: args.field
|
|
39
99
|
})
|
|
40
|
-
.
|
|
100
|
+
.editSemantic({
|
|
101
|
+
property: `layer[${id}].encoding.strokeWidth.fieldType`,
|
|
102
|
+
value: fieldType
|
|
103
|
+
})
|
|
104
|
+
.editSemantic({
|
|
105
|
+
property: `layer[${id}].encoding.strokeWidth.scale`,
|
|
106
|
+
value: scale.id
|
|
107
|
+
});
|
|
108
|
+
next = applyEncodingScale(next, scale, requestedScale, {
|
|
109
|
+
reassignment: previous?.scale === scale.id
|
|
110
|
+
});
|
|
111
|
+
return applyMaterializationPlan(
|
|
112
|
+
next,
|
|
113
|
+
planEncodingRematerialization(next, {
|
|
114
|
+
target: id,
|
|
115
|
+
channel: "strokeWidth",
|
|
116
|
+
scale: scale.id
|
|
117
|
+
})
|
|
118
|
+
);
|
|
41
119
|
}
|
|
42
120
|
);
|
|
43
121
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import {
|
|
4
5
|
validateOptionObject,
|
|
@@ -8,12 +9,15 @@ import {
|
|
|
8
9
|
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
9
10
|
import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
|
|
10
11
|
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
12
|
+
import { planIntervalEdit } from "../data/intervalEdit.js";
|
|
11
13
|
import {
|
|
12
14
|
ERROR_BAND_BOUNDARY_OPTIONS,
|
|
13
15
|
resolveBoundaryAppearance
|
|
14
16
|
} from "./options.js";
|
|
15
17
|
|
|
16
|
-
const EDIT_OPTIONS = Object.freeze([
|
|
18
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
19
|
+
"target", "fill", "opacity", "curve", "statistics", "boundaries"
|
|
20
|
+
]);
|
|
17
21
|
const BOUNDARY_EDIT_OPTIONS = Object.freeze([
|
|
18
22
|
"target", "boundary", ...ERROR_BAND_BOUNDARY_OPTIONS
|
|
19
23
|
]);
|
|
@@ -49,13 +53,16 @@ function currentBoundaryAppearance(program, id) {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
function defaultBoundaryAppearance(program, owner) {
|
|
52
|
-
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return resolveBoundaryAppearance({}, {
|
|
57
|
+
defaults: {
|
|
58
|
+
stroke: DEFAULT_COLORS.mark,
|
|
59
|
+
strokeWidth: 1,
|
|
60
|
+
strokeDash: "solid",
|
|
61
|
+
opacity: 1,
|
|
62
|
+
curve: program.markConfigs[owner]?.curve ?? "linear"
|
|
63
|
+
},
|
|
64
|
+
operation: "editErrorBand boundaries"
|
|
65
|
+
});
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
function createBoundary(program, owner, id, bound, appearance) {
|
|
@@ -82,6 +89,34 @@ function createBoundary(program, owner, id, bound, appearance) {
|
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
function removeBoundary(program, id) {
|
|
93
|
+
if (findLayer(program, id) === undefined) return program;
|
|
94
|
+
const selectionIds = Object.entries(
|
|
95
|
+
program.materializationConfigs.selections ?? {}
|
|
96
|
+
).filter(([, config]) => config.target === id).map(([selection]) => selection);
|
|
97
|
+
let next = program;
|
|
98
|
+
for (const [highlight, config] of Object.entries(
|
|
99
|
+
program.materializationConfigs.highlights ?? {}
|
|
100
|
+
)) {
|
|
101
|
+
if (config.target === id || selectionIds.includes(config.selection)) {
|
|
102
|
+
next = next._withoutMaterializationConfig(["highlights", highlight]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const selection of selectionIds) {
|
|
106
|
+
next = next._withoutMaterializationConfig(["selections", selection]);
|
|
107
|
+
}
|
|
108
|
+
return next
|
|
109
|
+
.editSemantic({ property: `layer[${id}]`, remove: true })
|
|
110
|
+
.editGraphics({ target: id, remove: true })
|
|
111
|
+
._withoutMaterializationConfig(["marks", id])
|
|
112
|
+
._withContext({
|
|
113
|
+
...(program.context.currentMark === id ? { currentMark: undefined } : {}),
|
|
114
|
+
...(selectionIds.includes(program.context.currentSelection)
|
|
115
|
+
? { currentSelection: undefined }
|
|
116
|
+
: {})
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
export const rematerializeErrorBandBoundary = action(
|
|
86
121
|
{
|
|
87
122
|
op: "rematerializeErrorBandBoundary",
|
|
@@ -121,8 +156,9 @@ export const editErrorBand = action(
|
|
|
121
156
|
},
|
|
122
157
|
function (args = {}) {
|
|
123
158
|
validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
|
|
124
|
-
if (!["fill", "opacity", "curve"
|
|
125
|
-
|
|
159
|
+
if (!["fill", "opacity", "curve", "statistics", "boundaries"]
|
|
160
|
+
.some(key => Object.hasOwn(args, key))) {
|
|
161
|
+
throw new Error("editErrorBand requires at least one change.");
|
|
126
162
|
}
|
|
127
163
|
const owner = resolveOwner(this, args.target);
|
|
128
164
|
const config = { ...this.markConfigs[owner.id] };
|
|
@@ -136,9 +172,81 @@ export const editErrorBand = action(
|
|
|
136
172
|
if (Object.hasOwn(args, "curve")) {
|
|
137
173
|
config.curve = validateCurveInterpolation(args.curve);
|
|
138
174
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
const interval = Object.hasOwn(args, "statistics")
|
|
176
|
+
? planIntervalEdit(this, {
|
|
177
|
+
owner: owner.id,
|
|
178
|
+
data: errorBand.data,
|
|
179
|
+
consumers: [
|
|
180
|
+
owner.id,
|
|
181
|
+
...[errorBand.lowerBoundaryId, errorBand.upperBoundaryId]
|
|
182
|
+
.filter(id => findLayer(this, id) !== undefined)
|
|
183
|
+
],
|
|
184
|
+
statistics: args.statistics,
|
|
185
|
+
operation: "editErrorBand"
|
|
186
|
+
})
|
|
187
|
+
: { changed: false };
|
|
188
|
+
if (
|
|
189
|
+
Object.hasOwn(args, "boundaries") &&
|
|
190
|
+
args.boundaries !== false &&
|
|
191
|
+
!isPlainObject(args.boundaries)
|
|
192
|
+
) {
|
|
193
|
+
throw new TypeError(
|
|
194
|
+
"editErrorBand boundaries must be false or a plain object."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const applyEdit = program => {
|
|
199
|
+
let next = program;
|
|
200
|
+
if (interval.changed) {
|
|
201
|
+
next = next.createIntervalData(interval.dataArgs);
|
|
202
|
+
for (const rebind of interval.revision.rebinds) {
|
|
203
|
+
next = next.rebindLayerData(rebind);
|
|
204
|
+
}
|
|
205
|
+
errorBand.data = interval.revision.id;
|
|
206
|
+
}
|
|
207
|
+
next = next
|
|
208
|
+
._withMarkConfig(owner.id, { ...config, errorBand })
|
|
209
|
+
.rematerializeAreaMark({ id: owner.id });
|
|
210
|
+
if (Object.hasOwn(args, "boundaries")) {
|
|
211
|
+
if (args.boundaries === false) {
|
|
212
|
+
next = removeBoundary(next, errorBand.lowerBoundaryId);
|
|
213
|
+
next = removeBoundary(next, errorBand.upperBoundaryId);
|
|
214
|
+
} else if (Object.keys(args.boundaries).length === 0) {
|
|
215
|
+
for (const [id, bound] of [
|
|
216
|
+
[errorBand.lowerBoundaryId, errorBand.lowerField],
|
|
217
|
+
[errorBand.upperBoundaryId, errorBand.upperField]
|
|
218
|
+
]) {
|
|
219
|
+
next = findLayer(next, id) === undefined
|
|
220
|
+
? createBoundary(
|
|
221
|
+
next,
|
|
222
|
+
owner.id,
|
|
223
|
+
id,
|
|
224
|
+
bound,
|
|
225
|
+
defaultBoundaryAppearance(next, owner.id)
|
|
226
|
+
)
|
|
227
|
+
: interval.changed
|
|
228
|
+
? next.rematerializeLineMark({ id })
|
|
229
|
+
: next;
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
next = next.editErrorBandBoundary({
|
|
233
|
+
target: owner.id,
|
|
234
|
+
...args.boundaries
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} else if (interval.changed) {
|
|
238
|
+
for (const id of [errorBand.lowerBoundaryId, errorBand.upperBoundaryId]) {
|
|
239
|
+
if (findLayer(next, id) !== undefined) {
|
|
240
|
+
next = next.rematerializeLineMark({ id });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return interval.changed
|
|
245
|
+
? next.releaseDerivedData(interval.revision.release)
|
|
246
|
+
: next;
|
|
247
|
+
};
|
|
248
|
+
if (interval.changed || Object.hasOwn(args, "boundaries")) applyEdit(this);
|
|
249
|
+
return applyEdit(this);
|
|
142
250
|
}
|
|
143
251
|
);
|
|
144
252
|
|
|
@@ -2,7 +2,9 @@ import { action } from "../../core/action.js";
|
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
3
|
import { validateKeys } from "../../core/validation.js";
|
|
4
4
|
import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
|
|
5
|
+
import { planIntervalEdit } from "../data/intervalEdit.js";
|
|
5
6
|
import {
|
|
7
|
+
ERROR_BAR_APPEARANCE_OPTIONS,
|
|
6
8
|
ERROR_BAR_EDIT_OPTIONS,
|
|
7
9
|
resolveErrorBarAppearance
|
|
8
10
|
} from "./options.js";
|
|
@@ -112,19 +114,54 @@ export const editErrorBar = action(
|
|
|
112
114
|
validateKeys(args, ERROR_BAR_EDIT_OPTIONS, "editErrorBar");
|
|
113
115
|
const editable = ERROR_BAR_EDIT_OPTIONS.filter(option => option !== "target");
|
|
114
116
|
if (!editable.some(option => Object.hasOwn(args, option))) {
|
|
115
|
-
throw new Error("editErrorBar requires at least one
|
|
117
|
+
throw new Error("editErrorBar requires at least one change.");
|
|
116
118
|
}
|
|
117
119
|
const owner = resolveOwner(this, args.target);
|
|
118
120
|
const current = this.markConfigs[owner.id].errorBar;
|
|
119
|
-
const
|
|
121
|
+
const appearanceArgs = Object.fromEntries(
|
|
122
|
+
ERROR_BAR_APPEARANCE_OPTIONS
|
|
123
|
+
.filter(key => Object.hasOwn(args, key))
|
|
124
|
+
.map(key => [key, args[key]])
|
|
125
|
+
);
|
|
126
|
+
const appearance = resolveErrorBarAppearance(appearanceArgs, {
|
|
120
127
|
defaults: current,
|
|
121
128
|
operation: "editErrorBar"
|
|
122
129
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
const interval = Object.hasOwn(args, "statistics")
|
|
131
|
+
? planIntervalEdit(this, {
|
|
132
|
+
owner: owner.id,
|
|
133
|
+
data: current.data,
|
|
134
|
+
consumers: [
|
|
135
|
+
owner.id,
|
|
136
|
+
...[current.lowerCapId, current.upperCapId]
|
|
137
|
+
.filter(id => findLayer(this, id) !== undefined)
|
|
138
|
+
],
|
|
139
|
+
statistics: args.statistics,
|
|
140
|
+
operation: "editErrorBar"
|
|
141
|
+
})
|
|
142
|
+
: { changed: false };
|
|
143
|
+
|
|
144
|
+
const applyEdit = program => {
|
|
145
|
+
let next = program;
|
|
146
|
+
if (interval.changed) {
|
|
147
|
+
next = next.createIntervalData(interval.dataArgs);
|
|
148
|
+
for (const rebind of interval.revision.rebinds) {
|
|
149
|
+
next = next.rebindLayerData(rebind);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
next = next._withMarkConfig(owner.id, {
|
|
153
|
+
...next.markConfigs[owner.id],
|
|
154
|
+
errorBar: {
|
|
155
|
+
...current,
|
|
156
|
+
...appearance,
|
|
157
|
+
...(interval.changed ? { data: interval.revision.id } : {})
|
|
158
|
+
}
|
|
159
|
+
}).rematerializeErrorBar({ id: owner.id });
|
|
160
|
+
return interval.changed
|
|
161
|
+
? next.releaseDerivedData(interval.revision.release)
|
|
162
|
+
: next;
|
|
163
|
+
};
|
|
164
|
+
if (interval.changed) applyEdit(this);
|
|
165
|
+
return applyEdit(this);
|
|
129
166
|
}
|
|
130
167
|
);
|
|
@@ -13,9 +13,15 @@ import {
|
|
|
13
13
|
|
|
14
14
|
export const ERROR_BAR_EDIT_OPTIONS = Object.freeze([
|
|
15
15
|
"target", "caps", "capSize", "stroke", "strokeWidth",
|
|
16
|
-
"strokeDash", "opacity"
|
|
16
|
+
"strokeDash", "opacity", "statistics"
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
export const ERROR_BAR_APPEARANCE_OPTIONS = Object.freeze(
|
|
20
|
+
ERROR_BAR_EDIT_OPTIONS.filter(option =>
|
|
21
|
+
option !== "target" && option !== "statistics"
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
19
25
|
export function resolveErrorBarAppearance(args, {
|
|
20
26
|
defaults,
|
|
21
27
|
operation
|