ggaction 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +77 -14
- package/package.json +19 -2
- package/src/BasicChartProgram.js +10 -0
- package/src/actions/basic.js +21 -0
- package/src/actions/boxPlots/edit.js +455 -3
- package/src/actions/boxPlots/options.js +3 -3
- package/src/actions/canvas/actions.js +42 -0
- package/src/actions/canvas/index.js +5 -1
- package/src/actions/charts/basic.js +13 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/data/basic.js +11 -0
- package/src/actions/data/basicBin2d.js +43 -0
- package/src/actions/data/bin2d.js +138 -40
- package/src/actions/data/bin2dMaterialize.js +29 -0
- package/src/actions/data/index.js +6 -1
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +41 -3
- package/src/actions/encodings/basic.js +21 -0
- package/src/actions/encodings/density.js +215 -80
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/index.js +5 -1
- package/src/actions/encodings/ranged.js +6 -2
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +136 -1
- package/src/actions/gradientPlots/edit.js +352 -4
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +10 -2
- package/src/actions/guides/basic.js +19 -0
- package/src/actions/guides/grids/grid.js +7 -3
- package/src/actions/guides/grids/index.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +19 -8
- package/src/actions/guides/legends/categorical/index.js +6 -2
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/edit.js +61 -3
- package/src/actions/guides/legends/remove.js +124 -23
- package/src/actions/guides/legends/strokeWidth.js +42 -14
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/bar/index.js +5 -1
- package/src/actions/marks/basic.js +11 -0
- package/src/actions/marks/line/actions.js +5 -1
- package/src/actions/marks/line/index.js +4 -1
- package/src/actions/marks/point/edit.js +15 -3
- package/src/actions/marks/point/index.js +5 -1
- package/src/actions/marks/point/materialize.js +9 -3
- package/src/actions/marks/rect/actions.js +5 -1
- package/src/actions/marks/rect/index.js +4 -1
- package/src/actions/marks/rule/actions.js +4 -1
- package/src/actions/regression/edit.js +235 -74
- package/src/actions/scales/index.js +5 -1
- package/src/actions/selection/actions.js +122 -2
- package/src/actions/selection/index.js +6 -0
- package/src/basic.js +2 -0
- package/src/core/vocabulary.js +7 -0
- package/src/renderers/canvas/index.js +96 -60
- package/src/renderers/canvas/text.js +3 -14
- package/src/renderers/pdf.js +112 -0
- package/src/renderers/svg.js +559 -0
- package/src/renderers/text.js +11 -0
- package/types/basic.d.ts +107 -0
- package/types/index.d.ts +1 -0
- package/types/pdf.d.ts +26 -0
- package/types/program.d.ts +71 -7
- package/types/svg.d.ts +11 -0
|
@@ -162,7 +162,11 @@ const editRectMark = action(
|
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
export function registerRectMarkActions(ProgramClass) {
|
|
165
|
-
ProgramClass.prototype.createRectMark = createRectMark;
|
|
166
165
|
ProgramClass.prototype.editRectMark = editRectMark;
|
|
166
|
+
registerBasicRectMarkActions(ProgramClass);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function registerBasicRectMarkActions(ProgramClass) {
|
|
170
|
+
ProgramClass.prototype.createRectMark = createRectMark;
|
|
167
171
|
ProgramClass.prototype.rematerializeRectMark = rematerializeRectMark;
|
|
168
172
|
}
|
|
@@ -238,7 +238,10 @@ const rematerializeRuleMark = action(
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
const resolvedConfig =
|
|
241
|
+
const resolvedConfig = {
|
|
242
|
+
...DEFAULT_RULE_CONFIG,
|
|
243
|
+
...resolved.markConfigs[id]
|
|
244
|
+
};
|
|
242
245
|
const dashEncoding = layer.encoding?.strokeDash;
|
|
243
246
|
const opacityEncoding = layer.encoding?.opacity;
|
|
244
247
|
const strokeWidthEncoding = layer.encoding?.strokeWidth;
|
|
@@ -12,12 +12,12 @@ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
|
12
12
|
import { planDerivedDataRevision } from
|
|
13
13
|
"../../materialization/dataProvenance.js";
|
|
14
14
|
import { findLayer } from "../../selectors/layers.js";
|
|
15
|
+
import { removeLegendKinds } from "../guides/legends/remove.js";
|
|
16
|
+
import { requireRegressionField } from "./resolve.js";
|
|
15
17
|
|
|
16
18
|
const OPTIONS = Object.freeze([
|
|
17
|
-
"target", "
|
|
18
|
-
|
|
19
|
-
const STATISTICAL_OPTIONS = Object.freeze([
|
|
20
|
-
"method", "degree", "span", "confidence", "interval"
|
|
19
|
+
"target", "data", "x", "y", "groupBy", "method", "degree", "span",
|
|
20
|
+
"confidence", "interval", "band", "line"
|
|
21
21
|
]);
|
|
22
22
|
const BAND_OPTIONS = Object.freeze([
|
|
23
23
|
"color", "opacity", "stroke", "strokeWidth", "curve"
|
|
@@ -118,11 +118,137 @@ function resolveParameters(previous, args) {
|
|
|
118
118
|
return normalizeRegressionParameters(raw);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function removeOwnedColorLegends(program, target) {
|
|
122
|
+
const kinds = Object.entries(program.guideConfigs.legend ?? {})
|
|
123
|
+
.filter(([kind, config]) =>
|
|
124
|
+
config?.target === target &&
|
|
125
|
+
(
|
|
126
|
+
config.channels?.includes("color") ||
|
|
127
|
+
["gradient", "interval"].includes(kind)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
.map(([kind]) => kind);
|
|
131
|
+
return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
function removeBand(program, id) {
|
|
122
|
-
|
|
135
|
+
const selectionIds = Object.entries(
|
|
136
|
+
program.materializationConfigs.selections ?? {}
|
|
137
|
+
).filter(([, config]) => config.target === id).map(([selection]) => selection);
|
|
138
|
+
let next = program;
|
|
139
|
+
for (const [highlight, config] of Object.entries(
|
|
140
|
+
program.materializationConfigs.highlights ?? {}
|
|
141
|
+
)) {
|
|
142
|
+
if (config.target === id || selectionIds.includes(config.selection)) {
|
|
143
|
+
next = next._withoutMaterializationConfig(["highlights", highlight]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const selection of selectionIds) {
|
|
147
|
+
next = next._withoutMaterializationConfig(["selections", selection]);
|
|
148
|
+
}
|
|
149
|
+
return next
|
|
123
150
|
.editSemantic({ property: `layer[${id}]`, remove: true })
|
|
124
151
|
.editGraphics({ target: id, remove: true })
|
|
125
|
-
._withoutMaterializationConfig(["marks", id])
|
|
152
|
+
._withoutMaterializationConfig(["marks", id])
|
|
153
|
+
._withContext({
|
|
154
|
+
...(program.context.currentMark === id ? { currentMark: undefined } : {}),
|
|
155
|
+
...(selectionIds.includes(program.context.currentSelection)
|
|
156
|
+
? { currentSelection: undefined }
|
|
157
|
+
: {})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function regressionColorScale(owner, current, groupBy) {
|
|
162
|
+
if (groupBy === undefined || groupBy === current.groupBy) {
|
|
163
|
+
return current.colorScale;
|
|
164
|
+
}
|
|
165
|
+
return owner.encoding?.color?.field === groupBy
|
|
166
|
+
? owner.encoding.color.scale
|
|
167
|
+
: `${owner.id}RegressionColor`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function updateRegressionLineSemantics(program, current, {
|
|
171
|
+
x,
|
|
172
|
+
y,
|
|
173
|
+
groupBy,
|
|
174
|
+
colorScale
|
|
175
|
+
}) {
|
|
176
|
+
let next = program
|
|
177
|
+
.editSemantic({
|
|
178
|
+
property: `layer[${current.lineId}].encoding.x.field`,
|
|
179
|
+
value: x
|
|
180
|
+
})
|
|
181
|
+
.editSemantic({
|
|
182
|
+
property: `layer[${current.lineId}].encoding.y.field`,
|
|
183
|
+
value: y
|
|
184
|
+
});
|
|
185
|
+
if (groupBy === undefined) {
|
|
186
|
+
if (findLayer(next, current.lineId).encoding?.group !== undefined) {
|
|
187
|
+
next = next.editSemantic({
|
|
188
|
+
property: `layer[${current.lineId}].encoding.group`,
|
|
189
|
+
remove: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (findLayer(next, current.lineId).encoding?.color !== undefined) {
|
|
193
|
+
next = next.editSemantic({
|
|
194
|
+
property: `layer[${current.lineId}].encoding.color`,
|
|
195
|
+
remove: true
|
|
196
|
+
});
|
|
197
|
+
next = removeOwnedColorLegends(next, current.lineId);
|
|
198
|
+
}
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
201
|
+
next = findLayer(next, current.lineId).encoding?.group === undefined
|
|
202
|
+
? next.encodeGroup({ target: current.lineId, field: groupBy })
|
|
203
|
+
: next.editSemantic({
|
|
204
|
+
property: `layer[${current.lineId}].encoding.group.field`,
|
|
205
|
+
value: groupBy
|
|
206
|
+
});
|
|
207
|
+
const color = findLayer(next, current.lineId).encoding?.color;
|
|
208
|
+
if (color === undefined || color.scale !== colorScale) {
|
|
209
|
+
next = next.encodeColor({
|
|
210
|
+
target: current.lineId,
|
|
211
|
+
field: groupBy,
|
|
212
|
+
scale: { id: colorScale }
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
next = next.editSemantic({
|
|
216
|
+
property: `layer[${current.lineId}].encoding.color.field`,
|
|
217
|
+
value: groupBy
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateRegressionBandSemantics(program, bandId, { x, groupBy }) {
|
|
224
|
+
if (bandId === undefined) return program;
|
|
225
|
+
let next = program.editSemantic({
|
|
226
|
+
property: `layer[${bandId}].encoding.x.field`,
|
|
227
|
+
value: x
|
|
228
|
+
});
|
|
229
|
+
const layer = findLayer(next, bandId);
|
|
230
|
+
if (groupBy === undefined && layer.encoding?.group !== undefined) {
|
|
231
|
+
next = next.editSemantic({
|
|
232
|
+
property: `layer[${bandId}].encoding.group`,
|
|
233
|
+
remove: true
|
|
234
|
+
});
|
|
235
|
+
} else if (groupBy !== undefined) {
|
|
236
|
+
next = layer.encoding?.group === undefined
|
|
237
|
+
? next.encodeGroup({ target: bandId, field: groupBy })
|
|
238
|
+
: next.editSemantic({
|
|
239
|
+
property: `layer[${bandId}].encoding.group.field`,
|
|
240
|
+
value: groupBy
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const config = next.markConfigs[bandId];
|
|
244
|
+
return next._withMarkConfig(bandId, {
|
|
245
|
+
...config,
|
|
246
|
+
errorBand: {
|
|
247
|
+
...config.errorBand,
|
|
248
|
+
position: { ...config.errorBand.position, field: x },
|
|
249
|
+
groupBy
|
|
250
|
+
}
|
|
251
|
+
});
|
|
126
252
|
}
|
|
127
253
|
|
|
128
254
|
export const editRegression = action(
|
|
@@ -137,10 +263,27 @@ export const editRegression = action(
|
|
|
137
263
|
}
|
|
138
264
|
const owner = resolveRegressionOwner(this, args.target);
|
|
139
265
|
const current = this.markConfigs[owner.id].regression;
|
|
266
|
+
const source = Object.hasOwn(args, "data")
|
|
267
|
+
? validateUserId(args.data, "Regression data id")
|
|
268
|
+
: current.source;
|
|
269
|
+
const x = Object.hasOwn(args, "x")
|
|
270
|
+
? requireRegressionField(args.x, "Regression x")
|
|
271
|
+
: current.x;
|
|
272
|
+
const y = Object.hasOwn(args, "y")
|
|
273
|
+
? requireRegressionField(args.y, "Regression y")
|
|
274
|
+
: current.y;
|
|
275
|
+
const groupBy = Object.hasOwn(args, "groupBy")
|
|
276
|
+
? args.groupBy === false
|
|
277
|
+
? undefined
|
|
278
|
+
: requireRegressionField(args.groupBy, "Regression groupBy")
|
|
279
|
+
: current.groupBy;
|
|
280
|
+
const colorScale = regressionColorScale(owner, current, groupBy);
|
|
140
281
|
const parameters = resolveParameters(current.parameters, args);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
282
|
+
const changesProvenance = source !== current.source ||
|
|
283
|
+
x !== current.x || y !== current.y || groupBy !== current.groupBy;
|
|
284
|
+
const changesParameters = JSON.stringify(parameters) !==
|
|
285
|
+
JSON.stringify(current.parameters);
|
|
286
|
+
const changesStatistics = changesProvenance || changesParameters;
|
|
144
287
|
const bandPatch = Object.hasOwn(args, "band")
|
|
145
288
|
? validateBandPatch(args.band)
|
|
146
289
|
: undefined;
|
|
@@ -156,8 +299,6 @@ export const editRegression = action(
|
|
|
156
299
|
(bandPatch === false
|
|
157
300
|
? false
|
|
158
301
|
: hadBand || bandPatch !== undefined || current.parameters.method === "loess");
|
|
159
|
-
let dataId = current.dataId;
|
|
160
|
-
let next = this;
|
|
161
302
|
const revision = changesStatistics
|
|
162
303
|
? planDerivedDataRevision(this, {
|
|
163
304
|
owner: owner.id,
|
|
@@ -170,76 +311,96 @@ export const editRegression = action(
|
|
|
170
311
|
})
|
|
171
312
|
: undefined;
|
|
172
313
|
|
|
173
|
-
|
|
174
|
-
dataId =
|
|
175
|
-
next =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
314
|
+
const applyEdit = program => {
|
|
315
|
+
let dataId = current.dataId;
|
|
316
|
+
let next = program;
|
|
317
|
+
if (changesStatistics) {
|
|
318
|
+
dataId = revision.id;
|
|
319
|
+
next = next.createRegressionData({
|
|
320
|
+
id: dataId,
|
|
321
|
+
source,
|
|
322
|
+
x,
|
|
323
|
+
y,
|
|
324
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
325
|
+
...parameters
|
|
326
|
+
});
|
|
327
|
+
for (const rebind of revision.rebinds) {
|
|
328
|
+
next = next.rebindLayerData(rebind);
|
|
329
|
+
}
|
|
185
330
|
}
|
|
186
|
-
}
|
|
187
331
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
} else if (hadBand && changesStatistics) {
|
|
207
|
-
const bandConfig = next.markConfigs[bandId];
|
|
208
|
-
next = next._withMarkConfig(bandId, {
|
|
209
|
-
...bandConfig,
|
|
210
|
-
errorBand: { ...bandConfig.errorBand, data: dataId }
|
|
332
|
+
let bandId = current.bandId;
|
|
333
|
+
if (hadBand && !wantsBand) {
|
|
334
|
+
next = removeBand(next, current.bandId);
|
|
335
|
+
bandId = undefined;
|
|
336
|
+
} else if (!hadBand && wantsBand) {
|
|
337
|
+
bandId = `${owner.id}RegressionBands`;
|
|
338
|
+
next = next.createRegressionBand({
|
|
339
|
+
id: bandId,
|
|
340
|
+
data: dataId,
|
|
341
|
+
x,
|
|
342
|
+
lower: "__regression_ci_lower",
|
|
343
|
+
upper: "__regression_ci_upper",
|
|
344
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
345
|
+
coordinate: current.coordinate,
|
|
346
|
+
xScale: current.xScale,
|
|
347
|
+
yScale: current.yScale,
|
|
348
|
+
...(bandPatch === undefined || bandPatch === false ? {} : bandPatch)
|
|
211
349
|
});
|
|
212
|
-
|
|
350
|
+
} else if (hadBand && changesStatistics) {
|
|
351
|
+
const bandConfig = next.markConfigs[bandId];
|
|
352
|
+
next = next._withMarkConfig(bandId, {
|
|
353
|
+
...bandConfig,
|
|
354
|
+
errorBand: { ...bandConfig.errorBand, data: dataId }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
213
357
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
next = next
|
|
358
|
+
if (changesProvenance) {
|
|
359
|
+
next = updateRegressionLineSemantics(next, current, {
|
|
360
|
+
x,
|
|
361
|
+
y,
|
|
362
|
+
groupBy,
|
|
363
|
+
colorScale
|
|
364
|
+
});
|
|
365
|
+
next = updateRegressionBandSemantics(next, bandId, { x, groupBy });
|
|
366
|
+
}
|
|
367
|
+
if (bandId !== undefined) {
|
|
368
|
+
if (
|
|
369
|
+
bandPatch !== undefined &&
|
|
370
|
+
bandPatch !== false &&
|
|
371
|
+
Object.keys(bandPatch).length > 0 &&
|
|
372
|
+
hadBand
|
|
373
|
+
) {
|
|
374
|
+
next = next.editRegressionBand({ target: bandId, ...bandPatch });
|
|
375
|
+
} else if (changesStatistics) {
|
|
376
|
+
next = next.rematerializeAreaMark({ id: bandId });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
|
|
380
|
+
next = next.editRegressionLine({ target: current.lineId, ...linePatch });
|
|
222
381
|
} else if (changesStatistics) {
|
|
223
|
-
next = next.
|
|
382
|
+
next = next.rematerializeLineMark({ id: current.lineId });
|
|
224
383
|
}
|
|
225
|
-
}
|
|
226
|
-
if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
|
|
227
|
-
next = next.editRegressionLine({ target: current.lineId, ...linePatch });
|
|
228
|
-
} else if (changesStatistics) {
|
|
229
|
-
next = next.rematerializeLineMark({ id: current.lineId });
|
|
230
|
-
}
|
|
231
384
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
return next._withMarkConfig(owner.id, {
|
|
236
|
-
...next.markConfigs[owner.id],
|
|
237
|
-
regression: {
|
|
238
|
-
...current,
|
|
239
|
-
dataId,
|
|
240
|
-
bandId,
|
|
241
|
-
parameters
|
|
385
|
+
if (changesStatistics) {
|
|
386
|
+
next = next.releaseDerivedData(revision.release);
|
|
242
387
|
}
|
|
243
|
-
|
|
388
|
+
return next._withMarkConfig(owner.id, {
|
|
389
|
+
...next.markConfigs[owner.id],
|
|
390
|
+
regression: {
|
|
391
|
+
...current,
|
|
392
|
+
source,
|
|
393
|
+
x,
|
|
394
|
+
y,
|
|
395
|
+
groupBy,
|
|
396
|
+
colorScale,
|
|
397
|
+
dataId,
|
|
398
|
+
bandId,
|
|
399
|
+
parameters
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
if (changesStatistics || hadBand !== wantsBand) applyEdit(this);
|
|
404
|
+
return applyEdit(this);
|
|
244
405
|
}
|
|
245
406
|
);
|
|
@@ -4,8 +4,12 @@ import { rematerializeScale } from "./materialize.js";
|
|
|
4
4
|
import { setQuantitativeColorScale } from "./quantitativeColor.js";
|
|
5
5
|
|
|
6
6
|
export function registerScaleActions(ProgramClass) {
|
|
7
|
-
ProgramClass.prototype.createScale = createScale;
|
|
8
7
|
ProgramClass.prototype.editScale = editScale;
|
|
8
|
+
registerBasicScaleActions(ProgramClass);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerBasicScaleActions(ProgramClass) {
|
|
12
|
+
ProgramClass.prototype.createScale = createScale;
|
|
9
13
|
ProgramClass.prototype.rematerializeScale = rematerializeScale;
|
|
10
14
|
ProgramClass.prototype.setQuantitativeColorScale = setQuantitativeColorScale;
|
|
11
15
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
findSelectionPolicy,
|
|
18
18
|
requireSelectionPolicy
|
|
19
19
|
} from "../../materialization/selection/policies/index.js";
|
|
20
|
+
import { legendGraphicIds } from "../../materialization/guides/resources.js";
|
|
20
21
|
import {
|
|
21
22
|
normalizeDimOthers,
|
|
22
23
|
validateUnitInterval
|
|
@@ -27,6 +28,8 @@ const SELECTOR_KEYS = Object.freeze([
|
|
|
27
28
|
"inclusive", "count", "groupBy", "ties"
|
|
28
29
|
]);
|
|
29
30
|
const SELECT_OPTIONS = Object.freeze(["id", "target", ...SELECTOR_KEYS]);
|
|
31
|
+
const EDIT_SELECTION_OPTIONS = Object.freeze(["selection", ...SELECTOR_KEYS]);
|
|
32
|
+
const REMOVE_SELECTION_OPTIONS = Object.freeze(["selection"]);
|
|
30
33
|
const HIGHLIGHT_OPTIONS = Object.freeze([
|
|
31
34
|
"id", "target", "select", "selection", "color", "opacity", "fill",
|
|
32
35
|
"stroke", "strokeWidth", "strokeDash", "shape", "size", "offset",
|
|
@@ -63,6 +66,51 @@ function hasLegendSelection(program, target, selection) {
|
|
|
63
66
|
selector.field === legend.field;
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
function categoricalLegendKind(program, target) {
|
|
70
|
+
return ["series", "color"].find(
|
|
71
|
+
kind => program.guideConfigs.legend?.[kind]?.target === target
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resetCategoricalLegendSymbols(program, target) {
|
|
76
|
+
const kind = categoricalLegendKind(program, target);
|
|
77
|
+
if (kind === undefined) return program;
|
|
78
|
+
let next = program;
|
|
79
|
+
for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
|
|
80
|
+
const graphic = next.graphicSpec.objects[id];
|
|
81
|
+
if (graphic === undefined) continue;
|
|
82
|
+
next = graphic.type === "collection"
|
|
83
|
+
? next.editGraphics({ target: id, property: "items", value: [] })
|
|
84
|
+
: next.editGraphics({ target: id, property: "length", value: 0 });
|
|
85
|
+
}
|
|
86
|
+
return next.rematerializeLegendSymbols();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function targetHighlightEntries(program, target) {
|
|
90
|
+
return Object.entries(program.materializationConfigs.highlights ?? {})
|
|
91
|
+
.filter(([, config]) => config.target === target);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rebuildTargetHighlights(program, target) {
|
|
95
|
+
const layer = resolveTarget(program, target, "highlight mark");
|
|
96
|
+
const highlights = targetHighlightEntries(program, target);
|
|
97
|
+
let baseline = program;
|
|
98
|
+
for (const [id] of highlights) {
|
|
99
|
+
baseline = baseline._withoutMaterializationConfig(["highlights", id]);
|
|
100
|
+
}
|
|
101
|
+
const graphic = baseline.graphicSpec.objects[target];
|
|
102
|
+
baseline = graphic.type === "collection"
|
|
103
|
+
? baseline.editGraphics({ target, property: "items", value: [] })
|
|
104
|
+
: baseline.editGraphics({ target, property: "length", value: 0 });
|
|
105
|
+
baseline = baseline[requireSelectionPolicy(
|
|
106
|
+
layer.mark.type
|
|
107
|
+
).rematerializeOp]({ id: target });
|
|
108
|
+
baseline = resetCategoricalLegendSymbols(baseline, target);
|
|
109
|
+
return highlights.length === 0
|
|
110
|
+
? baseline
|
|
111
|
+
: baseline.rematerializeMarkHighlights({ target, highlights });
|
|
112
|
+
}
|
|
113
|
+
|
|
66
114
|
function selectedKeys(args, resolved) {
|
|
67
115
|
if (args.keys === undefined) return resolved.keys;
|
|
68
116
|
if (!Array.isArray(args.keys) || !args.keys.every(key =>
|
|
@@ -86,6 +134,79 @@ export const selectMarks = action(
|
|
|
86
134
|
}
|
|
87
135
|
);
|
|
88
136
|
|
|
137
|
+
export const editMarkSelection = action(
|
|
138
|
+
{
|
|
139
|
+
op: "editMarkSelection",
|
|
140
|
+
description: "Replace one stored mark selector while preserving its identity and target."
|
|
141
|
+
},
|
|
142
|
+
function (args = {}) {
|
|
143
|
+
validateKeys(args, EDIT_SELECTION_OPTIONS, "editMarkSelection");
|
|
144
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
145
|
+
const replacement = resolveMarkSelection(
|
|
146
|
+
this,
|
|
147
|
+
current.definition.target,
|
|
148
|
+
selectorFrom(args)
|
|
149
|
+
);
|
|
150
|
+
const next = this
|
|
151
|
+
._withSelectionConfig(current.id, {
|
|
152
|
+
target: current.definition.target,
|
|
153
|
+
selector: replacement.selector
|
|
154
|
+
})
|
|
155
|
+
._withContext({ currentSelection: current.id });
|
|
156
|
+
const hasDependentHighlight = Object.values(
|
|
157
|
+
next.materializationConfigs.highlights ?? {}
|
|
158
|
+
).some(config => config.selection === current.id);
|
|
159
|
+
return hasDependentHighlight
|
|
160
|
+
? rebuildTargetHighlights(next, current.definition.target)
|
|
161
|
+
: next;
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
export const removeMarkHighlight = action(
|
|
166
|
+
{
|
|
167
|
+
op: "removeMarkHighlight",
|
|
168
|
+
description: "Remove one stored highlight assignment and restore the clean mark baseline."
|
|
169
|
+
},
|
|
170
|
+
function (args = {}) {
|
|
171
|
+
validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkHighlight");
|
|
172
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
173
|
+
const dependent = Object.entries(
|
|
174
|
+
this.materializationConfigs.highlights ?? {}
|
|
175
|
+
).filter(([, config]) => config.selection === current.id);
|
|
176
|
+
if (dependent.length === 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Selection "${current.id}" has no highlight assignment.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
let next = this;
|
|
182
|
+
for (const [id] of dependent) {
|
|
183
|
+
next = next._withoutMaterializationConfig(["highlights", id]);
|
|
184
|
+
}
|
|
185
|
+
return rebuildTargetHighlights(next, current.definition.target);
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export const removeMarkSelection = action(
|
|
190
|
+
{
|
|
191
|
+
op: "removeMarkSelection",
|
|
192
|
+
description: "Remove one stored selection after removing its dependent highlight."
|
|
193
|
+
},
|
|
194
|
+
function (args = {}) {
|
|
195
|
+
validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkSelection");
|
|
196
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
197
|
+
const hasDependentHighlight = Object.values(
|
|
198
|
+
this.materializationConfigs.highlights ?? {}
|
|
199
|
+
).some(config => config.selection === current.id);
|
|
200
|
+
let next = hasDependentHighlight
|
|
201
|
+
? this.removeMarkHighlight({ selection: current.id })
|
|
202
|
+
: this;
|
|
203
|
+
next = next._withoutMaterializationConfig(["selections", current.id]);
|
|
204
|
+
return this.context.currentSelection === current.id
|
|
205
|
+
? next._withContext({ currentSelection: undefined })
|
|
206
|
+
: next;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
89
210
|
export const applyPointHighlight = action(
|
|
90
211
|
{ op: "applyPointHighlight", description: "Apply selected point appearance and geometry." },
|
|
91
212
|
function (args = {}) {
|
|
@@ -393,8 +514,7 @@ export const highlightMarks = action(
|
|
|
393
514
|
const existing = next.materializationConfigs.highlights?.[selection];
|
|
394
515
|
if (existing !== undefined) {
|
|
395
516
|
next = next._withoutMaterializationConfig(["highlights", selection]);
|
|
396
|
-
|
|
397
|
-
next = next[policy.rematerializeOp]({ id: resolved.definition.target });
|
|
517
|
+
next = rebuildTargetHighlights(next, resolved.definition.target);
|
|
398
518
|
}
|
|
399
519
|
const policy = requireSelectionPolicy(layer.mark.type);
|
|
400
520
|
next = next[policy.applyHighlightOp]({ selection, style, keys });
|
|
@@ -5,14 +5,20 @@ import {
|
|
|
5
5
|
applyPointHighlight,
|
|
6
6
|
applyRuleHighlight,
|
|
7
7
|
dimUnselectedMarkItems,
|
|
8
|
+
editMarkSelection,
|
|
8
9
|
highlightMarks,
|
|
9
10
|
placeSelectedMarkItemsLast,
|
|
10
11
|
rematerializeMarkHighlights,
|
|
12
|
+
removeMarkHighlight,
|
|
13
|
+
removeMarkSelection,
|
|
11
14
|
selectMarks
|
|
12
15
|
} from "./actions.js";
|
|
13
16
|
|
|
14
17
|
export function registerSelectionActions(ProgramClass) {
|
|
15
18
|
ProgramClass.prototype.selectMarks = selectMarks;
|
|
19
|
+
ProgramClass.prototype.editMarkSelection = editMarkSelection;
|
|
20
|
+
ProgramClass.prototype.removeMarkHighlight = removeMarkHighlight;
|
|
21
|
+
ProgramClass.prototype.removeMarkSelection = removeMarkSelection;
|
|
16
22
|
ProgramClass.prototype.highlightMarks = highlightMarks;
|
|
17
23
|
ProgramClass.prototype.applyBarHighlight = applyBarHighlight;
|
|
18
24
|
ProgramClass.prototype.applyRectHighlight = applyRectHighlight;
|
package/src/basic.js
ADDED
package/src/core/vocabulary.js
CHANGED
|
@@ -198,6 +198,13 @@ export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
|
|
|
198
198
|
"shape"
|
|
199
199
|
]);
|
|
200
200
|
|
|
201
|
+
export const LEGEND_CHANNELS = Object.freeze([
|
|
202
|
+
...CATEGORICAL_LEGEND_CHANNELS,
|
|
203
|
+
"strokeWidth",
|
|
204
|
+
"size",
|
|
205
|
+
"opacity"
|
|
206
|
+
]);
|
|
207
|
+
|
|
201
208
|
export const LEGEND_CONFIG_KINDS = Object.freeze([
|
|
202
209
|
"series",
|
|
203
210
|
"color",
|