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
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
import { resolveEligibleLayer } from "../../../selectors/layers.js";
|
|
14
14
|
import { rematerializeExistingLegend } from "../../encodings/shared.js";
|
|
15
15
|
import { validateMarkOptions } from "../shared.js";
|
|
16
|
+
import { validateItemMissing } from "../../../grammar/itemMissing.js";
|
|
16
17
|
|
|
17
18
|
const OPTIONS = Object.freeze([
|
|
18
|
-
"target", "shape", "fill", "opacity", "stroke", "strokeWidth",
|
|
19
|
+
"target", "missing", "shape", "fill", "opacity", "stroke", "strokeWidth",
|
|
19
20
|
...STROKE_STYLE_PROPERTIES
|
|
20
21
|
]);
|
|
21
22
|
|
|
@@ -28,12 +29,12 @@ export const editPointMark = /* @__PURE__ */ action(
|
|
|
28
29
|
validateMarkOptions(args, OPTIONS, "editPointMark");
|
|
29
30
|
const strokeDetails = requestedStrokeDetails(args, "editPointMark");
|
|
30
31
|
const editable = [
|
|
31
|
-
"shape", "fill", "opacity", "stroke", "strokeWidth",
|
|
32
|
+
"missing", "shape", "fill", "opacity", "stroke", "strokeWidth",
|
|
32
33
|
...STROKE_STYLE_PROPERTIES
|
|
33
34
|
];
|
|
34
35
|
if (!editable.some(property => Object.hasOwn(args, property))) {
|
|
35
36
|
throw new Error(
|
|
36
|
-
"editPointMark requires shape, fill, opacity, stroke, or
|
|
37
|
+
"editPointMark requires shape, fill, opacity, stroke, strokeWidth, or missing."
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
if (args.target !== undefined) validateUserId(args.target, "Point mark id");
|
|
@@ -61,7 +62,13 @@ export const editPointMark = /* @__PURE__ */ action(
|
|
|
61
62
|
if (Object.hasOwn(args, "opacity") && layer.encoding?.opacity?.field !== undefined) {
|
|
62
63
|
throw new Error("editPointMark opacity conflicts with a field encoding; use encodeOpacity with value to replace it.");
|
|
63
64
|
}
|
|
64
|
-
const
|
|
65
|
+
const semantic = Object.hasOwn(args, "missing")
|
|
66
|
+
? this.editSemantic({
|
|
67
|
+
property: `layer[${id}].mark.missing`,
|
|
68
|
+
value: validateItemMissing(args.missing, "Point missing")
|
|
69
|
+
})
|
|
70
|
+
: this;
|
|
71
|
+
const config = { ...semantic.markConfigs[id] };
|
|
65
72
|
Object.assign(config, strokeDetails);
|
|
66
73
|
if (Object.hasOwn(args, "shape")) {
|
|
67
74
|
config.shape = validatePointShape(args.shape);
|
|
@@ -97,7 +104,7 @@ export const editPointMark = /* @__PURE__ */ action(
|
|
|
97
104
|
"Point strokeWidth"
|
|
98
105
|
);
|
|
99
106
|
}
|
|
100
|
-
const next =
|
|
107
|
+
const next = semantic
|
|
101
108
|
._withMarkConfig(id, config)
|
|
102
109
|
.rematerializePointMark({ id });
|
|
103
110
|
return rematerializeExistingLegend(next);
|
|
@@ -40,6 +40,7 @@ import { offsetCategoryPositions } from
|
|
|
40
40
|
"../../../materialization/categorySlotOffset.js";
|
|
41
41
|
import { requestedStrokeDetails } from
|
|
42
42
|
"../../../grammar/strokeStyle.js";
|
|
43
|
+
import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
|
|
43
44
|
|
|
44
45
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
45
46
|
const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
|
|
@@ -337,10 +338,11 @@ export const rematerializePointMark = /* @__PURE__ */ action(
|
|
|
337
338
|
if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
|
|
338
339
|
throw new Error(`Point mark "${id}" requires point graphics.`);
|
|
339
340
|
}
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
341
|
+
const sourceDataset = findDataset(resolved, layer.data);
|
|
342
|
+
if (sourceDataset === undefined) {
|
|
342
343
|
throw new Error(`Point mark "${id}" requires an existing dataset.`);
|
|
343
344
|
}
|
|
345
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
344
346
|
|
|
345
347
|
if (graphic.items.length === 0 && !canMaterializePoint(resolved, layer)) {
|
|
346
348
|
return resolved;
|
|
@@ -29,13 +29,15 @@ import {
|
|
|
29
29
|
} from "../../../grammar/roundedRect.js";
|
|
30
30
|
import { STROKE_STYLE_PROPERTIES } from
|
|
31
31
|
"../../../grammar/strokeStyle.js";
|
|
32
|
+
import { applyItemMissingPolicy, validateItemMissing } from
|
|
33
|
+
"../../../grammar/itemMissing.js";
|
|
32
34
|
|
|
33
35
|
const STYLE_OPTIONS = Object.freeze([
|
|
34
36
|
"fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
|
|
35
37
|
...STROKE_STYLE_PROPERTIES
|
|
36
38
|
]);
|
|
37
|
-
const CREATE_OPTIONS = Object.freeze(["id", "data", ...STYLE_OPTIONS]);
|
|
38
|
-
const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
|
|
39
|
+
const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...STYLE_OPTIONS]);
|
|
40
|
+
const EDIT_OPTIONS = Object.freeze(["target", "missing", ...STYLE_OPTIONS]);
|
|
39
41
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id", "scales"]);
|
|
40
42
|
|
|
41
43
|
function requireRectLayer(program, requested, operation) {
|
|
@@ -71,6 +73,12 @@ const createRectMark = /* @__PURE__ */ action(
|
|
|
71
73
|
let next = this
|
|
72
74
|
.editSemantic({ property: `layer[${id}].mark.type`, value: "rect" })
|
|
73
75
|
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
76
|
+
if (Object.hasOwn(args, "missing")) {
|
|
77
|
+
next = next.editSemantic({
|
|
78
|
+
property: `layer[${id}].mark.missing`,
|
|
79
|
+
value: validateItemMissing(args.missing, "Rect missing")
|
|
80
|
+
});
|
|
81
|
+
}
|
|
74
82
|
next = applyLayeredMarkInheritance(next, id, inherited)
|
|
75
83
|
.createGraphics({
|
|
76
84
|
id,
|
|
@@ -120,10 +128,11 @@ const rematerializeRectMark = /* @__PURE__ */ action(
|
|
|
120
128
|
if (this.markConfigs[id]?.gradientPlot?.materialized === true) {
|
|
121
129
|
return this.materializeGradientPlotFill({ id });
|
|
122
130
|
}
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
131
|
+
const sourceDataset = findDataset(this, layer.data);
|
|
132
|
+
if (sourceDataset === undefined) {
|
|
125
133
|
throw new Error(`Rect mark "${id}" requires an existing dataset.`);
|
|
126
134
|
}
|
|
135
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
127
136
|
if (!canMaterializeRect(this, layer)) {
|
|
128
137
|
return replaceGraphicItems(this, id, "rect", []);
|
|
129
138
|
}
|
|
@@ -151,7 +160,7 @@ const editRectMark = /* @__PURE__ */ action(
|
|
|
151
160
|
function (args = {}) {
|
|
152
161
|
validateMarkOptions(args, EDIT_OPTIONS, "editRectMark");
|
|
153
162
|
requestedRectStyleDetails(args, "editRectMark");
|
|
154
|
-
if (!STYLE_OPTIONS.some(option => Object.hasOwn(args, option))) {
|
|
163
|
+
if (!["missing", ...STYLE_OPTIONS].some(option => Object.hasOwn(args, option))) {
|
|
155
164
|
throw new Error("editRectMark requires at least one editable property.");
|
|
156
165
|
}
|
|
157
166
|
const layer = requireRectLayer(this, args.target, "editRectMark");
|
|
@@ -163,16 +172,22 @@ const editRectMark = /* @__PURE__ */ action(
|
|
|
163
172
|
"editRectMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
|
|
164
173
|
);
|
|
165
174
|
}
|
|
166
|
-
const
|
|
175
|
+
const semantic = Object.hasOwn(args, "missing")
|
|
176
|
+
? this.editSemantic({
|
|
177
|
+
property: `layer[${layer.id}].mark.missing`,
|
|
178
|
+
value: validateItemMissing(args.missing, "Rect missing")
|
|
179
|
+
})
|
|
180
|
+
: this;
|
|
181
|
+
const next = semantic._withMarkConfig(
|
|
167
182
|
layer.id,
|
|
168
183
|
{
|
|
169
184
|
...normalizeRectMarkConfig(
|
|
170
185
|
args,
|
|
171
|
-
|
|
186
|
+
semantic.markConfigs[layer.id] ?? DEFAULT_RECT_MARK
|
|
172
187
|
),
|
|
173
188
|
fillExplicit: Object.hasOwn(args, "fill")
|
|
174
189
|
? true
|
|
175
|
-
:
|
|
190
|
+
: semantic.markConfigs[layer.id]?.fillExplicit ?? false
|
|
176
191
|
}
|
|
177
192
|
);
|
|
178
193
|
const materialized = canMaterializeRect(next, layer)
|
|
@@ -39,13 +39,15 @@ import {
|
|
|
39
39
|
requestedStrokeDetails,
|
|
40
40
|
STROKE_STYLE_PROPERTIES
|
|
41
41
|
} from "../../../grammar/strokeStyle.js";
|
|
42
|
+
import { applyItemMissingPolicy, validateItemMissing } from
|
|
43
|
+
"../../../grammar/itemMissing.js";
|
|
42
44
|
|
|
43
45
|
const APPEARANCE_OPTIONS = Object.freeze([
|
|
44
46
|
"stroke", "strokeWidth", "strokeDash", "opacity",
|
|
45
47
|
...STROKE_STYLE_PROPERTIES
|
|
46
48
|
]);
|
|
47
|
-
const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
|
|
48
|
-
const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
|
|
49
|
+
const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...APPEARANCE_OPTIONS]);
|
|
50
|
+
const EDIT_OPTIONS = Object.freeze(["target", "missing", ...APPEARANCE_OPTIONS]);
|
|
49
51
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
50
52
|
const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
|
|
51
53
|
const DEFAULT_RULE_CONFIG = Object.freeze({
|
|
@@ -66,10 +68,11 @@ function requireRule(program, id) {
|
|
|
66
68
|
if (layer?.mark?.type !== "rule") {
|
|
67
69
|
throw new Error(`Unknown rule mark "${id}".`);
|
|
68
70
|
}
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
+
const sourceDataset = findDataset(program, layer.data);
|
|
72
|
+
if (sourceDataset === undefined) {
|
|
71
73
|
throw new Error(`Rule mark "${id}" requires an existing dataset.`);
|
|
72
74
|
}
|
|
75
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
73
76
|
const graphic = program.graphicSpec.objects[id];
|
|
74
77
|
if (graphic?.type !== "line" || graphic.items === undefined) {
|
|
75
78
|
throw new Error(`Rule mark "${id}" requires line collection graphics.`);
|
|
@@ -118,6 +121,12 @@ const createRuleMark = /* @__PURE__ */ action(
|
|
|
118
121
|
let created = this
|
|
119
122
|
.editSemantic({ property: `layer[${id}].mark.type`, value: "rule" })
|
|
120
123
|
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
124
|
+
if (Object.hasOwn(args, "missing")) {
|
|
125
|
+
created = created.editSemantic({
|
|
126
|
+
property: `layer[${id}].mark.missing`,
|
|
127
|
+
value: validateItemMissing(args.missing, "Rule missing")
|
|
128
|
+
});
|
|
129
|
+
}
|
|
121
130
|
created = applyLayeredMarkInheritance(created, id, inherited);
|
|
122
131
|
created = created
|
|
123
132
|
.createGraphics({
|
|
@@ -154,11 +163,18 @@ const editRuleMark = /* @__PURE__ */ action(
|
|
|
154
163
|
label: "rule mark"
|
|
155
164
|
});
|
|
156
165
|
const appearance = planRuleAppearance(args, layer);
|
|
157
|
-
if (appearance.length === 0 && Object.keys(strokeDetails).length === 0
|
|
158
|
-
|
|
166
|
+
if (appearance.length === 0 && Object.keys(strokeDetails).length === 0 &&
|
|
167
|
+
!Object.hasOwn(args, "missing")) {
|
|
168
|
+
throw new Error("editRuleMark requires missing or an appearance change.");
|
|
159
169
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
170
|
+
const semantic = Object.hasOwn(args, "missing")
|
|
171
|
+
? this.editSemantic({
|
|
172
|
+
property: `layer[${layer.id}].mark.missing`,
|
|
173
|
+
value: validateItemMissing(args.missing, "Rule missing")
|
|
174
|
+
})
|
|
175
|
+
: this;
|
|
176
|
+
const configured = semantic._withMarkConfig(layer.id, {
|
|
177
|
+
...semantic.markConfigs[layer.id],
|
|
162
178
|
...strokeDetails
|
|
163
179
|
});
|
|
164
180
|
const materialized = appearance.length === 0
|
|
@@ -39,13 +39,14 @@ import {
|
|
|
39
39
|
} from "../../charts/shared.js";
|
|
40
40
|
import { resolveMarkGraphicPlacement } from
|
|
41
41
|
"../../../materialization/graphicHierarchy.js";
|
|
42
|
+
import { validateItemMissing } from "../../../grammar/itemMissing.js";
|
|
42
43
|
|
|
43
44
|
const STYLE_OPTIONS = Object.freeze([
|
|
44
45
|
"fill", "opacity", "fontSize", "fontFamily", "fontWeight",
|
|
45
46
|
"align", "baseline", "rotation", "dx", "dy"
|
|
46
47
|
]);
|
|
47
|
-
const CREATE_OPTIONS = Object.freeze(["id", "data", "source", "text", ...STYLE_OPTIONS]);
|
|
48
|
-
const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
|
|
48
|
+
const CREATE_OPTIONS = Object.freeze(["id", "data", "source", "text", "missing", ...STYLE_OPTIONS]);
|
|
49
|
+
const EDIT_OPTIONS = Object.freeze(["target", "missing", ...STYLE_OPTIONS]);
|
|
49
50
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id", "replayLayout"]);
|
|
50
51
|
|
|
51
52
|
function sourceMatchesData(program, layer, requestedData) {
|
|
@@ -139,6 +140,9 @@ const createTextMark = /* @__PURE__ */ action(
|
|
|
139
140
|
operation: "createTextMark"
|
|
140
141
|
});
|
|
141
142
|
const inherited = resolveTextInheritance(this, args);
|
|
143
|
+
if (Object.hasOwn(args, "missing") && inherited?.source !== undefined) {
|
|
144
|
+
throw new Error("Text missing policy requires a row-backed text mark, not source-owned labels.");
|
|
145
|
+
}
|
|
142
146
|
const { data } = resolveMarkData(this, {
|
|
143
147
|
...args,
|
|
144
148
|
...(args.data === undefined && inherited?.data !== undefined
|
|
@@ -150,6 +154,12 @@ const createTextMark = /* @__PURE__ */ action(
|
|
|
150
154
|
let next = this
|
|
151
155
|
.editSemantic({ property: `layer[${id}].mark.type`, value: "text" })
|
|
152
156
|
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
157
|
+
if (Object.hasOwn(args, "missing")) {
|
|
158
|
+
next = next.editSemantic({
|
|
159
|
+
property: `layer[${id}].mark.missing`,
|
|
160
|
+
value: validateItemMissing(args.missing, "Text missing")
|
|
161
|
+
});
|
|
162
|
+
}
|
|
153
163
|
if (inherited?.source !== undefined) {
|
|
154
164
|
next = next.editSemantic({
|
|
155
165
|
property: `layer[${id}].source`,
|
|
@@ -465,20 +475,29 @@ const editTextMark = /* @__PURE__ */ action(
|
|
|
465
475
|
},
|
|
466
476
|
function (args = {}) {
|
|
467
477
|
validateMarkOptions(args, EDIT_OPTIONS, "editTextMark");
|
|
468
|
-
if (!STYLE_OPTIONS.some(option => Object.hasOwn(args, option))) {
|
|
478
|
+
if (!["missing", ...STYLE_OPTIONS].some(option => Object.hasOwn(args, option))) {
|
|
469
479
|
throw new Error("editTextMark requires at least one editable property.");
|
|
470
480
|
}
|
|
471
481
|
const layer = requireTextLayer(this, args.target, "editTextMark");
|
|
472
|
-
|
|
482
|
+
if (Object.hasOwn(args, "missing") && layer.source !== undefined) {
|
|
483
|
+
throw new Error("Text missing policy requires a row-backed text mark, not source-owned labels.");
|
|
484
|
+
}
|
|
485
|
+
const semantic = Object.hasOwn(args, "missing")
|
|
486
|
+
? this.editSemantic({
|
|
487
|
+
property: `layer[${layer.id}].mark.missing`,
|
|
488
|
+
value: validateItemMissing(args.missing, "Text missing")
|
|
489
|
+
})
|
|
490
|
+
: this;
|
|
491
|
+
const next = semantic._withMarkConfig(
|
|
473
492
|
layer.id,
|
|
474
493
|
{
|
|
475
494
|
...normalizeTextMarkConfig(
|
|
476
495
|
args,
|
|
477
|
-
|
|
496
|
+
semantic.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
|
|
478
497
|
),
|
|
479
498
|
fillExplicit: Object.hasOwn(args, "fill")
|
|
480
499
|
? true
|
|
481
|
-
:
|
|
500
|
+
: semantic.markConfigs[layer.id]?.fillExplicit ?? false
|
|
482
501
|
}
|
|
483
502
|
);
|
|
484
503
|
return canMaterializeText(next, layer)
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
requestedStrokeDetails,
|
|
22
22
|
STROKE_STYLE_PROPERTIES
|
|
23
23
|
} from "../../../grammar/strokeStyle.js";
|
|
24
|
+
import { applyItemMissingPolicy, validateItemMissing } from
|
|
25
|
+
"../../../grammar/itemMissing.js";
|
|
24
26
|
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
25
27
|
import { rematerializeExistingLegend } from "../../encodings/shared.js";
|
|
26
28
|
import {
|
|
@@ -37,8 +39,8 @@ import {
|
|
|
37
39
|
const APPEARANCE_OPTIONS = Object.freeze([
|
|
38
40
|
"length", "stroke", "strokeWidth", "opacity", ...STROKE_STYLE_PROPERTIES
|
|
39
41
|
]);
|
|
40
|
-
const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
|
|
41
|
-
const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
|
|
42
|
+
const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...APPEARANCE_OPTIONS]);
|
|
43
|
+
const EDIT_OPTIONS = Object.freeze(["target", "missing", ...APPEARANCE_OPTIONS]);
|
|
42
44
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
43
45
|
|
|
44
46
|
export const DEFAULT_TICK_CONFIG = Object.freeze({
|
|
@@ -114,6 +116,12 @@ export const createTickMark = /* @__PURE__ */ action(
|
|
|
114
116
|
let created = this
|
|
115
117
|
.editSemantic({ property: `layer[${id}].mark.type`, value: "tick" })
|
|
116
118
|
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
119
|
+
if (Object.hasOwn(args, "missing")) {
|
|
120
|
+
created = created.editSemantic({
|
|
121
|
+
property: `layer[${id}].mark.missing`,
|
|
122
|
+
value: validateItemMissing(args.missing, "Tick missing")
|
|
123
|
+
});
|
|
124
|
+
}
|
|
117
125
|
created = applyLayeredMarkInheritance(created, id, inherited)
|
|
118
126
|
.createGraphics({
|
|
119
127
|
id,
|
|
@@ -141,9 +149,9 @@ export const editTickMark = /* @__PURE__ */ action(
|
|
|
141
149
|
function (args = {}) {
|
|
142
150
|
validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
|
|
143
151
|
requestedStrokeDetails(args, "editTickMark");
|
|
144
|
-
if (!APPEARANCE_OPTIONS.some(property => Object.hasOwn(args, property))) {
|
|
152
|
+
if (!["missing", ...APPEARANCE_OPTIONS].some(property => Object.hasOwn(args, property))) {
|
|
145
153
|
throw new Error(
|
|
146
|
-
"editTickMark requires length, stroke, strokeWidth, or
|
|
154
|
+
"editTickMark requires length, stroke, strokeWidth, opacity, or missing."
|
|
147
155
|
);
|
|
148
156
|
}
|
|
149
157
|
const layer = resolveTick(this, args.target, "editTickMark");
|
|
@@ -152,11 +160,17 @@ export const editTickMark = /* @__PURE__ */ action(
|
|
|
152
160
|
"editTickMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
|
|
153
161
|
);
|
|
154
162
|
}
|
|
163
|
+
const semantic = Object.hasOwn(args, "missing")
|
|
164
|
+
? this.editSemantic({
|
|
165
|
+
property: `layer[${layer.id}].mark.missing`,
|
|
166
|
+
value: validateItemMissing(args.missing, "Tick missing")
|
|
167
|
+
})
|
|
168
|
+
: this;
|
|
155
169
|
const config = validateTickConfig(args, {
|
|
156
170
|
...DEFAULT_TICK_CONFIG,
|
|
157
|
-
...
|
|
171
|
+
...semantic.markConfigs[layer.id]
|
|
158
172
|
});
|
|
159
|
-
const materialized =
|
|
173
|
+
const materialized = semantic
|
|
160
174
|
._withMarkConfig(layer.id, config)
|
|
161
175
|
.rematerializeTickMark({ id: layer.id });
|
|
162
176
|
return rematerializeExistingLegend(materialized);
|
|
@@ -190,10 +204,11 @@ export const rematerializeTickMark = /* @__PURE__ */ action(
|
|
|
190
204
|
if (graphic?.type !== "line" || !Array.isArray(graphic.items)) {
|
|
191
205
|
throw new Error(`Tick mark "${id}" requires line collection graphics.`);
|
|
192
206
|
}
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
207
|
+
const sourceDataset = findDataset(this, layer.data);
|
|
208
|
+
if (sourceDataset === undefined) {
|
|
195
209
|
throw new Error(`Tick mark "${id}" requires an existing dataset.`);
|
|
196
210
|
}
|
|
211
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
197
212
|
if (
|
|
198
213
|
layer.encoding?.x?.scale === undefined ||
|
|
199
214
|
layer.encoding?.y?.scale === undefined
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
removeOwnedPath
|
|
8
8
|
} from "../../core/immutable.js";
|
|
9
9
|
import { parseSemanticPath } from "../../grammar/schemas/semanticPath.js";
|
|
10
|
+
import { deriveTransformSchema, inferDatasetSchema, validateRowsAgainstSchema } from "../../grammar/datasetSchema.js";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
const CONTEXT_KEYS = Object.freeze({
|
|
@@ -44,7 +45,39 @@ function updateEntity(spec, parsed, value) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const current = index === -1 ? { id: parsed.id } : collection[index];
|
|
47
|
-
|
|
48
|
+
let updated = setNestedProperty(current, parsed.path, value);
|
|
49
|
+
if (
|
|
50
|
+
parsed.kind === "dataset" && parsed.path.length === 1 &&
|
|
51
|
+
parsed.path[0] === "transform" && current.schema === undefined &&
|
|
52
|
+
current.source !== undefined && value.length === 1
|
|
53
|
+
) {
|
|
54
|
+
const source = collection.find(dataset => dataset.id === current.source);
|
|
55
|
+
if (source?.schema !== undefined) {
|
|
56
|
+
updated = setNestedProperty(updated, ["schema"], deriveTransformSchema(
|
|
57
|
+
source.schema,
|
|
58
|
+
value[0],
|
|
59
|
+
undefined,
|
|
60
|
+
{ sourceId: source.id, ownerId: parsed.id }
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
parsed.kind === "dataset" && parsed.path.length === 1 &&
|
|
66
|
+
parsed.path[0] === "values" &&
|
|
67
|
+
(current.schema === undefined || current.schema.origin === "derived")
|
|
68
|
+
) {
|
|
69
|
+
const source = current.source === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: collection.find(dataset => dataset.id === current.source);
|
|
72
|
+
const schema = source?.schema !== undefined && current.transform?.length === 1
|
|
73
|
+
? deriveTransformSchema(source.schema, current.transform[0], value, {
|
|
74
|
+
sourceId: source.id,
|
|
75
|
+
ownerId: parsed.id
|
|
76
|
+
})
|
|
77
|
+
: inferDatasetSchema(value);
|
|
78
|
+
validateRowsAgainstSchema(value, schema, `Dataset "${parsed.id}"`);
|
|
79
|
+
updated = setNestedProperty(updated, ["schema"], schema);
|
|
80
|
+
}
|
|
48
81
|
const nextCollection = [...collection];
|
|
49
82
|
|
|
50
83
|
if (index === -1) {
|
|
@@ -155,13 +188,14 @@ export function withRematerializedDerivedDataset(program, {
|
|
|
155
188
|
id,
|
|
156
189
|
source,
|
|
157
190
|
transform,
|
|
158
|
-
values
|
|
191
|
+
values,
|
|
192
|
+
schema
|
|
159
193
|
}) {
|
|
160
194
|
const semanticSpec = {
|
|
161
195
|
...program.semanticSpec,
|
|
162
196
|
datasets: program.semanticSpec.datasets.map(dataset =>
|
|
163
197
|
dataset.id === id
|
|
164
|
-
? { ...dataset, source, transform: [transform], values }
|
|
198
|
+
? { ...dataset, source, transform: [transform], values, ...(schema === undefined ? {} : { schema }) }
|
|
165
199
|
: dataset
|
|
166
200
|
)
|
|
167
201
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
2
|
import { validateDataRows } from "../../../core/validation.js";
|
|
3
|
-
import { hasDataset } from "../../../selectors/datasets.js";
|
|
3
|
+
import { findDataset, hasDataset } from "../../../selectors/datasets.js";
|
|
4
|
+
import {
|
|
5
|
+
validateDatasetSchema,
|
|
6
|
+
validateRowsAgainstSchema
|
|
7
|
+
} from "../../../grammar/datasetSchema.js";
|
|
4
8
|
|
|
5
9
|
export function validateDatasetSemanticValue(
|
|
6
10
|
program,
|
|
@@ -11,6 +15,14 @@ export function validateDatasetSemanticValue(
|
|
|
11
15
|
const property = parsed.path[0];
|
|
12
16
|
if (property === "values") {
|
|
13
17
|
validateDataRows(value, `Dataset "${parsed.id}"`);
|
|
18
|
+
const schema = findDataset(program, parsed.id)?.schema;
|
|
19
|
+
if (schema !== undefined && schema.origin !== "derived") {
|
|
20
|
+
validateRowsAgainstSchema(value, schema, `Dataset "${parsed.id}"`);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (property === "schema") {
|
|
25
|
+
validateDatasetSchema(value);
|
|
14
26
|
return;
|
|
15
27
|
}
|
|
16
28
|
if (property === "source") {
|
|
@@ -19,6 +19,8 @@ import { findLayer } from "../../../selectors/layers.js";
|
|
|
19
19
|
import { validateNonEmptySemanticString } from "./shared.js";
|
|
20
20
|
import { normalizeStatisticalWeight } from
|
|
21
21
|
"../../../grammar/weightedStatistics.js";
|
|
22
|
+
import { ITEM_MISSING_MARK_TYPES, validateItemMissing } from
|
|
23
|
+
"../../../grammar/itemMissing.js";
|
|
22
24
|
|
|
23
25
|
function validateLayerSource(program, parsed, value, sourceMarkTypes) {
|
|
24
26
|
validateUserId(value, "Layer source id");
|
|
@@ -52,8 +54,26 @@ export function validateLayerSemanticValue(
|
|
|
52
54
|
if (property === "mark.type" && !MARK_TYPES.includes(value)) {
|
|
53
55
|
throw new Error(`Unknown mark type "${value}".`);
|
|
54
56
|
}
|
|
55
|
-
if (property === "mark.missing"
|
|
56
|
-
|
|
57
|
+
if (property === "mark.missing") {
|
|
58
|
+
const markType = findLayer(program, parsed.id)?.mark?.type;
|
|
59
|
+
if (markType === "area") {
|
|
60
|
+
if (!["error", "break"].includes(value)) {
|
|
61
|
+
throw new Error(`Unsupported area missing policy "${value}".`);
|
|
62
|
+
}
|
|
63
|
+
} else if (ITEM_MISSING_MARK_TYPES.includes(markType)) {
|
|
64
|
+
validateItemMissing(value, `${markType} missing`);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`Mark type "${markType}" does not support a missing policy.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (property === "derivedBindings.regression") {
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
71
|
+
Object.keys(value).length !== 2 || value.mode !== "follow" ||
|
|
72
|
+
!Array.isArray(value.roles) || value.roles.length !== 3 ||
|
|
73
|
+
value.roles.join(",") !== "data,x,y") {
|
|
74
|
+
throw new Error("Regression derived binding must follow data, x, and y roles.");
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
57
77
|
}
|
|
58
78
|
if (property === "layout.mode" && !COLOR_LAYOUTS.includes(value)) {
|
|
59
79
|
throw new Error(`Unsupported series layout "${value}".`);
|
|
@@ -55,6 +55,12 @@ export function validateScaleSemanticValue(program, parsed, value) {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
if (property === "domain") return validateSemanticScaleDomain(value);
|
|
58
|
+
if (property === "emptyDomain") {
|
|
59
|
+
if (!["preserve", "require-explicit"].includes(value)) {
|
|
60
|
+
throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
58
64
|
if (property === "range") return validateSemanticScaleRange(value);
|
|
59
65
|
if (["nice", "zero", "clamp", "reverse"].includes(property)) {
|
|
60
66
|
if (typeof value !== "boolean") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closedAction } from "../../core/action.js";
|
|
2
2
|
import { validateKeys } from "../../core/validation.js";
|
|
3
|
-
import { normalizeRegressionParameters } from "../../grammar/regression/index.js";
|
|
3
|
+
import { normalizeRegressionParameters, normalizeRegressionPredict } from "../../grammar/regression/index.js";
|
|
4
4
|
import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
|
|
5
5
|
import {
|
|
6
6
|
findRegressionPoint,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
const REGRESSION_OPTIONS = Object.freeze([
|
|
13
13
|
"target", "x", "y", "groupBy", "method", "degree", "span",
|
|
14
14
|
"confidenceMethod", "level", "confidence", "interval", "band", "line"
|
|
15
|
+
, "predict", "sourceBinding", "missing"
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
export const createRegression = /* @__PURE__ */ closedAction(
|
|
@@ -20,6 +21,10 @@ export const createRegression = /* @__PURE__ */ closedAction(
|
|
|
20
21
|
description: "Fit and layer regression lines with optional interval bands."
|
|
21
22
|
}, REGRESSION_OPTIONS,
|
|
22
23
|
function (args = {}) {
|
|
24
|
+
const sourceBinding = args.sourceBinding ?? "fixed";
|
|
25
|
+
if (!["fixed", "follow"].includes(sourceBinding)) {
|
|
26
|
+
throw new Error('Regression sourceBinding must be "fixed" or "follow".');
|
|
27
|
+
}
|
|
23
28
|
const point = findRegressionPoint(this, args.target);
|
|
24
29
|
const x = args.x === undefined
|
|
25
30
|
? point.encoding.x.field
|
|
@@ -37,11 +42,21 @@ export const createRegression = /* @__PURE__ */ closedAction(
|
|
|
37
42
|
);
|
|
38
43
|
}
|
|
39
44
|
const parameters = normalizeRegressionParameters(args);
|
|
45
|
+
const predict = normalizeRegressionPredict(args.predict);
|
|
46
|
+
const regressionDefinition = {
|
|
47
|
+
...parameters,
|
|
48
|
+
...(predict === undefined ? {} : { predict }),
|
|
49
|
+
...(args.missing === undefined ? {} : { missing: args.missing })
|
|
50
|
+
};
|
|
40
51
|
let band = false;
|
|
41
52
|
if (parameters.method === "loess") {
|
|
42
53
|
if (args.band !== undefined && args.band !== false) {
|
|
43
54
|
throw new Error("LOESS regression does not support a band object.");
|
|
44
55
|
}
|
|
56
|
+
} else if (parameters.interval === false) {
|
|
57
|
+
if (args.band !== undefined && args.band !== false) {
|
|
58
|
+
throw new Error("Regression interval false cannot include a band object.");
|
|
59
|
+
}
|
|
45
60
|
} else if (args.band !== false) {
|
|
46
61
|
band = requireRegressionObject(args.band ?? {}, "Regression band");
|
|
47
62
|
}
|
|
@@ -78,7 +93,7 @@ export const createRegression = /* @__PURE__ */ closedAction(
|
|
|
78
93
|
x,
|
|
79
94
|
y,
|
|
80
95
|
...(groupBy === undefined ? {} : { groupBy }),
|
|
81
|
-
...
|
|
96
|
+
...regressionDefinition
|
|
82
97
|
});
|
|
83
98
|
if (band !== false) {
|
|
84
99
|
next = next.createRegressionBand({
|
|
@@ -105,7 +120,7 @@ export const createRegression = /* @__PURE__ */ closedAction(
|
|
|
105
120
|
yScale,
|
|
106
121
|
...line
|
|
107
122
|
});
|
|
108
|
-
|
|
123
|
+
next = next._withMarkConfig(point.id, {
|
|
109
124
|
...next.markConfigs[point.id],
|
|
110
125
|
regression: {
|
|
111
126
|
source: point.data,
|
|
@@ -119,8 +134,14 @@ export const createRegression = /* @__PURE__ */ closedAction(
|
|
|
119
134
|
dataId,
|
|
120
135
|
bandId: band === false ? undefined : bandId,
|
|
121
136
|
lineId,
|
|
122
|
-
parameters
|
|
137
|
+
parameters: regressionDefinition
|
|
123
138
|
}
|
|
124
139
|
});
|
|
140
|
+
return sourceBinding === "follow"
|
|
141
|
+
? next.editSemantic({
|
|
142
|
+
property: `layer[${point.id}].derivedBindings.regression`,
|
|
143
|
+
value: { mode: "follow", roles: ["data", "x", "y"] }
|
|
144
|
+
})
|
|
145
|
+
: next;
|
|
125
146
|
}
|
|
126
147
|
);
|