ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../core/validation.js";
|
|
4
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
5
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
6
|
+
import { resolveEligibleLayer } from "../../selectors/layers.js";
|
|
7
|
+
import {
|
|
8
|
+
REGRESSION_LOWER_FIELD,
|
|
9
|
+
REGRESSION_UPPER_FIELD
|
|
10
|
+
} from "../../grammar/regression.js";
|
|
11
|
+
import { validateAreaCreateOutline } from "../marks/area.js";
|
|
12
|
+
|
|
13
|
+
const BAND_OPTIONS = Object.freeze([
|
|
14
|
+
"id", "data", "x", "lower", "upper", "groupBy", "coordinate", "xScale", "yScale",
|
|
15
|
+
"color", "opacity", "stroke", "strokeWidth", "curve"
|
|
16
|
+
]);
|
|
17
|
+
const LINE_OPTIONS = Object.freeze([
|
|
18
|
+
"id", "data", "x", "y", "groupBy", "coordinate", "xScale", "yScale", "colorScale",
|
|
19
|
+
"strokeWidth", "curve"
|
|
20
|
+
]);
|
|
21
|
+
const EDIT_BAND_OPTIONS = Object.freeze([
|
|
22
|
+
"target", "color", "opacity", "stroke", "strokeWidth", "curve"
|
|
23
|
+
]);
|
|
24
|
+
const EDIT_LINE_OPTIONS = Object.freeze(["target", "strokeWidth", "curve"]);
|
|
25
|
+
|
|
26
|
+
function isRegressionLayer(program, layer, type) {
|
|
27
|
+
const dataset = findDataset(program, layer.data);
|
|
28
|
+
return layer.mark?.type === type &&
|
|
29
|
+
dataset?.transform?.length === 1 &&
|
|
30
|
+
dataset.transform[0].type === "regression";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveRegressionComponent(program, args, type, label) {
|
|
34
|
+
const target = Object.hasOwn(args, "target")
|
|
35
|
+
? validateUserId(args.target, `${label} id`)
|
|
36
|
+
: undefined;
|
|
37
|
+
return resolveEligibleLayer(program, {
|
|
38
|
+
target,
|
|
39
|
+
predicate: layer => isRegressionLayer(program, layer, type),
|
|
40
|
+
label
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function requireRegressionBandDataset(program, args) {
|
|
45
|
+
const dataset = findDataset(program, args.data);
|
|
46
|
+
const transform = dataset?.transform?.length === 1
|
|
47
|
+
? dataset.transform[0]
|
|
48
|
+
: undefined;
|
|
49
|
+
if (transform?.type !== "regression" || transform.method === "loess") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"createRegressionBand requires a regression dataset with interval bounds."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
args.x !== transform.x ||
|
|
56
|
+
args.lower !== REGRESSION_LOWER_FIELD ||
|
|
57
|
+
args.upper !== REGRESSION_UPPER_FIELD ||
|
|
58
|
+
args.groupBy !== transform.groupBy
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"createRegressionBand fields and grouping must match regression provenance."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return { dataset, transform };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const createRegressionBand = action(
|
|
68
|
+
{
|
|
69
|
+
op: "createRegressionBand",
|
|
70
|
+
description: "Create and encode a grouped regression confidence band."
|
|
71
|
+
},
|
|
72
|
+
function (args = {}) {
|
|
73
|
+
validateKeys(args, BAND_OPTIONS, "createRegressionBand");
|
|
74
|
+
const id = validateUserId(args.id, "Regression band id");
|
|
75
|
+
const { transform } = requireRegressionBandDataset(this, args);
|
|
76
|
+
validateAreaCreateOutline(args, "createRegressionBand");
|
|
77
|
+
let next = this
|
|
78
|
+
.createErrorBand({
|
|
79
|
+
id,
|
|
80
|
+
data: args.data,
|
|
81
|
+
x: {
|
|
82
|
+
field: args.x,
|
|
83
|
+
fieldType: "quantitative",
|
|
84
|
+
scale: { id: args.xScale }
|
|
85
|
+
},
|
|
86
|
+
y: {
|
|
87
|
+
center: transform.y,
|
|
88
|
+
lower: args.lower,
|
|
89
|
+
upper: args.upper,
|
|
90
|
+
scale: { id: args.yScale }
|
|
91
|
+
},
|
|
92
|
+
...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
|
|
93
|
+
coordinate: args.coordinate,
|
|
94
|
+
fill: args.color ?? DEFAULT_COLORS.regressionBand,
|
|
95
|
+
opacity: args.opacity ?? 0.18,
|
|
96
|
+
...(args.curve === undefined ? {} : { curve: args.curve })
|
|
97
|
+
})
|
|
98
|
+
.editSemantic({
|
|
99
|
+
property: `layer[${id}].encoding.y.title`,
|
|
100
|
+
remove: true
|
|
101
|
+
});
|
|
102
|
+
if (args.stroke !== undefined || args.strokeWidth !== undefined) {
|
|
103
|
+
next = next.editAreaMark({
|
|
104
|
+
target: id,
|
|
105
|
+
...(args.stroke === undefined ? {} : { stroke: args.stroke }),
|
|
106
|
+
...(args.strokeWidth === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { strokeWidth: args.strokeWidth })
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
export const editRegressionBand = action(
|
|
116
|
+
{
|
|
117
|
+
op: "editRegressionBand",
|
|
118
|
+
description: "Edit regression-band fill, opacity, and outline."
|
|
119
|
+
},
|
|
120
|
+
function (args = {}) {
|
|
121
|
+
validateKeys(args, EDIT_BAND_OPTIONS, "editRegressionBand");
|
|
122
|
+
const changes = ["color", "opacity", "stroke", "strokeWidth", "curve"];
|
|
123
|
+
if (!changes.some(key => Object.hasOwn(args, key))) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"editRegressionBand requires color, opacity, stroke, strokeWidth, or curve."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const layer = resolveRegressionComponent(
|
|
129
|
+
this,
|
|
130
|
+
args,
|
|
131
|
+
"area",
|
|
132
|
+
"regression band"
|
|
133
|
+
);
|
|
134
|
+
return this.editAreaMark({
|
|
135
|
+
target: layer.id,
|
|
136
|
+
...(Object.hasOwn(args, "color") ? { fill: args.color } : {}),
|
|
137
|
+
...(Object.hasOwn(args, "opacity") ? { opacity: args.opacity } : {}),
|
|
138
|
+
...(Object.hasOwn(args, "stroke") ? { stroke: args.stroke } : {}),
|
|
139
|
+
...(Object.hasOwn(args, "strokeWidth")
|
|
140
|
+
? { strokeWidth: args.strokeWidth }
|
|
141
|
+
: {}),
|
|
142
|
+
...(Object.hasOwn(args, "curve") ? { curve: args.curve } : {})
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export const editRegressionLine = action(
|
|
148
|
+
{
|
|
149
|
+
op: "editRegressionLine",
|
|
150
|
+
description: "Edit regression-line width or curve."
|
|
151
|
+
},
|
|
152
|
+
function (args = {}) {
|
|
153
|
+
validateKeys(args, EDIT_LINE_OPTIONS, "editRegressionLine");
|
|
154
|
+
if (
|
|
155
|
+
!Object.hasOwn(args, "strokeWidth") &&
|
|
156
|
+
!Object.hasOwn(args, "curve")
|
|
157
|
+
) {
|
|
158
|
+
throw new Error("editRegressionLine requires strokeWidth or curve.");
|
|
159
|
+
}
|
|
160
|
+
const layer = resolveRegressionComponent(
|
|
161
|
+
this,
|
|
162
|
+
args,
|
|
163
|
+
"line",
|
|
164
|
+
"regression line"
|
|
165
|
+
);
|
|
166
|
+
return this.editLineMark({
|
|
167
|
+
target: layer.id,
|
|
168
|
+
...(Object.hasOwn(args, "strokeWidth")
|
|
169
|
+
? { strokeWidth: args.strokeWidth }
|
|
170
|
+
: {}),
|
|
171
|
+
...(Object.hasOwn(args, "curve") ? { curve: args.curve } : {})
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
export const createRegressionLine = action(
|
|
177
|
+
{
|
|
178
|
+
op: "createRegressionLine",
|
|
179
|
+
description: "Create and encode grouped regression line paths."
|
|
180
|
+
},
|
|
181
|
+
function (args = {}) {
|
|
182
|
+
validateKeys(args, LINE_OPTIONS, "createRegressionLine");
|
|
183
|
+
const id = validateUserId(args.id, "Regression line id");
|
|
184
|
+
let next = this
|
|
185
|
+
.createLineMark({
|
|
186
|
+
id,
|
|
187
|
+
data: args.data,
|
|
188
|
+
strokeWidth: args.strokeWidth ?? 3,
|
|
189
|
+
...(args.curve === undefined ? {} : { curve: args.curve })
|
|
190
|
+
})
|
|
191
|
+
.encodeX({
|
|
192
|
+
target: id,
|
|
193
|
+
field: args.x,
|
|
194
|
+
fieldType: "quantitative",
|
|
195
|
+
coordinate: args.coordinate,
|
|
196
|
+
scale: { id: args.xScale }
|
|
197
|
+
})
|
|
198
|
+
.encodeY({
|
|
199
|
+
target: id,
|
|
200
|
+
field: args.y,
|
|
201
|
+
fieldType: "quantitative",
|
|
202
|
+
coordinate: args.coordinate,
|
|
203
|
+
scale: { id: args.yScale }
|
|
204
|
+
});
|
|
205
|
+
if (args.groupBy !== undefined) {
|
|
206
|
+
next = next
|
|
207
|
+
.encodeColor({
|
|
208
|
+
target: id,
|
|
209
|
+
field: args.groupBy,
|
|
210
|
+
scale: { id: args.colorScale }
|
|
211
|
+
})
|
|
212
|
+
.encodeGroup({ target: id, field: args.groupBy });
|
|
213
|
+
}
|
|
214
|
+
return next.rematerializeLineMark({ id });
|
|
215
|
+
}
|
|
216
|
+
);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateKeys } from "../../core/validation.js";
|
|
3
|
+
import { normalizeRegressionParameters } from "../../grammar/regression.js";
|
|
4
|
+
import {
|
|
5
|
+
findRegressionPoint,
|
|
6
|
+
inferRegressionGroup,
|
|
7
|
+
requireRegressionField,
|
|
8
|
+
requireRegressionObject
|
|
9
|
+
} from "./resolve.js";
|
|
10
|
+
|
|
11
|
+
const REGRESSION_OPTIONS = Object.freeze([
|
|
12
|
+
"target", "x", "y", "groupBy", "method", "degree", "span",
|
|
13
|
+
"confidence", "interval", "band", "line"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const createRegression = action(
|
|
17
|
+
{
|
|
18
|
+
op: "createRegression",
|
|
19
|
+
description: "Fit and layer regression lines with optional interval bands."
|
|
20
|
+
},
|
|
21
|
+
function (args = {}) {
|
|
22
|
+
validateKeys(args, REGRESSION_OPTIONS, "createRegression");
|
|
23
|
+
const point = findRegressionPoint(this, args.target);
|
|
24
|
+
const x = args.x === undefined
|
|
25
|
+
? point.encoding.x.field
|
|
26
|
+
: requireRegressionField(args.x, "Regression x");
|
|
27
|
+
const y = args.y === undefined
|
|
28
|
+
? point.encoding.y.field
|
|
29
|
+
: requireRegressionField(args.y, "Regression y");
|
|
30
|
+
const groupBy = inferRegressionGroup(point, args);
|
|
31
|
+
const coordinate = point.coordinate;
|
|
32
|
+
const xScale = point.encoding.x.scale;
|
|
33
|
+
const yScale = point.encoding.y.scale;
|
|
34
|
+
if (coordinate === undefined || xScale === undefined || yScale === undefined) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"createRegression target requires stored coordinate and scales."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const parameters = normalizeRegressionParameters(args);
|
|
40
|
+
let band = false;
|
|
41
|
+
if (parameters.method === "loess") {
|
|
42
|
+
if (args.band !== undefined && args.band !== false) {
|
|
43
|
+
throw new Error("LOESS regression does not support a band object.");
|
|
44
|
+
}
|
|
45
|
+
} else if (args.band !== false) {
|
|
46
|
+
band = requireRegressionObject(args.band ?? {}, "Regression band");
|
|
47
|
+
}
|
|
48
|
+
const line = requireRegressionObject(args.line ?? {}, "Regression line");
|
|
49
|
+
if (band !== false) {
|
|
50
|
+
validateKeys(
|
|
51
|
+
band,
|
|
52
|
+
["color", "opacity", "stroke", "strokeWidth", "curve"],
|
|
53
|
+
"regression band"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
validateKeys(line, ["strokeWidth", "curve"], "regression line");
|
|
57
|
+
const colorEncoding = point.encoding?.color;
|
|
58
|
+
const namespace = point.id;
|
|
59
|
+
const dataId = `${namespace}RegressionData`;
|
|
60
|
+
const bandId = `${namespace}RegressionBands`;
|
|
61
|
+
const lineId = `${namespace}RegressionLines`;
|
|
62
|
+
const colorScale =
|
|
63
|
+
groupBy !== undefined && colorEncoding?.field === groupBy
|
|
64
|
+
? colorEncoding.scale
|
|
65
|
+
: `${namespace}RegressionColor`;
|
|
66
|
+
|
|
67
|
+
let next = this
|
|
68
|
+
.createRegressionData({
|
|
69
|
+
id: dataId,
|
|
70
|
+
source: point.data,
|
|
71
|
+
x,
|
|
72
|
+
y,
|
|
73
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
74
|
+
...parameters
|
|
75
|
+
});
|
|
76
|
+
if (band !== false) {
|
|
77
|
+
next = next.createRegressionBand({
|
|
78
|
+
id: bandId,
|
|
79
|
+
data: dataId,
|
|
80
|
+
x,
|
|
81
|
+
lower: "__regression_ci_lower",
|
|
82
|
+
upper: "__regression_ci_upper",
|
|
83
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
84
|
+
coordinate,
|
|
85
|
+
xScale,
|
|
86
|
+
yScale,
|
|
87
|
+
...band
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return next.createRegressionLine({
|
|
91
|
+
id: lineId,
|
|
92
|
+
data: dataId,
|
|
93
|
+
x,
|
|
94
|
+
y,
|
|
95
|
+
...(groupBy === undefined ? {} : { groupBy, colorScale }),
|
|
96
|
+
coordinate,
|
|
97
|
+
xScale,
|
|
98
|
+
yScale,
|
|
99
|
+
...line
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRegressionBand,
|
|
3
|
+
createRegressionLine,
|
|
4
|
+
editRegressionBand,
|
|
5
|
+
editRegressionLine
|
|
6
|
+
} from "./components.js";
|
|
7
|
+
import { createRegression } from "./create.js";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
createRegressionBand,
|
|
11
|
+
createRegressionLine,
|
|
12
|
+
editRegressionBand,
|
|
13
|
+
editRegressionLine
|
|
14
|
+
} from "./components.js";
|
|
15
|
+
export { createRegression } from "./create.js";
|
|
16
|
+
|
|
17
|
+
export function registerRegressionActions(ProgramClass) {
|
|
18
|
+
ProgramClass.prototype.createRegressionBand = createRegressionBand;
|
|
19
|
+
ProgramClass.prototype.createRegressionLine = createRegressionLine;
|
|
20
|
+
ProgramClass.prototype.editRegressionBand = editRegressionBand;
|
|
21
|
+
ProgramClass.prototype.editRegressionLine = editRegressionLine;
|
|
22
|
+
ProgramClass.prototype.createRegression = createRegression;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
3
|
+
|
|
4
|
+
export function requireRegressionObject(value, label) {
|
|
5
|
+
if (!isPlainObject(value)) {
|
|
6
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function requireRegressionField(value, label) {
|
|
12
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
13
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function findRegressionPoint(program, requested) {
|
|
19
|
+
const eligible = program.semanticSpec.layers.filter(layer =>
|
|
20
|
+
layer.mark?.type === "point" &&
|
|
21
|
+
layer.encoding?.x?.fieldType === "quantitative" &&
|
|
22
|
+
layer.encoding?.y?.fieldType === "quantitative"
|
|
23
|
+
);
|
|
24
|
+
if (requested !== undefined) {
|
|
25
|
+
const selected = findLayer(program, requested);
|
|
26
|
+
if (selected === undefined || !eligible.includes(selected)) {
|
|
27
|
+
throw new Error(`Unknown regression point target "${requested}".`);
|
|
28
|
+
}
|
|
29
|
+
return selected;
|
|
30
|
+
}
|
|
31
|
+
const current = findLayer(program, program.context.currentMark);
|
|
32
|
+
if (current !== undefined && eligible.includes(current)) return current;
|
|
33
|
+
if (eligible.length === 1) return eligible[0];
|
|
34
|
+
if (eligible.length === 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"createRegression requires an eligible quantitative point mark."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
throw new Error("createRegression target is ambiguous; provide target.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function inferRegressionGroup(layer, args) {
|
|
43
|
+
if (Object.hasOwn(args, "groupBy")) {
|
|
44
|
+
return args.groupBy === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: requireRegressionField(args.groupBy, "Regression groupBy");
|
|
47
|
+
}
|
|
48
|
+
const candidates = [...new Set(
|
|
49
|
+
[layer.encoding?.color, layer.encoding?.shape]
|
|
50
|
+
.filter(encoding => encoding?.fieldType === "nominal")
|
|
51
|
+
.map(encoding => encoding.field)
|
|
52
|
+
)];
|
|
53
|
+
if (candidates.length > 1) {
|
|
54
|
+
throw new Error("createRegression groupBy is ambiguous; provide groupBy.");
|
|
55
|
+
}
|
|
56
|
+
return candidates[0];
|
|
57
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
|
|
2
|
+
import {
|
|
3
|
+
BAR_GRAINS,
|
|
4
|
+
resolveBarChannels,
|
|
5
|
+
resolveBarColorLayout,
|
|
6
|
+
resolveBarGrain
|
|
7
|
+
} from "../../grammar/bars/policy.js";
|
|
8
|
+
import {
|
|
9
|
+
countHistogramBins,
|
|
10
|
+
findHistogramBinIndex,
|
|
11
|
+
resolveHistogramBins
|
|
12
|
+
} from "../../grammar/histogram.js";
|
|
13
|
+
import { deriveDensityAreaSeries } from "../../grammar/areaSeries.js";
|
|
14
|
+
import { deriveLineSeries } from "../../grammar/lineSeries.js";
|
|
15
|
+
import { resolveSeriesLayoutDomainValues } from "../../grammar/seriesLayout.js";
|
|
16
|
+
import {
|
|
17
|
+
readNominalField,
|
|
18
|
+
readQuantitativeField,
|
|
19
|
+
readScaleField,
|
|
20
|
+
readTemporalField,
|
|
21
|
+
resolveOrdinalDomain
|
|
22
|
+
} from "../../grammar/scales.js";
|
|
23
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
24
|
+
import { requireSemanticScale } from "../../selectors/scales.js";
|
|
25
|
+
import { isAggregate } from "../../grammar/aggregate.js";
|
|
26
|
+
import { normalizeRuleDatum } from "../../grammar/rules.js";
|
|
27
|
+
|
|
28
|
+
export function findScale(program, id) {
|
|
29
|
+
return requireSemanticScale(program, id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findScaleConsumers(program, id) {
|
|
33
|
+
const consumers = [];
|
|
34
|
+
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
|
+
]) {
|
|
39
|
+
const encoding = layer.encoding?.[channel];
|
|
40
|
+
if (encoding?.scale === id) consumers.push({ layer, channel, encoding });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return consumers;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveConsumerValues(program, consumer) {
|
|
47
|
+
const dataset = findDataset(program, consumer.layer.data);
|
|
48
|
+
if (dataset === undefined) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Mark "${consumer.layer.id}" references unknown dataset "${consumer.layer.data}".`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Object.hasOwn(consumer.encoding, "datum")) {
|
|
55
|
+
return [normalizeRuleDatum(
|
|
56
|
+
consumer.encoding.datum,
|
|
57
|
+
consumer.encoding.fieldType,
|
|
58
|
+
consumer.channel
|
|
59
|
+
)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
consumer.channel === "strokeDash" &&
|
|
64
|
+
!["line", "rule"].includes(consumer.layer.mark?.type)
|
|
65
|
+
) {
|
|
66
|
+
throw new Error("strokeDash scale materialization requires a line mark or rule mark.");
|
|
67
|
+
}
|
|
68
|
+
const scale = findScale(program, consumer.encoding.scale);
|
|
69
|
+
const allowUnknown = Object.hasOwn(scale, "unknown");
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
["color", "strokeDash", "xOffset", "shape"].includes(consumer.channel) &&
|
|
73
|
+
consumer.encoding.fieldType === "nominal"
|
|
74
|
+
) {
|
|
75
|
+
return allowUnknown
|
|
76
|
+
? readScaleField(
|
|
77
|
+
dataset.values,
|
|
78
|
+
consumer.encoding.field,
|
|
79
|
+
consumer.encoding.fieldType,
|
|
80
|
+
{ allowUnknown: true }
|
|
81
|
+
)
|
|
82
|
+
: readNominalField(dataset.values, consumer.encoding.field);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
consumer.layer.mark?.type === "line" &&
|
|
87
|
+
isAggregate(consumer.layer.encoding?.y?.aggregate)
|
|
88
|
+
) {
|
|
89
|
+
const derived = deriveLineSeries(dataset.values, consumer.layer);
|
|
90
|
+
return consumer.channel === "x" ? derived.xValues : derived.yValues;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
resolveBarGrain(consumer.layer) === BAR_GRAINS.aggregate
|
|
95
|
+
) {
|
|
96
|
+
const derived = deriveBarAggregates(dataset.values, consumer.layer);
|
|
97
|
+
if (consumer.channel === "color") {
|
|
98
|
+
return derived.values.map(value => value.color);
|
|
99
|
+
}
|
|
100
|
+
return consumer.channel === "x" ? derived.xValues : derived.yValues;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (consumer.encoding.fieldType === "temporal") {
|
|
104
|
+
return allowUnknown
|
|
105
|
+
? readScaleField(dataset.values, consumer.encoding.field, "temporal", {
|
|
106
|
+
allowUnknown: true
|
|
107
|
+
})
|
|
108
|
+
: readTemporalField(dataset.values, consumer.encoding.field);
|
|
109
|
+
}
|
|
110
|
+
if (["nominal", "ordinal"].includes(consumer.encoding.fieldType)) {
|
|
111
|
+
if (!["ordinal", "band", "point"].includes(scale.type)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Scale materialization requires a quantitative encoding on mark "${consumer.layer.id}".`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return allowUnknown
|
|
117
|
+
? readScaleField(
|
|
118
|
+
dataset.values,
|
|
119
|
+
consumer.encoding.field,
|
|
120
|
+
consumer.encoding.fieldType,
|
|
121
|
+
{ allowUnknown: true }
|
|
122
|
+
)
|
|
123
|
+
: readNominalField(dataset.values, consumer.encoding.field);
|
|
124
|
+
}
|
|
125
|
+
if (consumer.encoding.fieldType !== "quantitative") {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Scale materialization requires a quantitative encoding on mark "${consumer.layer.id}".`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return allowUnknown
|
|
131
|
+
? readScaleField(dataset.values, consumer.encoding.field, "quantitative", {
|
|
132
|
+
allowUnknown: true
|
|
133
|
+
})
|
|
134
|
+
: readQuantitativeField(dataset.values, consumer.encoding.field);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolveHistogramCountValues(program, consumer) {
|
|
138
|
+
const xEncoding = consumer.layer.encoding?.x;
|
|
139
|
+
const xScale = xEncoding?.scale === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: findScale(program, xEncoding.scale);
|
|
142
|
+
const dataset = findDataset(program, consumer.layer.data);
|
|
143
|
+
|
|
144
|
+
if (xEncoding?.bin === undefined || xScale === undefined) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Histogram mark "${consumer.layer.id}" requires a binned x scale.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (dataset === undefined) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Histogram mark "${consumer.layer.id}" requires an existing dataset.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const xValues = readQuantitativeField(dataset.values, xEncoding.field);
|
|
156
|
+
const bins = resolveHistogramBins({
|
|
157
|
+
values: xValues,
|
|
158
|
+
bin: xEncoding.bin,
|
|
159
|
+
domain: xScale.domain,
|
|
160
|
+
nice: xScale.nice ?? true,
|
|
161
|
+
zero: xScale.zero ?? false
|
|
162
|
+
});
|
|
163
|
+
return countHistogramBins(xValues, bins.boundaries);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveHistogramPartitions(program, consumer) {
|
|
167
|
+
const layer = consumer.layer;
|
|
168
|
+
const dataset = findDataset(program, layer.data);
|
|
169
|
+
const xEncoding = layer.encoding.x;
|
|
170
|
+
const xScale = findScale(program, xEncoding.scale);
|
|
171
|
+
const xValues = readQuantitativeField(dataset.values, xEncoding.field);
|
|
172
|
+
const bins = resolveHistogramBins({
|
|
173
|
+
values: xValues,
|
|
174
|
+
bin: xEncoding.bin,
|
|
175
|
+
domain: xScale.domain,
|
|
176
|
+
nice: xScale.nice ?? true,
|
|
177
|
+
zero: xScale.zero ?? false
|
|
178
|
+
});
|
|
179
|
+
const colorEncoding = layer.encoding?.color;
|
|
180
|
+
if (colorEncoding?.scale === undefined) {
|
|
181
|
+
return countHistogramBins(xValues, bins.boundaries).map(value => [value]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const colorScale = findScale(program, colorEncoding.scale);
|
|
185
|
+
const colorValues = readNominalField(dataset.values, colorEncoding.field);
|
|
186
|
+
const colorDomain = resolveOrdinalDomain(colorScale.domain, colorValues);
|
|
187
|
+
const colorIndex = new Map(colorDomain.map((value, index) => [value, index]));
|
|
188
|
+
const partitions = bins.boundaries.slice(0, -1).map(() =>
|
|
189
|
+
colorDomain.map(() => 0)
|
|
190
|
+
);
|
|
191
|
+
for (let index = 0; index < xValues.length; index += 1) {
|
|
192
|
+
const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
|
|
193
|
+
const color = colorIndex.get(colorValues[index]);
|
|
194
|
+
if (bin !== -1 && color !== undefined) partitions[bin][color] += 1;
|
|
195
|
+
}
|
|
196
|
+
return partitions;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveAggregatePartitions(program, consumer) {
|
|
200
|
+
const layer = consumer.layer;
|
|
201
|
+
const dataset = findDataset(program, layer.data);
|
|
202
|
+
const derived = deriveBarAggregates(dataset.values, layer);
|
|
203
|
+
const channels = resolveBarChannels(layer);
|
|
204
|
+
const categoryEncoding = layer.encoding[channels.category];
|
|
205
|
+
const categoryScale = findScale(program, categoryEncoding.scale);
|
|
206
|
+
const categoryValues = channels.category === "x"
|
|
207
|
+
? derived.xValues
|
|
208
|
+
: derived.yValues;
|
|
209
|
+
const categoryDomain = resolveOrdinalDomain(categoryScale.domain, categoryValues);
|
|
210
|
+
const colorEncoding = layer.encoding?.color;
|
|
211
|
+
if (colorEncoding?.scale === undefined) {
|
|
212
|
+
const byCategory = new Map(derived.values.map(value => [
|
|
213
|
+
value[channels.category],
|
|
214
|
+
value[channels.measure]
|
|
215
|
+
]));
|
|
216
|
+
return categoryDomain.map(value => [byCategory.get(value) ?? 0]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const colorScale = findScale(program, colorEncoding.scale);
|
|
220
|
+
const colorValues = readNominalField(dataset.values, colorEncoding.field);
|
|
221
|
+
const colorDomain = resolveOrdinalDomain(colorScale.domain, colorValues);
|
|
222
|
+
const cells = new Map(derived.values.map(value => [
|
|
223
|
+
JSON.stringify([value[channels.category], value.color]),
|
|
224
|
+
value[channels.measure]
|
|
225
|
+
]));
|
|
226
|
+
return categoryDomain.map(category => colorDomain.map(color =>
|
|
227
|
+
cells.get(JSON.stringify([category, color])) ?? 0
|
|
228
|
+
));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveAreaPartitions(program, consumer) {
|
|
232
|
+
const layer = consumer.layer;
|
|
233
|
+
const dataset = findDataset(program, layer.data);
|
|
234
|
+
const transform = dataset?.transform?.length === 1 &&
|
|
235
|
+
dataset.transform[0].type === "density"
|
|
236
|
+
? dataset.transform[0]
|
|
237
|
+
: undefined;
|
|
238
|
+
if (transform === undefined) return undefined;
|
|
239
|
+
const derived = deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
240
|
+
if (derived.mode !== "y-density") return undefined;
|
|
241
|
+
const sampleCount = derived.series[0].values.length;
|
|
242
|
+
if (derived.series.some(series => series.values.length !== sampleCount)) {
|
|
243
|
+
throw new Error(`Area mark "${layer.id}" requires aligned layout samples.`);
|
|
244
|
+
}
|
|
245
|
+
return Array.from({ length: sampleCount }, (_, index) =>
|
|
246
|
+
derived.series.map(series => series.values[index].y)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function resolveSeriesLayoutScaleValues(program, consumer) {
|
|
251
|
+
if (consumer.layer.mark?.type === "bar") {
|
|
252
|
+
const grain = resolveBarGrain(consumer.layer);
|
|
253
|
+
const channels = resolveBarChannels(consumer.layer);
|
|
254
|
+
if (consumer.channel !== channels?.measure) return undefined;
|
|
255
|
+
const partitions = grain === BAR_GRAINS.histogram
|
|
256
|
+
? resolveHistogramPartitions(program, consumer)
|
|
257
|
+
: grain === BAR_GRAINS.aggregate
|
|
258
|
+
? resolveAggregatePartitions(program, consumer)
|
|
259
|
+
: undefined;
|
|
260
|
+
if (partitions === undefined) return undefined;
|
|
261
|
+
const layout = resolveBarColorLayout(consumer.layer);
|
|
262
|
+
return {
|
|
263
|
+
layout,
|
|
264
|
+
values: resolveSeriesLayoutDomainValues(partitions, layout)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (consumer.channel !== "y") return undefined;
|
|
268
|
+
if (consumer.layer.mark?.type === "area") {
|
|
269
|
+
const layout = consumer.layer.encoding?.color?.layout;
|
|
270
|
+
if (layout === undefined || layout === "overlay") return undefined;
|
|
271
|
+
const partitions = resolveAreaPartitions(program, consumer);
|
|
272
|
+
if (partitions === undefined) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Area layout "${layout}" currently requires vertical density series.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
layout,
|
|
279
|
+
values: resolveSeriesLayoutDomainValues(partitions, layout)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|