ggaction 0.0.16 → 0.0.17
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 +20 -0
- package/README.md +2 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +8 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +19 -6
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +4 -1
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +30 -2
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +6 -2
- package/src/actions/encodings/color/continuous.js +4 -1
- package/src/actions/encodings/color/index.js +4 -1
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/resolve.js +6 -1
- package/src/actions/encodings/ranged.js +6 -1
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +6 -1
- package/src/actions/marks/bar/create.js +8 -1
- package/src/actions/marks/bar/edit.js +12 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +12 -5
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +23 -8
- package/src/actions/marks/rule/actions.js +24 -8
- package/src/actions/marks/text/actions.js +25 -6
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +22 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +25 -4
- package/src/actions/regression/edit.js +47 -12
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/scales/channels.js +11 -11
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/create.js +2 -1
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +28 -1
- package/src/actions/theme/reconcile.js +2 -0
- package/src/core/action.js +3 -2
- package/src/core/programState.js +5 -1
- package/src/grammar/bin.js +39 -10
- package/src/grammar/datasetSchema.js +386 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/interval.js +32 -1
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/regression/derive.js +57 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +51 -22
- package/src/grammar/regression/parameters.js +56 -4
- package/src/grammar/scales/definition.js +7 -0
- package/src/grammar/schemas/semanticPath.js +3 -2
- package/src/grammar/sort.js +88 -0
- package/src/grammar/summary.js +117 -19
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +21 -11
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/materialization/bars/resolve.js +4 -2
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scales/resolve.js +67 -4
- package/src/materialization/text.js +4 -2
- package/src/persistence.js +79 -6
- package/src/version.js +1 -1
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +147 -27
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
validateNonNegativeFinite,
|
|
8
8
|
validateUnitInterval
|
|
9
9
|
} from "../../core/validation.js";
|
|
10
|
-
import { normalizeRegressionParameters } from "../../grammar/regression/index.js";
|
|
10
|
+
import { normalizeRegressionParameters, normalizeRegressionPredict } from "../../grammar/regression/index.js";
|
|
11
11
|
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
12
12
|
import {
|
|
13
13
|
requestedStrokeDetails,
|
|
@@ -23,6 +23,7 @@ import { requireRegressionField } from "./resolve.js";
|
|
|
23
23
|
const OPTIONS = Object.freeze([
|
|
24
24
|
"target", "data", "x", "y", "groupBy", "method", "degree", "span",
|
|
25
25
|
"confidenceMethod", "level", "confidence", "interval", "band", "line"
|
|
26
|
+
, "predict", "sourceBinding", "missing"
|
|
26
27
|
]);
|
|
27
28
|
const BAND_OPTIONS = Object.freeze([
|
|
28
29
|
"color", "opacity", "stroke", "strokeWidth", "curve",
|
|
@@ -115,7 +116,7 @@ function resolveParameters(previous, args) {
|
|
|
115
116
|
if (Object.hasOwn(args, key)) raw[key] = args[key];
|
|
116
117
|
}
|
|
117
118
|
const inheritsLevel = !Object.hasOwn(raw, "confidence");
|
|
118
|
-
if (method === previous.method) {
|
|
119
|
+
if (method === previous.method && args.interval !== false) {
|
|
119
120
|
for (const key of [
|
|
120
121
|
"degree", "span", "confidenceMethod", "level", "interval"
|
|
121
122
|
]) {
|
|
@@ -127,7 +128,7 @@ function resolveParameters(previous, args) {
|
|
|
127
128
|
raw[key] = previous[key];
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
|
-
} else if (method !== "loess" && previous.method !== "loess") {
|
|
131
|
+
} else if (method !== "loess" && previous.method !== "loess" && args.interval !== false) {
|
|
131
132
|
for (const key of ["confidenceMethod", "level", "interval"]) {
|
|
132
133
|
if (
|
|
133
134
|
!Object.hasOwn(raw, key) &&
|
|
@@ -138,7 +139,19 @@ function resolveParameters(previous, args) {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
+
const parameters = normalizeRegressionParameters(raw);
|
|
143
|
+
const predict = Object.hasOwn(args, "predict")
|
|
144
|
+
? args.predict === false ? undefined : normalizeRegressionPredict(args.predict)
|
|
145
|
+
: previous.predict;
|
|
146
|
+
const missing = Object.hasOwn(args, "missing") ? args.missing : previous.missing;
|
|
147
|
+
if (missing !== undefined && !["error", "drop"].includes(missing)) {
|
|
148
|
+
throw new Error('Regression missing must be "error" or "drop".');
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...parameters,
|
|
152
|
+
...(predict === undefined ? {} : { predict }),
|
|
153
|
+
...(missing === undefined ? {} : { missing })
|
|
154
|
+
};
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
function regressionColorScale(owner, current, groupBy) {
|
|
@@ -245,15 +258,31 @@ export const editRegression = /* @__PURE__ */ closedAction(
|
|
|
245
258
|
}
|
|
246
259
|
const owner = resolveRegressionOwner(this, args.target);
|
|
247
260
|
const current = this.markConfigs[owner.id].regression;
|
|
248
|
-
const
|
|
261
|
+
const currentBinding = owner.derivedBindings?.regression?.mode === "follow" ? "follow" : "fixed";
|
|
262
|
+
const sourceBinding = args.sourceBinding ?? currentBinding;
|
|
263
|
+
if (!["fixed", "follow"].includes(sourceBinding)) throw new Error('Regression sourceBinding must be "fixed" or "follow".');
|
|
264
|
+
const followed = sourceBinding === "follow" ? {
|
|
265
|
+
data: owner.data,
|
|
266
|
+
x: owner.encoding?.x?.fieldType === "quantitative" ? owner.encoding.x.field : undefined,
|
|
267
|
+
y: owner.encoding?.y?.fieldType === "quantitative" ? owner.encoding.y.field : undefined
|
|
268
|
+
} : undefined;
|
|
269
|
+
if (sourceBinding === "follow" && (!followed.data || !followed.x || !followed.y)) {
|
|
270
|
+
throw new Error(`Following regression source "${owner.id}" requires quantitative x/y fields and data.`);
|
|
271
|
+
}
|
|
272
|
+
for (const key of ["data", "x", "y"]) {
|
|
273
|
+
if (followed !== undefined && Object.hasOwn(args, key) && args[key] !== followed[key]) {
|
|
274
|
+
throw new Error(`Following regression ${key} must match source mark "${owner.id}".`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const source = followed?.data ?? (Object.hasOwn(args, "data")
|
|
249
278
|
? validateUserId(args.data, "Regression data id")
|
|
250
|
-
: current.source;
|
|
251
|
-
const x = Object.hasOwn(args, "x")
|
|
279
|
+
: current.source);
|
|
280
|
+
const x = followed?.x ?? (Object.hasOwn(args, "x")
|
|
252
281
|
? requireRegressionField(args.x, "Regression x")
|
|
253
|
-
: current.x;
|
|
254
|
-
const y = Object.hasOwn(args, "y")
|
|
282
|
+
: current.x);
|
|
283
|
+
const y = followed?.y ?? (Object.hasOwn(args, "y")
|
|
255
284
|
? requireRegressionField(args.y, "Regression y")
|
|
256
|
-
: current.y;
|
|
285
|
+
: current.y);
|
|
257
286
|
const groupBy = Object.hasOwn(args, "groupBy")
|
|
258
287
|
? args.groupBy === false
|
|
259
288
|
? undefined
|
|
@@ -272,12 +301,12 @@ export const editRegression = /* @__PURE__ */ closedAction(
|
|
|
272
301
|
const linePatch = Object.hasOwn(args, "line")
|
|
273
302
|
? validateLinePatch(args.line)
|
|
274
303
|
: undefined;
|
|
275
|
-
if (parameters.method === "loess" && bandPatch !== undefined && bandPatch !== false) {
|
|
304
|
+
if ((parameters.method === "loess" || parameters.interval === false) && bandPatch !== undefined && bandPatch !== false) {
|
|
276
305
|
throw new Error("LOESS regression does not support a band object.");
|
|
277
306
|
}
|
|
278
307
|
|
|
279
308
|
const hadBand = current.bandId !== undefined;
|
|
280
|
-
const wantsBand = parameters.method !== "loess" &&
|
|
309
|
+
const wantsBand = parameters.method !== "loess" && parameters.interval !== false &&
|
|
281
310
|
(bandPatch === false
|
|
282
311
|
? false
|
|
283
312
|
: hadBand || bandPatch !== undefined || current.parameters.method === "loess");
|
|
@@ -385,6 +414,12 @@ export const editRegression = /* @__PURE__ */ closedAction(
|
|
|
385
414
|
if (changesStatistics) {
|
|
386
415
|
next = next.releaseDerivedData(revision.release);
|
|
387
416
|
}
|
|
417
|
+
const binding = findLayer(next, owner.id)?.derivedBindings?.regression;
|
|
418
|
+
if (sourceBinding === "follow" && binding?.mode !== "follow") {
|
|
419
|
+
next = next.editSemantic({ property: `layer[${owner.id}].derivedBindings.regression`, value: { mode: "follow", roles: ["data", "x", "y"] } });
|
|
420
|
+
} else if (sourceBinding === "fixed" && binding !== undefined) {
|
|
421
|
+
next = next.editSemantic({ property: `layer[${owner.id}].derivedBindings.regression`, remove: true });
|
|
422
|
+
}
|
|
388
423
|
return next;
|
|
389
424
|
};
|
|
390
425
|
return applyEdit(this);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function reconcileFollowingRegressions(program) {
|
|
2
|
+
let next = program;
|
|
3
|
+
for (const layer of program.semanticSpec.layers) {
|
|
4
|
+
if (layer.derivedBindings?.regression?.mode !== "follow") continue;
|
|
5
|
+
const config = next.markConfigs[layer.id]?.regression;
|
|
6
|
+
if (config === undefined) continue;
|
|
7
|
+
const x = layer.encoding?.x;
|
|
8
|
+
const y = layer.encoding?.y;
|
|
9
|
+
if (typeof layer.data !== "string" || x?.fieldType !== "quantitative" || y?.fieldType !== "quantitative" ||
|
|
10
|
+
typeof x.field !== "string" || typeof y.field !== "string") {
|
|
11
|
+
throw new Error(`Following regression source "${layer.id}" requires quantitative x/y fields and data.`);
|
|
12
|
+
}
|
|
13
|
+
if (config.source === layer.data && config.x === x.field && config.y === y.field) continue;
|
|
14
|
+
next = next.editRegression({
|
|
15
|
+
target: layer.id,
|
|
16
|
+
data: layer.data,
|
|
17
|
+
x: x.field,
|
|
18
|
+
y: y.field
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
@@ -10,7 +10,7 @@ const DEFINITIONS = Object.freeze({
|
|
|
10
10
|
editXScale: Object.freeze({
|
|
11
11
|
channel: "x",
|
|
12
12
|
options: Object.freeze([
|
|
13
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
13
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
14
14
|
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
15
15
|
"padding", "align", "unknown"
|
|
16
16
|
])
|
|
@@ -18,7 +18,7 @@ const DEFINITIONS = Object.freeze({
|
|
|
18
18
|
editYScale: Object.freeze({
|
|
19
19
|
channel: "y",
|
|
20
20
|
options: Object.freeze([
|
|
21
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
21
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
22
22
|
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
23
23
|
"padding", "align", "unknown"
|
|
24
24
|
])
|
|
@@ -26,21 +26,21 @@ const DEFINITIONS = Object.freeze({
|
|
|
26
26
|
editThetaScale: Object.freeze({
|
|
27
27
|
channel: "theta",
|
|
28
28
|
options: Object.freeze([
|
|
29
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
29
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
30
30
|
"paddingInner", "paddingOuter", "padding", "align"
|
|
31
31
|
])
|
|
32
32
|
}),
|
|
33
33
|
editRScale: Object.freeze({
|
|
34
34
|
channel: "radius",
|
|
35
35
|
options: Object.freeze([
|
|
36
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
36
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
37
37
|
"base", "exponent", "constant", "radialMapping"
|
|
38
38
|
])
|
|
39
39
|
}),
|
|
40
40
|
editColorScale: Object.freeze({
|
|
41
41
|
channel: "color",
|
|
42
42
|
options: Object.freeze([
|
|
43
|
-
"type", "domain", "range", "clamp", "reverse", "palette",
|
|
43
|
+
"type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
|
|
44
44
|
"interpolate", "midpoint", "unknown"
|
|
45
45
|
])
|
|
46
46
|
}),
|
|
@@ -48,37 +48,37 @@ const DEFINITIONS = Object.freeze({
|
|
|
48
48
|
channel: "stroke",
|
|
49
49
|
targetOnly: true,
|
|
50
50
|
options: Object.freeze([
|
|
51
|
-
"type", "domain", "range", "clamp", "reverse", "palette",
|
|
51
|
+
"type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
|
|
52
52
|
"interpolate", "midpoint", "unknown"
|
|
53
53
|
])
|
|
54
54
|
}),
|
|
55
55
|
editSizeScale: Object.freeze({
|
|
56
56
|
channel: "size",
|
|
57
57
|
options: Object.freeze([
|
|
58
|
-
"type", "domain", "range", "unknown", "clamp", "reverse", "base",
|
|
58
|
+
"type", "domain", "range", "emptyDomain", "unknown", "clamp", "reverse", "base",
|
|
59
59
|
"exponent"
|
|
60
60
|
])
|
|
61
61
|
}),
|
|
62
62
|
editOpacityScale: Object.freeze({
|
|
63
63
|
channel: "opacity",
|
|
64
64
|
options: Object.freeze([
|
|
65
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse", "unknown"
|
|
65
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse", "unknown"
|
|
66
66
|
])
|
|
67
67
|
}),
|
|
68
68
|
editShapeScale: Object.freeze({
|
|
69
69
|
channel: "shape",
|
|
70
|
-
options: Object.freeze(["type", "domain", "range", "unknown"])
|
|
70
|
+
options: Object.freeze(["type", "domain", "range", "emptyDomain", "unknown"])
|
|
71
71
|
}),
|
|
72
72
|
editStrokeWidthScale: Object.freeze({
|
|
73
73
|
channel: "strokeWidth",
|
|
74
74
|
options: Object.freeze([
|
|
75
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
75
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
76
76
|
"base", "exponent", "constant"
|
|
77
77
|
])
|
|
78
78
|
}),
|
|
79
79
|
editStrokeDashScale: Object.freeze({
|
|
80
80
|
channel: "strokeDash",
|
|
81
|
-
options: Object.freeze(["type", "domain", "range"])
|
|
81
|
+
options: Object.freeze(["type", "domain", "range", "emptyDomain"])
|
|
82
82
|
})
|
|
83
83
|
});
|
|
84
84
|
|
|
@@ -5,6 +5,7 @@ import { readScaleField } from "../../../grammar/scales/index.js";
|
|
|
5
5
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
6
6
|
import { requireSemanticScale } from "../../../selectors/scales.js";
|
|
7
7
|
import { SCALED_ENCODING_CHANNELS } from "../../../core/vocabulary.js";
|
|
8
|
+
import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
|
|
8
9
|
|
|
9
10
|
export function findScale(program, id) {
|
|
10
11
|
return requireSemanticScale(program, id);
|
|
@@ -39,7 +40,7 @@ export function requireConsumerDataset(program, consumer) {
|
|
|
39
40
|
`Mark "${consumer.layer.id}" references unknown dataset "${consumer.layer.data}".`
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
|
-
return dataset;
|
|
43
|
+
return applyItemMissingPolicy(consumer.layer, dataset);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function isDirectCategoricalConsumer(consumer) {
|
|
@@ -24,6 +24,7 @@ const CREATE_SCALE_OPTIONS = Object.freeze([
|
|
|
24
24
|
"id",
|
|
25
25
|
"type",
|
|
26
26
|
"domain",
|
|
27
|
+
"emptyDomain",
|
|
27
28
|
"range",
|
|
28
29
|
"nice",
|
|
29
30
|
"zero",
|
|
@@ -164,7 +165,7 @@ export const createScale = /* @__PURE__ */ action(
|
|
|
164
165
|
.editSemantic({ property: `scale[${id}].range`, value: definition.range });
|
|
165
166
|
|
|
166
167
|
for (const property of [
|
|
167
|
-
"nice", "zero", "clamp", "reverse", "base", "exponent", "constant",
|
|
168
|
+
"emptyDomain", "nice", "zero", "clamp", "reverse", "base", "exponent", "constant",
|
|
168
169
|
"paddingInner", "paddingOuter", "padding", "align", "interpolate", "midpoint",
|
|
169
170
|
"radialMapping", "unknown"
|
|
170
171
|
]) {
|
|
@@ -37,7 +37,7 @@ import { resolveRequestedOffsetPolicy } from
|
|
|
37
37
|
"../../materialization/scales/policies/offset.js";
|
|
38
38
|
import { findScaleConsumers } from "./consumers/index.js";
|
|
39
39
|
|
|
40
|
-
const BASE_OPTIONS = ["id", "type", "domain", "range"];
|
|
40
|
+
const BASE_OPTIONS = ["id", "type", "domain", "range", "emptyDomain"];
|
|
41
41
|
const UNKNOWN_OPTIONS = [...BASE_OPTIONS, "unknown"];
|
|
42
42
|
const CLAMP_REVERSE = ["clamp", "reverse"];
|
|
43
43
|
const BOOLEAN_OPTIONS = ["nice", "zero", ...CLAMP_REVERSE];
|
|
@@ -26,7 +26,7 @@ import { hasExactLegendSamplingForScale } from
|
|
|
26
26
|
"../guides/legends/sampling.js";
|
|
27
27
|
|
|
28
28
|
const OPTIONS = Object.freeze([
|
|
29
|
-
"id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
29
|
+
"id", "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
30
30
|
"base", "exponent", "constant", "paddingInner", "paddingOuter", "padding",
|
|
31
31
|
"align", "interpolate", "midpoint", "unknown", "palette", "radialMapping"
|
|
32
32
|
]);
|
|
@@ -6,7 +6,7 @@ import { requireLayer } from "../../selectors/layers.js";
|
|
|
6
6
|
import { requireSemanticScale } from "../../selectors/scales.js";
|
|
7
7
|
|
|
8
8
|
const QUANTITATIVE_OPTIONS = Object.freeze([
|
|
9
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
9
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
10
10
|
"base", "exponent", "constant", "unknown"
|
|
11
11
|
]);
|
|
12
12
|
const ORDINAL_OPTIONS = Object.freeze([
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const SCALE_EDIT_PROPERTIES = Object.freeze([
|
|
2
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
2
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
3
3
|
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
4
4
|
"padding", "align", "interpolate", "midpoint", "unknown"
|
|
5
5
|
]);
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
resolveScaleConsumerBounds
|
|
10
10
|
} from "../../materialization/coordinateBounds.js";
|
|
11
11
|
import { requireCoordinate } from "../../selectors/coordinates.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
rememberResolvedScaleBinding,
|
|
14
|
+
resolveScaleMaterialization
|
|
15
|
+
} from "../../materialization/scales/resolve.js";
|
|
13
16
|
import { findScale, findScaleConsumers, resolveConsumerCategoryOrder,
|
|
14
17
|
resolveConsumerValues, resolveSeriesLayoutScaleValues } from "./consumers/index.js";
|
|
15
18
|
|
|
@@ -126,6 +129,30 @@ export function resolveScalePreview(program, id) {
|
|
|
126
129
|
return resolveScalePreviewAtBounds(program, id, bounds);
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
export function restoreResolvedScaleBindings(program) {
|
|
133
|
+
for (const scale of program.semanticSpec.scales) {
|
|
134
|
+
const resolved = program.resolvedScales[scale.id];
|
|
135
|
+
if (resolved === undefined) continue;
|
|
136
|
+
const consumers = findScaleConsumers(program, scale.id);
|
|
137
|
+
const channels = new Set(consumers.map(consumer =>
|
|
138
|
+
normalizePositionScaleChannel(consumer.channel)
|
|
139
|
+
));
|
|
140
|
+
const families = new Set([...channels].map(channel =>
|
|
141
|
+
channel === "stroke" ? "color" : channel
|
|
142
|
+
));
|
|
143
|
+
if (families.size === 1) {
|
|
144
|
+
const channel = channels.size === 1
|
|
145
|
+
? channels.values().next().value
|
|
146
|
+
: families.values().next().value;
|
|
147
|
+
rememberResolvedScaleBinding(resolved, channel, consumers);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const child of Object.values(program.children)) {
|
|
151
|
+
restoreResolvedScaleBindings(child);
|
|
152
|
+
}
|
|
153
|
+
return program;
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
export function validatePendingMeasuredScale(program, scale, consumers) {
|
|
130
157
|
const bounds = resolveScaleConsumerBounds(program, consumers);
|
|
131
158
|
const range = resolveArcAutoPositionRange({ consumers, scale, channel: "radius",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
setThemeStateOverrides
|
|
7
7
|
} from "./state.js";
|
|
8
8
|
import { findLayer } from "../../selectors/layers.js";
|
|
9
|
+
import { reconcileFollowingRegressions } from "../regression/follow.js";
|
|
9
10
|
|
|
10
11
|
let rematerializeThemeHighlights = program => program;
|
|
11
12
|
|
|
@@ -979,6 +980,7 @@ function collectOverrides(program) {
|
|
|
979
980
|
}
|
|
980
981
|
|
|
981
982
|
export function reconcileProgramTheme(program, { source, metadata }) {
|
|
983
|
+
program = reconcileFollowingRegressions(program);
|
|
982
984
|
const storedState = program.materializationConfigs.theme;
|
|
983
985
|
if (storedState === undefined) return program;
|
|
984
986
|
// Data/context/trace-only transitions cannot change authored appearance.
|
package/src/core/action.js
CHANGED
|
@@ -192,7 +192,8 @@ export function action(metadata, implementation) {
|
|
|
192
192
|
const ownedMetadata = freezeOwned({
|
|
193
193
|
op: metadata.op,
|
|
194
194
|
description: metadata.description,
|
|
195
|
-
scope
|
|
195
|
+
scope,
|
|
196
|
+
...(Array.isArray(metadata.options) ? { options: [...metadata.options] } : {})
|
|
196
197
|
});
|
|
197
198
|
|
|
198
199
|
const wrappedAction = function wrappedAction(args = {}) {
|
|
@@ -240,7 +241,7 @@ export function action(metadata, implementation) {
|
|
|
240
241
|
// Private built-in convenience: action() owns the object boundary and trace,
|
|
241
242
|
// while each definition supplies its closed option vocabulary exactly once.
|
|
242
243
|
export function closedAction(metadata, options, implementation) {
|
|
243
|
-
return action(metadata, function (args = {}) {
|
|
244
|
+
return action({ ...metadata, options }, function (args = {}) {
|
|
244
245
|
if (options !== undefined) validateKeys(args, options, metadata.op);
|
|
245
246
|
return implementation.call(this, args);
|
|
246
247
|
});
|
package/src/core/programState.js
CHANGED
|
@@ -34,12 +34,16 @@ export function ownChildPrograms(children, ProgramClass) {
|
|
|
34
34
|
// domain's dependency validation and before the primitive deletion.
|
|
35
35
|
export function releaseNamedResourceOwnership(program, { kind, id, dataOwner }) {
|
|
36
36
|
if (kind !== "data") return program;
|
|
37
|
-
|
|
37
|
+
let materializationConfigs = dataOwner === undefined
|
|
38
38
|
? program.materializationConfigs
|
|
39
39
|
: removeMaterializationConfig(
|
|
40
40
|
program.materializationConfigs,
|
|
41
41
|
["data", dataOwner.family, dataOwner.owner]
|
|
42
42
|
).value;
|
|
43
|
+
materializationConfigs = removeMaterializationConfig(
|
|
44
|
+
materializationConfigs,
|
|
45
|
+
["calculations", "datasets", dataOwner?.current ?? id]
|
|
46
|
+
).value;
|
|
43
47
|
const context = program.context.currentData === id
|
|
44
48
|
? freezeOwned({ ...program.context, currentData: undefined })
|
|
45
49
|
: program.context;
|
package/src/grammar/bin.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const TRANSFORM_KEYS = Object.freeze([
|
|
17
17
|
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
18
|
-
"members", "as", "weight", "resolved"
|
|
18
|
+
"members", "as", "weight", "missing", "resolved"
|
|
19
19
|
]);
|
|
20
20
|
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
21
21
|
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
@@ -59,7 +59,8 @@ export function normalizeBinTransform({
|
|
|
59
59
|
includeEmpty,
|
|
60
60
|
members,
|
|
61
61
|
as,
|
|
62
|
-
weight
|
|
62
|
+
weight,
|
|
63
|
+
missing
|
|
63
64
|
} = {}) {
|
|
64
65
|
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
65
66
|
if (selected.length > 1) {
|
|
@@ -83,7 +84,8 @@ export function normalizeBinTransform({
|
|
|
83
84
|
as: normalizeAs(field, as, resolvedMembers),
|
|
84
85
|
...(weight === undefined
|
|
85
86
|
? {}
|
|
86
|
-
: { weight: normalizeStatisticalWeight(weight, "Bin weight") })
|
|
87
|
+
: { weight: normalizeStatisticalWeight(weight, "Bin weight") }),
|
|
88
|
+
...(missing === undefined ? {} : { missing })
|
|
87
89
|
};
|
|
88
90
|
validateBinTransform(transform);
|
|
89
91
|
return cloneAndFreeze(transform);
|
|
@@ -101,6 +103,9 @@ export function validateBinTransform(transform) {
|
|
|
101
103
|
if (transform.weight !== undefined) {
|
|
102
104
|
normalizeStatisticalWeight(transform.weight, "Bin weight");
|
|
103
105
|
}
|
|
106
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
107
|
+
throw new Error('Bin missing must be "error" or "drop".');
|
|
108
|
+
}
|
|
104
109
|
normalizeHistogramBin(transform.bin);
|
|
105
110
|
if (
|
|
106
111
|
transform.extent !== "auto" &&
|
|
@@ -142,22 +147,31 @@ export function validateBinTransform(transform) {
|
|
|
142
147
|
return transform;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
function readValues(rows, field) {
|
|
146
|
-
|
|
150
|
+
function readValues(rows, field, missing) {
|
|
151
|
+
const eligible = [];
|
|
152
|
+
let excluded = 0;
|
|
153
|
+
rows.forEach((row, index) => {
|
|
147
154
|
const value = row[field];
|
|
155
|
+
if (value === null || value === undefined) {
|
|
156
|
+
if (missing === "drop") { excluded += 1; return; }
|
|
157
|
+
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
158
|
+
}
|
|
148
159
|
if (!Number.isFinite(value)) {
|
|
149
160
|
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
150
161
|
}
|
|
151
|
-
|
|
162
|
+
eligible.push({ row, index });
|
|
152
163
|
});
|
|
164
|
+
return { eligible, excluded };
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
export function deriveBinRows(rows, transform) {
|
|
156
168
|
validateBinTransform(transform);
|
|
157
|
-
const
|
|
169
|
+
const selected = readValues(rows, transform.field, transform.missing);
|
|
170
|
+
const eligibleRows = selected.eligible.map(entry => entry.row);
|
|
171
|
+
const values = eligibleRows.map(row => row[transform.field]);
|
|
158
172
|
const weights = transform.weight === undefined
|
|
159
173
|
? undefined
|
|
160
|
-
: readStatisticalWeights(
|
|
174
|
+
: readStatisticalWeights(eligibleRows, transform.weight, "Bin");
|
|
161
175
|
if (weights !== undefined) {
|
|
162
176
|
validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
|
|
163
177
|
}
|
|
@@ -188,7 +202,7 @@ export function deriveBinRows(rows, transform) {
|
|
|
188
202
|
});
|
|
189
203
|
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
190
204
|
const entries = weightSummary === undefined
|
|
191
|
-
?
|
|
205
|
+
? selected.eligible
|
|
192
206
|
: weightSummary.positive;
|
|
193
207
|
entries.forEach(entry => {
|
|
194
208
|
const value = entry.row[transform.field];
|
|
@@ -217,7 +231,22 @@ export function deriveBinRows(rows, transform) {
|
|
|
217
231
|
domain: resolved.domain,
|
|
218
232
|
...(resolved.step === undefined ? {} : { step: resolved.step }),
|
|
219
233
|
boundaries: resolved.boundaries
|
|
220
|
-
}
|
|
234
|
+
},
|
|
235
|
+
...(transform.missing === undefined ? {} : {
|
|
236
|
+
report: {
|
|
237
|
+
version: 1,
|
|
238
|
+
owner: { kind: "data", id: "pending" },
|
|
239
|
+
inputs: [],
|
|
240
|
+
units: [{
|
|
241
|
+
role: transform.as.count,
|
|
242
|
+
group: {},
|
|
243
|
+
inputRows: rows.length,
|
|
244
|
+
usedRows: eligibleRows.length,
|
|
245
|
+
excludedRows: selected.excluded,
|
|
246
|
+
excludedByReason: selected.excluded === 0 ? {} : { "missing-value": selected.excluded }
|
|
247
|
+
}]
|
|
248
|
+
}
|
|
249
|
+
})
|
|
221
250
|
};
|
|
222
251
|
}
|
|
223
252
|
|