ggaction 0.0.2 → 0.0.3
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 +28 -0
- package/README.md +12 -1
- package/package.json +6 -1
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/encodings/appearance.js +11 -0
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color.js +120 -25
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +90 -72
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +57 -8
- package/src/actions/encodings/position.js +44 -90
- package/src/actions/encodings/ranged.js +28 -6
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +21 -18
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +9 -4
- package/src/actions/guides/axes/titles.js +7 -5
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +5 -13
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +3 -1
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +5 -5
- package/src/actions/guides/legends/edit.js +2 -6
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +64 -0
- package/src/actions/guides/legends/size.js +12 -14
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/axes.js +55 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/index.js +4 -0
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +25 -3
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +18 -1
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -1
- package/src/actions/scales/consumers.js +15 -6
- package/src/actions/scales/definitions.js +13 -4
- package/src/actions/scales/edit.js +42 -19
- package/src/actions/scales/materialize.js +57 -408
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +200 -2
- package/src/core/action.js +10 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/bars/aggregate.js +2 -2
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +20 -1
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/facets.js +122 -0
- package/src/grammar/lineSeries.js +111 -3
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- package/src/grammar/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +5 -2
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/dependencies.js +71 -16
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides.js +426 -0
- package/src/materialization/facets.js +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- package/src/materialization/marks.js +220 -14
- package/src/materialization/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +24 -16
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/types/extension.d.ts +1 -0
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/marks/line.js +0 -248
- package/src/materialization/selection/items.js +0 -503
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import {
|
|
5
|
+
validateKeys,
|
|
6
|
+
validateNonEmptyString,
|
|
7
|
+
validateNonNegativeFinite,
|
|
8
|
+
validateUnitInterval
|
|
9
|
+
} from "../../core/validation.js";
|
|
10
|
+
import { normalizeRegressionParameters } from "../../grammar/regression.js";
|
|
11
|
+
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
12
|
+
import { hasDataset } from "../../selectors/index.js";
|
|
13
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
14
|
+
|
|
15
|
+
const OPTIONS = Object.freeze([
|
|
16
|
+
"target", "method", "degree", "span", "confidence", "interval", "band", "line"
|
|
17
|
+
]);
|
|
18
|
+
const STATISTICAL_OPTIONS = Object.freeze([
|
|
19
|
+
"method", "degree", "span", "confidence", "interval"
|
|
20
|
+
]);
|
|
21
|
+
const BAND_OPTIONS = Object.freeze([
|
|
22
|
+
"color", "opacity", "stroke", "strokeWidth", "curve"
|
|
23
|
+
]);
|
|
24
|
+
const LINE_OPTIONS = Object.freeze(["strokeWidth", "curve"]);
|
|
25
|
+
|
|
26
|
+
function resolveRegressionOwner(program, requested) {
|
|
27
|
+
const eligible = program.semanticSpec.layers.filter(
|
|
28
|
+
layer => program.markConfigs[layer.id]?.regression !== undefined
|
|
29
|
+
);
|
|
30
|
+
if (requested !== undefined) {
|
|
31
|
+
const id = validateUserId(requested, "Regression owner id");
|
|
32
|
+
const layer = findLayer(program, id);
|
|
33
|
+
if (layer === undefined || !eligible.includes(layer)) {
|
|
34
|
+
throw new Error(`Unknown regression owner "${id}".`);
|
|
35
|
+
}
|
|
36
|
+
return layer;
|
|
37
|
+
}
|
|
38
|
+
const current = findLayer(program, program.context.currentMark);
|
|
39
|
+
if (current !== undefined && eligible.includes(current)) return current;
|
|
40
|
+
if (eligible.length === 1) return eligible[0];
|
|
41
|
+
if (eligible.length === 0) throw new Error("No regression owner is available.");
|
|
42
|
+
throw new Error("Regression owner is ambiguous; provide target.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requirePatch(value, keys, label) {
|
|
46
|
+
if (!isPlainObject(value)) {
|
|
47
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
48
|
+
}
|
|
49
|
+
validateKeys(value, keys, label);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateBandPatch(value) {
|
|
54
|
+
if (value === false) return false;
|
|
55
|
+
const patch = requirePatch(value, BAND_OPTIONS, "editRegression band");
|
|
56
|
+
if (Object.hasOwn(patch, "color")) {
|
|
57
|
+
validateNonEmptyString(patch.color, "Regression band color");
|
|
58
|
+
}
|
|
59
|
+
if (Object.hasOwn(patch, "opacity")) {
|
|
60
|
+
validateUnitInterval(patch.opacity, "Regression band opacity");
|
|
61
|
+
}
|
|
62
|
+
if (Object.hasOwn(patch, "stroke")) {
|
|
63
|
+
if (patch.stroke !== false) {
|
|
64
|
+
validateNonEmptyString(patch.stroke, "Regression band stroke");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Object.hasOwn(patch, "strokeWidth")) {
|
|
68
|
+
validateNonNegativeFinite(
|
|
69
|
+
patch.strokeWidth,
|
|
70
|
+
"Regression band strokeWidth"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (patch.stroke === false && Object.hasOwn(patch, "strokeWidth")) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"editRegression band cannot set strokeWidth while removing stroke."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (Object.hasOwn(patch, "curve")) {
|
|
79
|
+
validateCurveInterpolation(patch.curve);
|
|
80
|
+
}
|
|
81
|
+
return patch;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateLinePatch(value) {
|
|
85
|
+
const patch = requirePatch(value, LINE_OPTIONS, "editRegression line");
|
|
86
|
+
if (Object.hasOwn(patch, "strokeWidth")) {
|
|
87
|
+
validateNonNegativeFinite(
|
|
88
|
+
patch.strokeWidth,
|
|
89
|
+
"Regression line strokeWidth"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (Object.hasOwn(patch, "curve")) {
|
|
93
|
+
validateCurveInterpolation(patch.curve);
|
|
94
|
+
}
|
|
95
|
+
return patch;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveParameters(previous, args) {
|
|
99
|
+
const method = args.method ?? previous.method;
|
|
100
|
+
const raw = { method };
|
|
101
|
+
for (const key of ["degree", "span", "confidence", "interval"]) {
|
|
102
|
+
if (Object.hasOwn(args, key)) raw[key] = args[key];
|
|
103
|
+
}
|
|
104
|
+
if (method === previous.method) {
|
|
105
|
+
for (const key of ["degree", "span", "confidence", "interval"]) {
|
|
106
|
+
if (!Object.hasOwn(raw, key) && Object.hasOwn(previous, key)) {
|
|
107
|
+
raw[key] = previous[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else if (method !== "loess" && previous.method !== "loess") {
|
|
111
|
+
for (const key of ["confidence", "interval"]) {
|
|
112
|
+
if (!Object.hasOwn(raw, key) && Object.hasOwn(previous, key)) {
|
|
113
|
+
raw[key] = previous[key];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return normalizeRegressionParameters(raw);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function nextRevisionId(program, ownerId) {
|
|
121
|
+
let revision = 1;
|
|
122
|
+
while (hasDataset(program, `${ownerId}RegressionDataRevision${revision}`)) {
|
|
123
|
+
revision += 1;
|
|
124
|
+
}
|
|
125
|
+
return `${ownerId}RegressionDataRevision${revision}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function removeBand(program, id) {
|
|
129
|
+
return program
|
|
130
|
+
.editSemantic({ property: `layer[${id}]`, remove: true })
|
|
131
|
+
.editGraphics({ target: id, remove: true })
|
|
132
|
+
._withoutMaterializationConfig(["marks", id]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const editRegression = action(
|
|
136
|
+
{
|
|
137
|
+
op: "editRegression",
|
|
138
|
+
description: "Revise a regression model and its owned visible components."
|
|
139
|
+
},
|
|
140
|
+
function (args = {}) {
|
|
141
|
+
validateKeys(args, OPTIONS, "editRegression");
|
|
142
|
+
if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
|
|
143
|
+
throw new Error("editRegression requires a statistical or component option.");
|
|
144
|
+
}
|
|
145
|
+
const owner = resolveRegressionOwner(this, args.target);
|
|
146
|
+
const current = this.markConfigs[owner.id].regression;
|
|
147
|
+
const parameters = resolveParameters(current.parameters, args);
|
|
148
|
+
const changesStatistics = STATISTICAL_OPTIONS.some(
|
|
149
|
+
key => Object.hasOwn(args, key)
|
|
150
|
+
) && JSON.stringify(parameters) !== JSON.stringify(current.parameters);
|
|
151
|
+
const bandPatch = Object.hasOwn(args, "band")
|
|
152
|
+
? validateBandPatch(args.band)
|
|
153
|
+
: undefined;
|
|
154
|
+
const linePatch = Object.hasOwn(args, "line")
|
|
155
|
+
? validateLinePatch(args.line)
|
|
156
|
+
: undefined;
|
|
157
|
+
if (parameters.method === "loess" && bandPatch !== undefined && bandPatch !== false) {
|
|
158
|
+
throw new Error("LOESS regression does not support a band object.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const hadBand = current.bandId !== undefined;
|
|
162
|
+
const wantsBand = parameters.method !== "loess" &&
|
|
163
|
+
(bandPatch === false
|
|
164
|
+
? false
|
|
165
|
+
: hadBand || bandPatch !== undefined || current.parameters.method === "loess");
|
|
166
|
+
let dataId = current.dataId;
|
|
167
|
+
let next = this;
|
|
168
|
+
|
|
169
|
+
if (changesStatistics) {
|
|
170
|
+
dataId = nextRevisionId(this, owner.id);
|
|
171
|
+
next = next.createRegressionData({
|
|
172
|
+
id: dataId,
|
|
173
|
+
source: current.source,
|
|
174
|
+
x: current.x,
|
|
175
|
+
y: current.y,
|
|
176
|
+
...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
|
|
177
|
+
...parameters
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let bandId = current.bandId;
|
|
182
|
+
if (hadBand && !wantsBand) {
|
|
183
|
+
next = removeBand(next, current.bandId);
|
|
184
|
+
bandId = undefined;
|
|
185
|
+
} else if (!hadBand && wantsBand) {
|
|
186
|
+
bandId = `${owner.id}RegressionBands`;
|
|
187
|
+
next = next.createRegressionBand({
|
|
188
|
+
id: bandId,
|
|
189
|
+
data: dataId,
|
|
190
|
+
x: current.x,
|
|
191
|
+
lower: "__regression_ci_lower",
|
|
192
|
+
upper: "__regression_ci_upper",
|
|
193
|
+
...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
|
|
194
|
+
coordinate: current.coordinate,
|
|
195
|
+
xScale: current.xScale,
|
|
196
|
+
yScale: current.yScale,
|
|
197
|
+
...(bandPatch === undefined || bandPatch === false ? {} : bandPatch)
|
|
198
|
+
});
|
|
199
|
+
} else if (hadBand && changesStatistics) {
|
|
200
|
+
const bandConfig = next.markConfigs[bandId];
|
|
201
|
+
next = next
|
|
202
|
+
.editSemantic({ property: `layer[${bandId}].data`, value: dataId })
|
|
203
|
+
._withMarkConfig(bandId, {
|
|
204
|
+
...bandConfig,
|
|
205
|
+
errorBand: { ...bandConfig.errorBand, data: dataId }
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (changesStatistics) {
|
|
210
|
+
next = next.editSemantic({
|
|
211
|
+
property: `layer[${current.lineId}].data`,
|
|
212
|
+
value: dataId
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (bandId !== undefined) {
|
|
217
|
+
if (
|
|
218
|
+
bandPatch !== undefined &&
|
|
219
|
+
bandPatch !== false &&
|
|
220
|
+
Object.keys(bandPatch).length > 0 &&
|
|
221
|
+
hadBand
|
|
222
|
+
) {
|
|
223
|
+
next = next.editRegressionBand({ target: bandId, ...bandPatch });
|
|
224
|
+
} else if (changesStatistics) {
|
|
225
|
+
next = next.rematerializeAreaMark({ id: bandId });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
|
|
229
|
+
next = next.editRegressionLine({ target: current.lineId, ...linePatch });
|
|
230
|
+
} else if (changesStatistics) {
|
|
231
|
+
next = next.rematerializeLineMark({ id: current.lineId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (changesStatistics) {
|
|
235
|
+
next = next.releaseDerivedData({ id: current.dataId });
|
|
236
|
+
}
|
|
237
|
+
return next._withMarkConfig(owner.id, {
|
|
238
|
+
...next.markConfigs[owner.id],
|
|
239
|
+
regression: {
|
|
240
|
+
...current,
|
|
241
|
+
dataId,
|
|
242
|
+
bandId,
|
|
243
|
+
parameters
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
editRegressionLine
|
|
6
6
|
} from "./components.js";
|
|
7
7
|
import { createRegression } from "./create.js";
|
|
8
|
+
import { editRegression } from "./edit.js";
|
|
8
9
|
|
|
9
10
|
export {
|
|
10
11
|
createRegressionBand,
|
|
@@ -13,6 +14,7 @@ export {
|
|
|
13
14
|
editRegressionLine
|
|
14
15
|
} from "./components.js";
|
|
15
16
|
export { createRegression } from "./create.js";
|
|
17
|
+
export { editRegression } from "./edit.js";
|
|
16
18
|
|
|
17
19
|
export function registerRegressionActions(ProgramClass) {
|
|
18
20
|
ProgramClass.prototype.createRegressionBand = createRegressionBand;
|
|
@@ -20,4 +22,5 @@ export function registerRegressionActions(ProgramClass) {
|
|
|
20
22
|
ProgramClass.prototype.editRegressionBand = editRegressionBand;
|
|
21
23
|
ProgramClass.prototype.editRegressionLine = editRegressionLine;
|
|
22
24
|
ProgramClass.prototype.createRegression = createRegression;
|
|
25
|
+
ProgramClass.prototype.editRegression = editRegression;
|
|
23
26
|
}
|
|
@@ -47,7 +47,9 @@ export function inferRegressionGroup(layer, args) {
|
|
|
47
47
|
}
|
|
48
48
|
const candidates = [...new Set(
|
|
49
49
|
[layer.encoding?.color, layer.encoding?.shape]
|
|
50
|
-
.filter(encoding =>
|
|
50
|
+
.filter(encoding =>
|
|
51
|
+
["nominal", "ordinal"].includes(encoding?.fieldType)
|
|
52
|
+
)
|
|
51
53
|
.map(encoding => encoding.field)
|
|
52
54
|
)];
|
|
53
55
|
if (candidates.length > 1) {
|
|
@@ -24,6 +24,8 @@ import { findDataset } from "../../selectors/datasets.js";
|
|
|
24
24
|
import { requireSemanticScale } from "../../selectors/scales.js";
|
|
25
25
|
import { isAggregate } from "../../grammar/aggregate.js";
|
|
26
26
|
import { normalizeRuleDatum } from "../../grammar/rules.js";
|
|
27
|
+
import { SCALED_ENCODING_CHANNELS } from "../../core/vocabulary.js";
|
|
28
|
+
import { resolveRectConsumerValues } from "../../materialization/rect.js";
|
|
27
29
|
|
|
28
30
|
export function findScale(program, id) {
|
|
29
31
|
return requireSemanticScale(program, id);
|
|
@@ -32,10 +34,7 @@ export function findScale(program, id) {
|
|
|
32
34
|
export function findScaleConsumers(program, id) {
|
|
33
35
|
const consumers = [];
|
|
34
36
|
for (const layer of program.semanticSpec.layers) {
|
|
35
|
-
for (const channel of
|
|
36
|
-
"x", "y", "x2", "y2", "xOffset", "color", "strokeDash", "size", "shape",
|
|
37
|
-
"opacity"
|
|
38
|
-
]) {
|
|
37
|
+
for (const channel of SCALED_ENCODING_CHANNELS) {
|
|
39
38
|
const encoding = layer.encoding?.[channel];
|
|
40
39
|
if (encoding?.scale === id) consumers.push({ layer, channel, encoding });
|
|
41
40
|
}
|
|
@@ -67,9 +66,16 @@ export function resolveConsumerValues(program, consumer) {
|
|
|
67
66
|
}
|
|
68
67
|
const scale = findScale(program, consumer.encoding.scale);
|
|
69
68
|
const allowUnknown = Object.hasOwn(scale, "unknown");
|
|
69
|
+
if (consumer.layer.mark?.type === "rect") {
|
|
70
|
+
return resolveRectConsumerValues(
|
|
71
|
+
consumer.layer,
|
|
72
|
+
dataset,
|
|
73
|
+
consumer.channel
|
|
74
|
+
);
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
if (
|
|
72
|
-
["color", "strokeDash", "xOffset", "shape"].includes(consumer.channel) &&
|
|
78
|
+
["color", "strokeDash", "xOffset", "yOffset", "shape"].includes(consumer.channel) &&
|
|
73
79
|
consumer.encoding.fieldType === "nominal"
|
|
74
80
|
) {
|
|
75
81
|
return allowUnknown
|
|
@@ -208,7 +214,10 @@ function resolveAggregatePartitions(program, consumer) {
|
|
|
208
214
|
: derived.yValues;
|
|
209
215
|
const categoryDomain = resolveOrdinalDomain(categoryScale.domain, categoryValues);
|
|
210
216
|
const colorEncoding = layer.encoding?.color;
|
|
211
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
colorEncoding?.scale === undefined ||
|
|
219
|
+
colorEncoding.fieldType === "quantitative"
|
|
220
|
+
) {
|
|
212
221
|
const byCategory = new Map(derived.values.map(value => [
|
|
213
222
|
value[channels.category],
|
|
214
223
|
value[channels.measure]
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
validateScaleTypeForRole,
|
|
24
24
|
isDiscretePositionScaleType
|
|
25
25
|
} from "../../grammar/scales.js";
|
|
26
|
+
import {
|
|
27
|
+
validateRadialRange,
|
|
28
|
+
validateThetaRange
|
|
29
|
+
} from "../../grammar/polar.js";
|
|
26
30
|
import { withScaleUnknown } from "../../grammar/scales.js";
|
|
27
31
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
28
32
|
|
|
@@ -87,6 +91,7 @@ export function resolvePositionScaleDefinition(
|
|
|
87
91
|
);
|
|
88
92
|
}
|
|
89
93
|
}
|
|
94
|
+
else if (channel === "theta") validateLinearScaleType(type);
|
|
90
95
|
else validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
|
|
91
96
|
const scale = {
|
|
92
97
|
id,
|
|
@@ -108,7 +113,11 @@ export function resolvePositionScaleDefinition(
|
|
|
108
113
|
["ordinal", "nominal"].includes(fieldType)
|
|
109
114
|
? validateOrdinalDomain(value)
|
|
110
115
|
: validateScaleDomain(value),
|
|
111
|
-
validateRange: (_scaleType, value) =>
|
|
116
|
+
validateRange: (_scaleType, value) => channel === "theta"
|
|
117
|
+
? validateThetaRange(value)
|
|
118
|
+
: channel === "radius"
|
|
119
|
+
? validateRadialRange(value)
|
|
120
|
+
: validateScaleRange(value)
|
|
112
121
|
})
|
|
113
122
|
};
|
|
114
123
|
return withScaleUnknown(scale, { ...existing, ...options }, channel);
|
|
@@ -305,15 +314,15 @@ export function resolveOpacityScaleDefinition(program, options) {
|
|
|
305
314
|
return withScaleUnknown(scale, { ...existing, ...options }, "opacity");
|
|
306
315
|
}
|
|
307
316
|
|
|
308
|
-
export function resolveOffsetScaleDefinition(program, options) {
|
|
317
|
+
export function resolveOffsetScaleDefinition(program, options, channel = "xOffset") {
|
|
309
318
|
optionsObject(options);
|
|
310
319
|
validateKeys(options, [...BASE_OPTIONS, "unknown"], "scale");
|
|
311
|
-
const id = validateUserId(options.id ??
|
|
320
|
+
const id = validateUserId(options.id ?? channel, "Scale id");
|
|
312
321
|
const existing = findSemanticScale(program, id);
|
|
313
322
|
return withScaleUnknown({
|
|
314
323
|
id,
|
|
315
324
|
type: validateOrdinalScaleType(options.type ?? existing?.type ?? "ordinal"),
|
|
316
325
|
domain: validateOrdinalDomain(options.domain ?? existing?.domain ?? "auto"),
|
|
317
326
|
range: validateScaleRange(options.range ?? existing?.range ?? "auto")
|
|
318
|
-
}, { ...existing, ...options },
|
|
327
|
+
}, { ...existing, ...options }, channel);
|
|
319
328
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
-
import {
|
|
4
|
-
import { validateKeys } from "../../core/validation.js";
|
|
3
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
5
4
|
import {
|
|
6
5
|
isTransformedScaleType,
|
|
7
6
|
SCALE_ROLES,
|
|
@@ -23,6 +22,10 @@ import {
|
|
|
23
22
|
hasOrdinalDomain,
|
|
24
23
|
normalizeScaleDefinition
|
|
25
24
|
} from "../../grammar/scales.js";
|
|
25
|
+
import {
|
|
26
|
+
validateRadialRange,
|
|
27
|
+
validateThetaRange
|
|
28
|
+
} from "../../grammar/polar.js";
|
|
26
29
|
import { getMarkMaterializationStep } from "../../materialization/marks.js";
|
|
27
30
|
import {
|
|
28
31
|
applyMaterializationPlan
|
|
@@ -32,13 +35,17 @@ import {
|
|
|
32
35
|
requireSemanticScale
|
|
33
36
|
} from "../../selectors/scales.js";
|
|
34
37
|
import { findScaleConsumers } from "./consumers.js";
|
|
38
|
+
import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
|
|
35
39
|
|
|
36
40
|
const OPTIONS = Object.freeze([
|
|
37
41
|
"id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
38
42
|
"base", "exponent", "constant", "paddingInner", "paddingOuter", "padding",
|
|
39
|
-
"align", "interpolate", "unknown"
|
|
43
|
+
"align", "interpolate", "unknown", "palette"
|
|
40
44
|
]);
|
|
41
|
-
const EDITABLE = Object.freeze(
|
|
45
|
+
const EDITABLE = Object.freeze(
|
|
46
|
+
OPTIONS.filter(option => !["id", "palette"].includes(option))
|
|
47
|
+
);
|
|
48
|
+
const REQUESTED_CHANGES = Object.freeze(OPTIONS.filter(option => option !== "id"));
|
|
42
49
|
|
|
43
50
|
function resolveScaleId(program, requested) {
|
|
44
51
|
if (requested !== undefined) {
|
|
@@ -60,13 +67,9 @@ function resolveScaleId(program, requested) {
|
|
|
60
67
|
);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
function normalizeChannel(channel) {
|
|
64
|
-
return channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
70
|
function resolveChannel(consumers, id) {
|
|
68
71
|
const channels = new Set(
|
|
69
|
-
consumers.map(consumer =>
|
|
72
|
+
consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
|
|
70
73
|
);
|
|
71
74
|
if (channels.size > 1) {
|
|
72
75
|
throw new Error(`Scale "${id}" cannot be shared across channels.`);
|
|
@@ -76,6 +79,8 @@ function resolveChannel(consumers, id) {
|
|
|
76
79
|
|
|
77
80
|
function validateRangeForChannel(scale, channel, value) {
|
|
78
81
|
if (value === "auto") return value;
|
|
82
|
+
if (channel === "theta") return validateThetaRange(value);
|
|
83
|
+
if (channel === "radius") return validateRadialRange(value);
|
|
79
84
|
if (scale.type === "sequential") {
|
|
80
85
|
return validateSequentialColorRange(value);
|
|
81
86
|
}
|
|
@@ -97,7 +102,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
|
|
|
97
102
|
if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
|
|
98
103
|
const discretized = isDiscretizedColorScaleType(nextType);
|
|
99
104
|
if (channel !== "color" || consumers.some(consumer =>
|
|
100
|
-
consumer.encoding.fieldType
|
|
105
|
+
["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
|
|
101
106
|
(discretized && (
|
|
102
107
|
consumer.encoding.fieldType !== "quantitative" ||
|
|
103
108
|
consumer.layer.mark?.type !== "point"
|
|
@@ -113,7 +118,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
|
|
|
113
118
|
if (nextType === "time") {
|
|
114
119
|
if (
|
|
115
120
|
consumers.length > 0 &&
|
|
116
|
-
(!["x", "y"].includes(channel) ||
|
|
121
|
+
(!["x", "y", "theta"].includes(channel) ||
|
|
117
122
|
consumers.some(consumer => consumer.encoding.fieldType !== "temporal"))
|
|
118
123
|
) {
|
|
119
124
|
throw new Error(
|
|
@@ -126,7 +131,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
|
|
|
126
131
|
if (discrete) {
|
|
127
132
|
if (
|
|
128
133
|
consumers.length > 0 &&
|
|
129
|
-
(!["x", "y"].includes(channel) || consumers.some(consumer =>
|
|
134
|
+
(!["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
|
|
130
135
|
!["nominal", "ordinal"].includes(consumer.encoding.fieldType)
|
|
131
136
|
))
|
|
132
137
|
) {
|
|
@@ -145,13 +150,16 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
|
|
|
145
150
|
const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
|
|
146
151
|
if (
|
|
147
152
|
!quantitative ||
|
|
148
|
-
(consumers.length > 0 && !["x", "y"].includes(channel))
|
|
153
|
+
(consumers.length > 0 && !["x", "y", "theta", "radius"].includes(channel))
|
|
149
154
|
) {
|
|
150
155
|
throw new Error(
|
|
151
156
|
"editScale type transition currently requires a quantitative position scale."
|
|
152
157
|
);
|
|
153
158
|
}
|
|
154
159
|
validateScaleTypeForRole(nextType, SCALE_ROLES.quantitativePosition);
|
|
160
|
+
if (channel === "theta" && nextType !== "linear") {
|
|
161
|
+
throw new Error("Theta quantitative position currently requires a linear scale.");
|
|
162
|
+
}
|
|
155
163
|
if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
|
|
156
164
|
throw new Error(
|
|
157
165
|
`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
|
|
@@ -242,20 +250,35 @@ export const editScale = action(
|
|
|
242
250
|
description: "Edit an existing scale and rematerialize its consumers."
|
|
243
251
|
},
|
|
244
252
|
function (args = {}) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
validateKeys(args, OPTIONS, "editScale");
|
|
249
|
-
if (!EDITABLE.some(property => Object.hasOwn(args, property))) {
|
|
253
|
+
validateOptionObject(args, OPTIONS, "editScale");
|
|
254
|
+
if (!REQUESTED_CHANGES.some(property => Object.hasOwn(args, property))) {
|
|
250
255
|
throw new Error("editScale requires at least one editable property.");
|
|
251
256
|
}
|
|
257
|
+
const hasPalette = Object.hasOwn(args, "palette");
|
|
258
|
+
if (hasPalette && Object.hasOwn(args, "range")) {
|
|
259
|
+
throw new Error("editScale cannot specify both palette and range.");
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
const id = resolveScaleId(this, args.id);
|
|
254
263
|
const scale = requireSemanticScale(this, id);
|
|
255
264
|
const consumers = findScaleConsumers(this, id);
|
|
256
265
|
const channel = resolveChannel(consumers, id);
|
|
266
|
+
if (
|
|
267
|
+
hasPalette &&
|
|
268
|
+
channel !== "color" &&
|
|
269
|
+
scale.type !== "sequential" &&
|
|
270
|
+
!isDiscretizedColorScaleType(scale.type)
|
|
271
|
+
) {
|
|
272
|
+
throw new Error("editScale palette requires a color scale.");
|
|
273
|
+
}
|
|
274
|
+
const patch = !hasPalette
|
|
275
|
+
? args
|
|
276
|
+
: Object.fromEntries([
|
|
277
|
+
...Object.entries(args).filter(([key]) => key !== "palette"),
|
|
278
|
+
["range", { palette: args.palette }]
|
|
279
|
+
]);
|
|
257
280
|
validateLegendTypeTransition(this, scale, args.type ?? scale.type);
|
|
258
|
-
const definition = normalizeDefinition(scale, channel, consumers,
|
|
281
|
+
const definition = normalizeDefinition(scale, channel, consumers, patch);
|
|
259
282
|
|
|
260
283
|
let next = this;
|
|
261
284
|
for (const property of EDITABLE) {
|