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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.0.6] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added explicit edit and removal lifecycles for encodings, point appearance, selections, highlights, legends, and
|
|
12
|
+
Cartesian axis components while preserving immutable state and meaningful action traces.
|
|
13
|
+
- Added revision workflows for 2D bins, interval statistics, regression, density, box plots, gradient plots, and facet
|
|
14
|
+
policies, including deterministic rematerialization after data-role and layout changes.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Expanded installed-package, Browser Canvas, Node PNG, TypeScript, action-contract, and cross-capability coverage for
|
|
19
|
+
the complete authoring lifecycle surface.
|
|
20
|
+
- Improved documentation entry points, navigation, chart discovery, gallery filtering, responsive presentation, and
|
|
21
|
+
generated API routing for the completed lifecycle actions.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Preserved unaffected shared-legend channels when selectively removing an encoding, and prevented removed resources
|
|
26
|
+
from returning after later rematerialization or facet replay.
|
|
27
|
+
- Kept documentation search results under the deployed `/ggaction/` base path and improved gallery text contrast.
|
|
28
|
+
- Allowed the approved Polar chart ink bounds to absorb the observed 1.5-pixel platform rasterization variance while
|
|
29
|
+
retaining exact primitive/public pixel comparisons and the existing density, color, and region checks.
|
|
30
|
+
|
|
7
31
|
## [0.0.5] - 2026-07-21
|
|
8
32
|
|
|
9
33
|
### Added
|
|
@@ -111,6 +135,7 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
111
135
|
- Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
|
|
112
136
|
not imply complete polar rendering.
|
|
113
137
|
|
|
138
|
+
[0.0.6]: https://github.com/ggaction/ggaction/releases/tag/v0.0.6
|
|
114
139
|
[0.0.5]: https://github.com/ggaction/ggaction/releases/tag/v0.0.5
|
|
115
140
|
[0.0.4]: https://github.com/ggaction/ggaction/releases/tag/v0.0.4
|
|
116
141
|
[0.0.3]: https://github.com/ggaction/ggaction/releases/tag/v0.0.3
|
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ All entries include TypeScript declarations. The default entry is browser-safe;
|
|
|
142
142
|
|
|
143
143
|
## Status and development
|
|
144
144
|
|
|
145
|
-
> **Status:** `0.0.
|
|
145
|
+
> **Status:** `0.0.6` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
148
|
npm install
|
package/package.json
CHANGED
|
@@ -8,16 +8,27 @@ import { planDerivedDataRevision } from
|
|
|
8
8
|
"../../materialization/dataProvenance.js";
|
|
9
9
|
import { findDataset } from "../../selectors/datasets.js";
|
|
10
10
|
import { findLayer } from "../../selectors/layers.js";
|
|
11
|
+
import {
|
|
12
|
+
assertDistributionScaleHandoff,
|
|
13
|
+
clearCartesianPositions,
|
|
14
|
+
rebindDistributionGuides,
|
|
15
|
+
resolveDistributionScalePlan,
|
|
16
|
+
setCartesianPosition,
|
|
17
|
+
setCartesianRange
|
|
18
|
+
} from "../distributions/revision.js";
|
|
11
19
|
import {
|
|
12
20
|
resolveBoxAppearance,
|
|
13
21
|
resolveBoxMedianAppearance,
|
|
14
22
|
resolveBoxOutlierAppearance,
|
|
23
|
+
resolveBoxPosition,
|
|
15
24
|
resolveBoxWhisker,
|
|
16
25
|
resolveBoxWidth
|
|
17
26
|
} from "./options.js";
|
|
27
|
+
import { resolveBoxOrientation } from "./resolve.js";
|
|
18
28
|
|
|
19
29
|
const OPTIONS = Object.freeze([
|
|
20
|
-
"target", "
|
|
30
|
+
"target", "data", "x", "y", "whisker", "width", "outliers", "box",
|
|
31
|
+
"median", "outlier"
|
|
21
32
|
]);
|
|
22
33
|
|
|
23
34
|
function resolveBoxOwner(program, requested) {
|
|
@@ -63,10 +74,287 @@ function resolveEditedWhisker(current, value) {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
function removeOwnedMark(program, id) {
|
|
66
|
-
|
|
77
|
+
const selections = Object.entries(
|
|
78
|
+
program.materializationConfigs.selections ?? {}
|
|
79
|
+
).filter(([, config]) => config.target === id).map(([selection]) => selection);
|
|
80
|
+
let next = program;
|
|
81
|
+
for (const [highlight, config] of Object.entries(
|
|
82
|
+
program.materializationConfigs.highlights ?? {}
|
|
83
|
+
)) {
|
|
84
|
+
if (config.target === id || selections.includes(config.selection)) {
|
|
85
|
+
next = next._withoutMaterializationConfig(["highlights", highlight]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const selection of selections) {
|
|
89
|
+
next = next._withoutMaterializationConfig(["selections", selection]);
|
|
90
|
+
}
|
|
91
|
+
return next
|
|
67
92
|
.editSemantic({ property: `layer[${id}]`, remove: true })
|
|
68
93
|
.editGraphics({ target: id, remove: true })
|
|
69
|
-
._withoutMaterializationConfig(["marks", id])
|
|
94
|
+
._withoutMaterializationConfig(["marks", id])
|
|
95
|
+
._withContext({
|
|
96
|
+
...(program.context.currentMark === id ? { currentMark: undefined } : {}),
|
|
97
|
+
...(selections.includes(program.context.currentSelection)
|
|
98
|
+
? { currentSelection: undefined }
|
|
99
|
+
: {})
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function currentBoxPositions(owner, current) {
|
|
104
|
+
const categoryChannel = current.orientation === "vertical" ? "x" : "y";
|
|
105
|
+
const measureChannel = current.orientation === "vertical" ? "y" : "x";
|
|
106
|
+
const categoryEncoding = owner.encoding[categoryChannel];
|
|
107
|
+
const measureEncoding = owner.encoding[measureChannel];
|
|
108
|
+
return {
|
|
109
|
+
x: current.orientation === "vertical"
|
|
110
|
+
? {
|
|
111
|
+
field: current.category,
|
|
112
|
+
fieldType: categoryEncoding.fieldType,
|
|
113
|
+
scale: categoryEncoding.scale
|
|
114
|
+
}
|
|
115
|
+
: {
|
|
116
|
+
field: current.measure,
|
|
117
|
+
fieldType: "quantitative",
|
|
118
|
+
scale: measureEncoding.scale
|
|
119
|
+
},
|
|
120
|
+
y: current.orientation === "vertical"
|
|
121
|
+
? {
|
|
122
|
+
field: current.measure,
|
|
123
|
+
fieldType: "quantitative",
|
|
124
|
+
scale: measureEncoding.scale
|
|
125
|
+
}
|
|
126
|
+
: {
|
|
127
|
+
field: current.category,
|
|
128
|
+
fieldType: categoryEncoding.fieldType,
|
|
129
|
+
scale: categoryEncoding.scale
|
|
130
|
+
},
|
|
131
|
+
categoryScale: categoryEncoding.scale,
|
|
132
|
+
measureScale: measureEncoding.scale
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function requirePositionField(position, label) {
|
|
137
|
+
if (typeof position.field !== "string" || position.field.length === 0) {
|
|
138
|
+
throw new TypeError(`${label} field must be a non-empty string.`);
|
|
139
|
+
}
|
|
140
|
+
return { fieldType: position.fieldType ?? "quantitative", ...position };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function roleCandidate(program, owner, current, args) {
|
|
144
|
+
const previous = currentBoxPositions(owner, current);
|
|
145
|
+
const x = requirePositionField(
|
|
146
|
+
Object.hasOwn(args, "x")
|
|
147
|
+
? resolveBoxPosition(args.x, "x", "editBoxPlot")
|
|
148
|
+
: previous.x,
|
|
149
|
+
"editBoxPlot x"
|
|
150
|
+
);
|
|
151
|
+
const y = requirePositionField(
|
|
152
|
+
Object.hasOwn(args, "y")
|
|
153
|
+
? resolveBoxPosition(args.y, "y", "editBoxPlot")
|
|
154
|
+
: previous.y,
|
|
155
|
+
"editBoxPlot y"
|
|
156
|
+
);
|
|
157
|
+
const orientation = resolveBoxOrientation(x, y);
|
|
158
|
+
if (orientation === undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"editBoxPlot requires one categorical axis and one quantitative axis."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const xRoleScale = orientation === "vertical"
|
|
164
|
+
? previous.categoryScale
|
|
165
|
+
: previous.measureScale;
|
|
166
|
+
const yRoleScale = orientation === "vertical"
|
|
167
|
+
? previous.measureScale
|
|
168
|
+
: previous.categoryScale;
|
|
169
|
+
const xScale = resolveDistributionScalePlan(program, {
|
|
170
|
+
channel: "x",
|
|
171
|
+
fieldType: x.fieldType,
|
|
172
|
+
requested: x.scale,
|
|
173
|
+
fallback: xRoleScale,
|
|
174
|
+
defaults: ["ordinal", "nominal"].includes(x.fieldType)
|
|
175
|
+
? { discreteType: "band" }
|
|
176
|
+
: { nice: true, zero: false }
|
|
177
|
+
});
|
|
178
|
+
const yScale = resolveDistributionScalePlan(program, {
|
|
179
|
+
channel: "y",
|
|
180
|
+
fieldType: y.fieldType,
|
|
181
|
+
requested: y.scale,
|
|
182
|
+
fallback: yRoleScale,
|
|
183
|
+
defaults: ["ordinal", "nominal"].includes(y.fieldType)
|
|
184
|
+
? { discreteType: "band" }
|
|
185
|
+
: { nice: true, zero: false }
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
source: Object.hasOwn(args, "data")
|
|
189
|
+
? validateUserId(args.data, "Box-plot data id")
|
|
190
|
+
: current.source,
|
|
191
|
+
orientation,
|
|
192
|
+
x: { ...x, scale: xScale.id },
|
|
193
|
+
y: { ...y, scale: yScale.id },
|
|
194
|
+
xScale,
|
|
195
|
+
yScale,
|
|
196
|
+
category: orientation === "vertical" ? x.field : y.field,
|
|
197
|
+
categoryType: orientation === "vertical" ? x.fieldType : y.fieldType,
|
|
198
|
+
measure: orientation === "vertical" ? y.field : x.field,
|
|
199
|
+
previous
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function sameRoleCandidate(current, candidate) {
|
|
204
|
+
return candidate.source === current.source &&
|
|
205
|
+
candidate.orientation === current.orientation &&
|
|
206
|
+
candidate.category === current.category &&
|
|
207
|
+
candidate.measure === current.measure &&
|
|
208
|
+
candidate.x.fieldType === candidate.previous.x.fieldType &&
|
|
209
|
+
candidate.y.fieldType === candidate.previous.y.fieldType &&
|
|
210
|
+
candidate.xScale.id === candidate.previous.x.scale &&
|
|
211
|
+
candidate.yScale.id === candidate.previous.y.scale &&
|
|
212
|
+
candidate.xScale.edit === undefined &&
|
|
213
|
+
candidate.yScale.edit === undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function updateBoxPositions(program, owner, current, candidate, {
|
|
217
|
+
hasOutliers
|
|
218
|
+
}) {
|
|
219
|
+
const whiskerConfig = program.markConfigs[current.whiskerId];
|
|
220
|
+
const capIds = [
|
|
221
|
+
whiskerConfig.errorBar.lowerCapId,
|
|
222
|
+
whiskerConfig.errorBar.upperCapId
|
|
223
|
+
].filter(Boolean);
|
|
224
|
+
const owned = [owner.id, current.whiskerId, ...capIds, current.medianId,
|
|
225
|
+
...(findLayer(program, current.outlierId) ? [current.outlierId] : [])];
|
|
226
|
+
assertDistributionScaleHandoff(program, {
|
|
227
|
+
owned,
|
|
228
|
+
oldXScale: candidate.previous.x.scale,
|
|
229
|
+
oldYScale: candidate.previous.y.scale,
|
|
230
|
+
newXScale: candidate.xScale.id,
|
|
231
|
+
newYScale: candidate.yScale.id
|
|
232
|
+
});
|
|
233
|
+
let next = program;
|
|
234
|
+
for (const id of owned) next = clearCartesianPositions(next, id);
|
|
235
|
+
for (const scale of [candidate.xScale, candidate.yScale]) {
|
|
236
|
+
if (scale.create) next = next.createScale(scale.definition);
|
|
237
|
+
}
|
|
238
|
+
const category = candidate.orientation === "vertical" ? candidate.x : candidate.y;
|
|
239
|
+
const measure = candidate.orientation === "vertical" ? candidate.y : candidate.x;
|
|
240
|
+
const categoryChannel = candidate.orientation === "vertical" ? "x" : "y";
|
|
241
|
+
const measureChannel = candidate.orientation === "vertical" ? "y" : "x";
|
|
242
|
+
next = setCartesianPosition(next, owner.id, categoryChannel, {
|
|
243
|
+
field: category.field,
|
|
244
|
+
fieldType: category.fieldType,
|
|
245
|
+
scale: category.scale
|
|
246
|
+
});
|
|
247
|
+
next = setCartesianRange(
|
|
248
|
+
next,
|
|
249
|
+
owner.id,
|
|
250
|
+
measureChannel,
|
|
251
|
+
BOX_FIELDS.q1,
|
|
252
|
+
BOX_FIELDS.q3,
|
|
253
|
+
measure.scale,
|
|
254
|
+
candidate.measure
|
|
255
|
+
);
|
|
256
|
+
next = next._withMarkConfig(current.whiskerId, {
|
|
257
|
+
...whiskerConfig,
|
|
258
|
+
errorBar: {
|
|
259
|
+
...whiskerConfig.errorBar,
|
|
260
|
+
data: findLayer(next, current.whiskerId).data,
|
|
261
|
+
orientation: candidate.orientation,
|
|
262
|
+
positionField: candidate.category,
|
|
263
|
+
positionFieldType: candidate.categoryType,
|
|
264
|
+
coordinate: owner.coordinate,
|
|
265
|
+
positionScale: category.scale,
|
|
266
|
+
intervalScale: measure.scale
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
next = setCartesianPosition(next, current.whiskerId, categoryChannel, {
|
|
270
|
+
field: category.field,
|
|
271
|
+
fieldType: category.fieldType,
|
|
272
|
+
scale: category.scale
|
|
273
|
+
});
|
|
274
|
+
next = setCartesianRange(
|
|
275
|
+
next,
|
|
276
|
+
current.whiskerId,
|
|
277
|
+
measureChannel,
|
|
278
|
+
BOX_FIELDS.lowerWhisker,
|
|
279
|
+
BOX_FIELDS.upperWhisker,
|
|
280
|
+
measure.scale,
|
|
281
|
+
candidate.measure
|
|
282
|
+
);
|
|
283
|
+
for (const [index, capId] of capIds.entries()) {
|
|
284
|
+
const field = index === 0 ? BOX_FIELDS.lowerWhisker : BOX_FIELDS.upperWhisker;
|
|
285
|
+
next = setCartesianPosition(next, capId, categoryChannel, {
|
|
286
|
+
field: category.field,
|
|
287
|
+
fieldType: category.fieldType,
|
|
288
|
+
scale: category.scale
|
|
289
|
+
});
|
|
290
|
+
next = setCartesianPosition(next, capId, measureChannel, {
|
|
291
|
+
field,
|
|
292
|
+
fieldType: "quantitative",
|
|
293
|
+
scale: measure.scale
|
|
294
|
+
})._withMarkConfig(capId, {
|
|
295
|
+
...next.markConfigs[capId],
|
|
296
|
+
fixedSpan: {
|
|
297
|
+
...next.markConfigs[capId].fixedSpan,
|
|
298
|
+
orientation: candidate.orientation === "vertical"
|
|
299
|
+
? "horizontal"
|
|
300
|
+
: "vertical"
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
next = setCartesianPosition(next, current.medianId, categoryChannel, {
|
|
305
|
+
field: category.field,
|
|
306
|
+
fieldType: category.fieldType,
|
|
307
|
+
scale: category.scale
|
|
308
|
+
});
|
|
309
|
+
next = setCartesianPosition(next, current.medianId, measureChannel, {
|
|
310
|
+
field: BOX_FIELDS.median,
|
|
311
|
+
fieldType: "quantitative",
|
|
312
|
+
scale: measure.scale
|
|
313
|
+
});
|
|
314
|
+
if (hasOutliers && findLayer(next, current.outlierId) !== undefined) {
|
|
315
|
+
next = setCartesianPosition(next, current.outlierId, categoryChannel, {
|
|
316
|
+
field: category.field,
|
|
317
|
+
fieldType: category.fieldType,
|
|
318
|
+
scale: category.scale
|
|
319
|
+
});
|
|
320
|
+
next = setCartesianPosition(next, current.outlierId, measureChannel, {
|
|
321
|
+
field: candidate.measure,
|
|
322
|
+
fieldType: "quantitative",
|
|
323
|
+
scale: measure.scale
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
for (const id of new Set([candidate.xScale.id, candidate.yScale.id])) {
|
|
327
|
+
next = next.rematerializeScale({ id, marks: false, guides: false });
|
|
328
|
+
}
|
|
329
|
+
next = rebindDistributionGuides(next, {
|
|
330
|
+
oldXScale: candidate.previous.x.scale,
|
|
331
|
+
oldYScale: candidate.previous.y.scale,
|
|
332
|
+
newXScale: candidate.xScale.id,
|
|
333
|
+
newYScale: candidate.yScale.id,
|
|
334
|
+
oldXTitle: candidate.previous.x.field,
|
|
335
|
+
oldYTitle: candidate.previous.y.field,
|
|
336
|
+
newXTitle: candidate.x.field,
|
|
337
|
+
newYTitle: candidate.y.field,
|
|
338
|
+
oldMeasureChannel: current.orientation === "vertical" ? "y" : "x",
|
|
339
|
+
newMeasureChannel: candidate.orientation === "vertical" ? "y" : "x"
|
|
340
|
+
});
|
|
341
|
+
for (const scale of [candidate.xScale, candidate.yScale]) {
|
|
342
|
+
if (scale.edit !== undefined) next = next.editScale(scale.edit);
|
|
343
|
+
}
|
|
344
|
+
next = next.rematerializeBarMark({ id: owner.id })
|
|
345
|
+
.rematerializeErrorBar({ id: current.whiskerId });
|
|
346
|
+
for (const capId of capIds) {
|
|
347
|
+
next = next.materializeRuleSpan({
|
|
348
|
+
id: capId,
|
|
349
|
+
orientation: candidate.orientation === "vertical" ? "horizontal" : "vertical",
|
|
350
|
+
size: whiskerConfig.errorBar.capSize
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
next = next.rematerializeRuleMark({ id: current.medianId });
|
|
354
|
+
if (hasOutliers && findLayer(next, current.outlierId) !== undefined) {
|
|
355
|
+
next = next.rematerializePointMark({ id: current.outlierId });
|
|
356
|
+
}
|
|
357
|
+
return next;
|
|
70
358
|
}
|
|
71
359
|
|
|
72
360
|
export const editBoxPlot = action(
|
|
@@ -116,6 +404,170 @@ export const editBoxPlot = action(
|
|
|
116
404
|
);
|
|
117
405
|
}
|
|
118
406
|
|
|
407
|
+
const candidate = roleCandidate(this, owner, current, args);
|
|
408
|
+
const sourceDataset = findDataset(this, candidate.source);
|
|
409
|
+
if (sourceDataset === undefined) {
|
|
410
|
+
throw new Error(`Unknown box-plot data "${candidate.source}".`);
|
|
411
|
+
}
|
|
412
|
+
const roleRequested = ["data", "x", "y"].some(
|
|
413
|
+
key => Object.hasOwn(args, key)
|
|
414
|
+
);
|
|
415
|
+
const changesRoles = !sameRoleCandidate(current, candidate);
|
|
416
|
+
if (changesRoles) {
|
|
417
|
+
const derived = deriveBoxData(
|
|
418
|
+
sourceDataset.values,
|
|
419
|
+
normalizeBoxTransform({
|
|
420
|
+
type: "boxSummary",
|
|
421
|
+
category: candidate.category,
|
|
422
|
+
field: candidate.measure,
|
|
423
|
+
whisker: whisker.type,
|
|
424
|
+
...(whisker.factor === undefined ? {} : { factor: whisker.factor })
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
const hasOutliers = outliers && whisker.type === "tukey" &&
|
|
428
|
+
derived.outliers.length > 0;
|
|
429
|
+
const whiskerConfig = this.markConfigs[current.whiskerId];
|
|
430
|
+
const capIds = [
|
|
431
|
+
whiskerConfig.errorBar.lowerCapId,
|
|
432
|
+
whiskerConfig.errorBar.upperCapId
|
|
433
|
+
].filter(Boolean);
|
|
434
|
+
const summaryRevision = planDerivedDataRevision(this, {
|
|
435
|
+
owner: owner.id,
|
|
436
|
+
role: "SummaryData",
|
|
437
|
+
previous: current.summaryId,
|
|
438
|
+
consumers: [owner.id, current.whiskerId, ...capIds, current.medianId]
|
|
439
|
+
});
|
|
440
|
+
const hadOutlierLayer = findLayer(this, current.outlierId) !== undefined;
|
|
441
|
+
const outlierRevision = hasOutliers
|
|
442
|
+
? planDerivedDataRevision(this, {
|
|
443
|
+
owner: owner.id,
|
|
444
|
+
role: "OutlierData",
|
|
445
|
+
...(current.outlierDataId === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { previous: current.outlierDataId }),
|
|
448
|
+
consumers: hadOutlierLayer ? [current.outlierId] : []
|
|
449
|
+
})
|
|
450
|
+
: undefined;
|
|
451
|
+
const applyEdit = program => {
|
|
452
|
+
let next = program.createBoxSummaryData({
|
|
453
|
+
id: summaryRevision.id,
|
|
454
|
+
source: candidate.source,
|
|
455
|
+
category: candidate.category,
|
|
456
|
+
field: candidate.measure,
|
|
457
|
+
whisker: whisker.type,
|
|
458
|
+
...(whisker.factor === undefined ? {} : { factor: whisker.factor })
|
|
459
|
+
});
|
|
460
|
+
if (outlierRevision !== undefined) {
|
|
461
|
+
next = next.createBoxOutlierData({
|
|
462
|
+
id: outlierRevision.id,
|
|
463
|
+
source: candidate.source,
|
|
464
|
+
category: candidate.category,
|
|
465
|
+
field: candidate.measure,
|
|
466
|
+
whisker: whisker.type,
|
|
467
|
+
factor: whisker.factor
|
|
468
|
+
});
|
|
469
|
+
for (const rebind of outlierRevision.rebinds) {
|
|
470
|
+
next = next.rebindLayerData(rebind);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const rebind of summaryRevision.rebinds) {
|
|
474
|
+
next = next.rebindLayerData(rebind);
|
|
475
|
+
}
|
|
476
|
+
next = next._withMarkConfig(current.whiskerId, {
|
|
477
|
+
...next.markConfigs[current.whiskerId],
|
|
478
|
+
errorBar: {
|
|
479
|
+
...next.markConfigs[current.whiskerId].errorBar,
|
|
480
|
+
data: summaryRevision.id
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
if (hadOutlierLayer && !hasOutliers) {
|
|
484
|
+
next = removeOwnedMark(next, current.outlierId);
|
|
485
|
+
}
|
|
486
|
+
next = next._withMarkConfig(owner.id, {
|
|
487
|
+
...next.markConfigs[owner.id],
|
|
488
|
+
boxPlot: {
|
|
489
|
+
...current,
|
|
490
|
+
whisker,
|
|
491
|
+
width,
|
|
492
|
+
outliers,
|
|
493
|
+
box,
|
|
494
|
+
median,
|
|
495
|
+
outlier,
|
|
496
|
+
source: candidate.source,
|
|
497
|
+
orientation: candidate.orientation,
|
|
498
|
+
category: candidate.category,
|
|
499
|
+
measure: candidate.measure,
|
|
500
|
+
summaryId: summaryRevision.id,
|
|
501
|
+
outlierDataId: outlierRevision?.id
|
|
502
|
+
},
|
|
503
|
+
barWidth: { band: width },
|
|
504
|
+
fill: box.fill,
|
|
505
|
+
opacity: box.opacity,
|
|
506
|
+
stroke: box.stroke,
|
|
507
|
+
strokeWidth: box.strokeWidth
|
|
508
|
+
});
|
|
509
|
+
next = updateBoxPositions(next, owner, current, candidate, {
|
|
510
|
+
outlierDataId: outlierRevision?.id,
|
|
511
|
+
hasOutliers
|
|
512
|
+
});
|
|
513
|
+
if (!hadOutlierLayer && hasOutliers) {
|
|
514
|
+
const category = candidate.orientation === "vertical"
|
|
515
|
+
? candidate.x
|
|
516
|
+
: candidate.y;
|
|
517
|
+
const measure = candidate.orientation === "vertical"
|
|
518
|
+
? candidate.y
|
|
519
|
+
: candidate.x;
|
|
520
|
+
next = next.createBoxOutliers({
|
|
521
|
+
id: current.outlierId,
|
|
522
|
+
data: outlierRevision.id,
|
|
523
|
+
category: candidate.category,
|
|
524
|
+
categoryType: candidate.categoryType,
|
|
525
|
+
measure: candidate.measure,
|
|
526
|
+
orientation: candidate.orientation,
|
|
527
|
+
coordinate: owner.coordinate,
|
|
528
|
+
categoryScale: category.scale,
|
|
529
|
+
measureScale: measure.scale,
|
|
530
|
+
shape: outlier.shape,
|
|
531
|
+
radius: outlier.radius,
|
|
532
|
+
opacity: outlier.opacity
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
next = next
|
|
536
|
+
.encodeStroke({ target: current.medianId, value: median.stroke })
|
|
537
|
+
.encodeStrokeWidth({
|
|
538
|
+
target: current.medianId,
|
|
539
|
+
value: median.strokeWidth
|
|
540
|
+
});
|
|
541
|
+
if (findLayer(next, current.outlierId) !== undefined) {
|
|
542
|
+
next = next
|
|
543
|
+
.editPointMark({
|
|
544
|
+
target: current.outlierId,
|
|
545
|
+
shape: outlier.shape,
|
|
546
|
+
opacity: outlier.opacity
|
|
547
|
+
})
|
|
548
|
+
.encodeRadius({ target: current.outlierId, value: outlier.radius });
|
|
549
|
+
}
|
|
550
|
+
next = next.releaseDerivedData(summaryRevision.release);
|
|
551
|
+
if (current.outlierDataId !== undefined) {
|
|
552
|
+
next = next.releaseDerivedData({ id: current.outlierDataId });
|
|
553
|
+
}
|
|
554
|
+
return next._withContext({
|
|
555
|
+
currentMark: owner.id,
|
|
556
|
+
currentData: candidate.source
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
applyEdit(this);
|
|
560
|
+
return applyEdit(this);
|
|
561
|
+
}
|
|
562
|
+
if (roleRequested && !OPTIONS.slice(4).some(
|
|
563
|
+
key => Object.hasOwn(args, key)
|
|
564
|
+
)) {
|
|
565
|
+
return this._withContext({
|
|
566
|
+
currentMark: owner.id,
|
|
567
|
+
currentData: current.source
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
119
571
|
const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
|
|
120
572
|
outliers !== current.outliers;
|
|
121
573
|
const changesBox = Object.hasOwn(args, "box") ||
|
|
@@ -37,12 +37,12 @@ function plainOptions(value, keys, label, operation = "createBoxPlot") {
|
|
|
37
37
|
return value;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function resolveBoxPosition(value, label) {
|
|
40
|
+
export function resolveBoxPosition(value, label, operation = "createBoxPlot") {
|
|
41
41
|
if (value === undefined) return undefined;
|
|
42
42
|
if (!isPlainObject(value)) {
|
|
43
|
-
throw new TypeError(
|
|
43
|
+
throw new TypeError(`${operation} ${label} must be a plain object.`);
|
|
44
44
|
}
|
|
45
|
-
validateKeys(value, ["field", "fieldType", "scale"],
|
|
45
|
+
validateKeys(value, ["field", "fieldType", "scale"], `${operation} ${label}`);
|
|
46
46
|
return value;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -16,7 +16,9 @@ import { materializeFacetGraphics } from "../../materialization/facets.js";
|
|
|
16
16
|
const CONCAT_OPTIONS = Object.freeze([
|
|
17
17
|
"id", "programs", "gap", "align", "padding"
|
|
18
18
|
]);
|
|
19
|
-
const LAYOUT_EDIT_OPTIONS = Object.freeze([
|
|
19
|
+
const LAYOUT_EDIT_OPTIONS = Object.freeze([
|
|
20
|
+
"columns", "gap", "align", "padding"
|
|
21
|
+
]);
|
|
20
22
|
const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
|
|
21
23
|
|
|
22
24
|
function normalizeChildEntry(entry, index) {
|
|
@@ -149,7 +151,7 @@ export const vconcatAction = concatAction("vertical", "vconcat");
|
|
|
149
151
|
const editCompositionLayout = action(
|
|
150
152
|
{
|
|
151
153
|
op: "editCompositionLayout",
|
|
152
|
-
description: "Edit composition
|
|
154
|
+
description: "Edit composition layout.",
|
|
153
155
|
scope: "composition"
|
|
154
156
|
},
|
|
155
157
|
function (args = {}) {
|
|
@@ -164,7 +166,9 @@ const editCompositionLayout = action(
|
|
|
164
166
|
...childDescriptor({ id, program: this.children[id] }),
|
|
165
167
|
value: current.facet.values[index]
|
|
166
168
|
})),
|
|
167
|
-
columns:
|
|
169
|
+
columns: Object.hasOwn(args, "columns")
|
|
170
|
+
? args.columns
|
|
171
|
+
: current.columns,
|
|
168
172
|
gap: Object.hasOwn(args, "gap") ? args.gap : current.gap,
|
|
169
173
|
align: Object.hasOwn(args, "align") ? args.align : current.align,
|
|
170
174
|
padding: Object.hasOwn(args, "padding")
|
|
@@ -176,12 +180,18 @@ const editCompositionLayout = action(
|
|
|
176
180
|
children: this.children,
|
|
177
181
|
compositionSpec: {
|
|
178
182
|
...current,
|
|
183
|
+
columns: layout.columns,
|
|
179
184
|
gap: layout.gap,
|
|
180
185
|
align: layout.align,
|
|
181
186
|
padding: layout.padding
|
|
182
187
|
}
|
|
183
188
|
}).materializeComposition();
|
|
184
189
|
}
|
|
190
|
+
if (Object.hasOwn(args, "columns")) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"editCompositionLayout columns is available only on a facet composition."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
185
195
|
const padding = Object.hasOwn(args, "padding")
|
|
186
196
|
? normalizeCompositionPadding(args.padding, current.padding)
|
|
187
197
|
: current.padding;
|