ggaction 0.0.7 → 0.0.8
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 +1 -1
- package/package.json +1 -1
- package/src/actions/categoryOrder/index.js +111 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/timeUnit.js +48 -0
- package/src/actions/encodings/angle.js +77 -0
- package/src/actions/encodings/color/index.js +17 -13
- package/src/actions/encodings/color/layout.js +2 -0
- package/src/actions/encodings/color/policy.js +3 -0
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/policies/area.js +40 -2
- package/src/actions/encodings/position/policies/bar.js +6 -0
- package/src/actions/encodings/position/policies/index.js +2 -0
- package/src/actions/encodings/position/policies/tick.js +19 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/continuous/common.js +17 -5
- package/src/actions/guides/legends/continuous/gradient.js +12 -8
- package/src/actions/guides/legends/continuous/opacity.js +70 -17
- package/src/actions/guides/legends/edit.js +13 -8
- package/src/actions/guides/legends/lane.js +468 -0
- package/src/actions/guides/legends/remove.js +3 -7
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +3 -1
- package/src/actions/marks/area/materialize.js +12 -3
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/point/materialize.js +17 -5
- package/src/actions/marks/tick/actions.js +225 -0
- package/src/actions/marks/tick/index.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +2 -0
- package/src/actions/scales/consumers/index.js +8 -0
- package/src/actions/scales/consumers/seriesLayout.js +22 -2
- package/src/actions/scales/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- package/src/core/vocabulary.js +7 -3
- package/src/grammar/areaSeries.js +112 -2
- package/src/grammar/categoryOrder.js +138 -0
- package/src/grammar/direction.js +46 -0
- package/src/grammar/facets/index.js +1 -1
- package/src/grammar/pointShapes.js +34 -6
- package/src/grammar/positionCompatibility.js +4 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/seriesLayout.js +10 -2
- package/src/grammar/timeUnit.js +108 -0
- package/src/grammar/transforms.js +6 -0
- package/src/grammar/window.js +73 -7
- package/src/layout/legendLane.js +338 -0
- package/src/materialization/marks/capabilities.js +9 -0
- package/src/materialization/marks/index.js +2 -1
- package/src/materialization/marks/policies.js +13 -0
- package/src/materialization/scales/policies/series.js +9 -0
- package/src/materialization/scales/resolve.js +25 -3
- package/src/materialization/selection/items/index.js +1 -0
- package/src/materialization/selection/items/path.js +4 -1
- package/src/materialization/selection/items/tick.js +42 -0
- package/src/materialization/selection/policies/index.js +2 -0
- package/src/materialization/selection/policies/tick.js +10 -0
- package/types/index.d.ts +10 -0
- package/types/program.d.ts +96 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import { readQuantitativeField } from "./scales/index.js";
|
|
3
|
+
|
|
4
|
+
function finite(value, label) {
|
|
5
|
+
if (!Number.isFinite(value)) {
|
|
6
|
+
throw new TypeError(`${label} must be finite.`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function directionVector(degrees) {
|
|
12
|
+
const radians = finite(degrees, "Direction degrees") * Math.PI / 180;
|
|
13
|
+
return cloneAndFreeze({
|
|
14
|
+
x: Math.sin(radians),
|
|
15
|
+
y: -Math.cos(radians)
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function centeredDirectionalSegment({ x, y, degrees = 0, length }) {
|
|
20
|
+
const centerX = finite(x, "Directional segment x");
|
|
21
|
+
const centerY = finite(y, "Directional segment y");
|
|
22
|
+
const resolvedLength = finite(length, "Directional segment length");
|
|
23
|
+
if (resolvedLength <= 0) {
|
|
24
|
+
throw new RangeError("Directional segment length must be positive.");
|
|
25
|
+
}
|
|
26
|
+
const vector = directionVector(degrees);
|
|
27
|
+
const half = resolvedLength / 2;
|
|
28
|
+
return cloneAndFreeze({
|
|
29
|
+
x1: centerX - vector.x * half,
|
|
30
|
+
y1: centerY - vector.y * half,
|
|
31
|
+
x2: centerX + vector.x * half,
|
|
32
|
+
y2: centerY + vector.y * half
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveDirectionValues(rows, encoding) {
|
|
37
|
+
if (encoding === undefined) return undefined;
|
|
38
|
+
if (Object.hasOwn(encoding, "datum")) {
|
|
39
|
+
const value = finite(encoding.datum, "Direction value");
|
|
40
|
+
return cloneAndFreeze(rows.map(() => value));
|
|
41
|
+
}
|
|
42
|
+
if (encoding.fieldType !== "quantitative") {
|
|
43
|
+
throw new Error("Direction field must be quantitative.");
|
|
44
|
+
}
|
|
45
|
+
return readQuantitativeField(rows, encoding.field);
|
|
46
|
+
}
|
|
@@ -5,7 +5,7 @@ import { planFacetDependencies } from "./dependencies.js";
|
|
|
5
5
|
import { readNominalField } from "../scales/index.js";
|
|
6
6
|
|
|
7
7
|
const SUPPORTED_MARKS = new Set([
|
|
8
|
-
"point", "line", "area", "bar", "rule", "rect"
|
|
8
|
+
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
9
9
|
]);
|
|
10
10
|
const SUPPORTED_BAR_GRAINS = new Set([
|
|
11
11
|
BAR_GRAINS.histogram,
|
|
@@ -77,6 +77,12 @@ function shapePolygon(shape) {
|
|
|
77
77
|
{ x: -1, y: -arm }, { x: -arm, y: -arm }
|
|
78
78
|
];
|
|
79
79
|
return {
|
|
80
|
+
square: [
|
|
81
|
+
{ x: -1, y: -1 },
|
|
82
|
+
{ x: 1, y: -1 },
|
|
83
|
+
{ x: 1, y: 1 },
|
|
84
|
+
{ x: -1, y: 1 }
|
|
85
|
+
],
|
|
80
86
|
diamond: [
|
|
81
87
|
{ x: 0, y: -1 },
|
|
82
88
|
{ x: 1, y: 0 },
|
|
@@ -120,7 +126,8 @@ export function createPointShapeGraphic({
|
|
|
120
126
|
fill,
|
|
121
127
|
stroke,
|
|
122
128
|
strokeWidth,
|
|
123
|
-
opacity
|
|
129
|
+
opacity,
|
|
130
|
+
angle
|
|
124
131
|
}) {
|
|
125
132
|
const validated = validatePointShape(shape);
|
|
126
133
|
if (![x, y, area].every(Number.isFinite) || area < 0) {
|
|
@@ -130,13 +137,16 @@ export function createPointShapeGraphic({
|
|
|
130
137
|
throw new TypeError("Point shape fill must be a non-empty string.");
|
|
131
138
|
}
|
|
132
139
|
const shared = appearance(fill, stroke, strokeWidth, opacity);
|
|
140
|
+
if (angle !== undefined && !Number.isFinite(angle)) {
|
|
141
|
+
throw new TypeError("Point shape angle must be finite degrees.");
|
|
142
|
+
}
|
|
133
143
|
if (validated === "circle") {
|
|
134
144
|
return {
|
|
135
145
|
type: "circle",
|
|
136
146
|
properties: { x, y, radius: Math.sqrt(area / Math.PI), ...shared }
|
|
137
147
|
};
|
|
138
148
|
}
|
|
139
|
-
if (validated === "square") {
|
|
149
|
+
if (validated === "square" && angle === undefined) {
|
|
140
150
|
const side = Math.sqrt(area);
|
|
141
151
|
return {
|
|
142
152
|
type: "rect",
|
|
@@ -155,7 +165,12 @@ export function createPointShapeGraphic({
|
|
|
155
165
|
type: "path",
|
|
156
166
|
properties: {
|
|
157
167
|
commands: buildLinearPathCommands(
|
|
158
|
-
normalizePolygon(
|
|
168
|
+
normalizePolygon(
|
|
169
|
+
rotate(shapePolygon(validated), (angle ?? 0) * Math.PI / 180),
|
|
170
|
+
x,
|
|
171
|
+
y,
|
|
172
|
+
area
|
|
173
|
+
),
|
|
159
174
|
{ close: true }
|
|
160
175
|
),
|
|
161
176
|
...shared
|
|
@@ -163,7 +178,12 @@ export function createPointShapeGraphic({
|
|
|
163
178
|
};
|
|
164
179
|
}
|
|
165
180
|
|
|
166
|
-
export function resolvePointShapeExtent({
|
|
181
|
+
export function resolvePointShapeExtent({
|
|
182
|
+
shape,
|
|
183
|
+
area,
|
|
184
|
+
strokeWidth = 0,
|
|
185
|
+
angle
|
|
186
|
+
}) {
|
|
167
187
|
const validated = validatePointShape(shape);
|
|
168
188
|
if (!Number.isFinite(area) || area < 0) {
|
|
169
189
|
throw new TypeError("Point shape area must be finite and non-negative.");
|
|
@@ -176,11 +196,19 @@ export function resolvePointShapeExtent({ shape, area, strokeWidth = 0 }) {
|
|
|
176
196
|
const radius = Math.sqrt(area / Math.PI) + strokeExtent;
|
|
177
197
|
return cloneAndFreeze({ x: radius, y: radius });
|
|
178
198
|
}
|
|
179
|
-
if (
|
|
199
|
+
if (angle !== undefined && !Number.isFinite(angle)) {
|
|
200
|
+
throw new TypeError("Point shape angle must be finite degrees.");
|
|
201
|
+
}
|
|
202
|
+
if (validated === "square" && angle === undefined) {
|
|
180
203
|
const halfSide = Math.sqrt(area) / 2 + strokeExtent;
|
|
181
204
|
return cloneAndFreeze({ x: halfSide, y: halfSide });
|
|
182
205
|
}
|
|
183
|
-
const points = normalizePolygon(
|
|
206
|
+
const points = normalizePolygon(
|
|
207
|
+
rotate(shapePolygon(validated), (angle ?? 0) * Math.PI / 180),
|
|
208
|
+
0,
|
|
209
|
+
0,
|
|
210
|
+
area
|
|
211
|
+
);
|
|
184
212
|
return cloneAndFreeze({
|
|
185
213
|
x: Math.max(0, ...points.map(point => Math.abs(point.x))) + strokeExtent,
|
|
186
214
|
y: Math.max(0, ...points.map(point => Math.abs(point.y))) + strokeExtent
|
|
@@ -23,6 +23,10 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
|
|
|
23
23
|
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
24
24
|
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
25
25
|
}),
|
|
26
|
+
tick: Object.freeze({
|
|
27
|
+
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
28
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
29
|
+
}),
|
|
26
30
|
bar: Object.freeze({
|
|
27
31
|
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
28
32
|
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
@@ -31,7 +31,8 @@ const ENCODING_PATHS = Object.freeze([
|
|
|
31
31
|
"encoding.parallel.missing",
|
|
32
32
|
...CARTESIAN_POSITION_CHANNELS.flatMap(channel => [
|
|
33
33
|
`encoding.${channel}.aggregate`,
|
|
34
|
-
`encoding.${channel}.stack
|
|
34
|
+
`encoding.${channel}.stack`,
|
|
35
|
+
`encoding.${channel}.categoryOrder`
|
|
35
36
|
])
|
|
36
37
|
]);
|
|
37
38
|
|
|
@@ -35,13 +35,13 @@ export function layoutSeriesPartition(
|
|
|
35
35
|
));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
if (
|
|
38
|
+
if (["stack", "fill", "center"].includes(layout)) {
|
|
39
39
|
if (values.some(value => value < 0)) {
|
|
40
40
|
throw new RangeError(`${layout} layout requires non-negative values.`);
|
|
41
41
|
}
|
|
42
42
|
const total = values.reduce((sum, value) => sum + value, 0);
|
|
43
43
|
if (layout === "fill" && total === 0) return cloneAndFreeze([]);
|
|
44
|
-
let offset = 0;
|
|
44
|
+
let offset = layout === "center" ? -total / 2 : 0;
|
|
45
45
|
return cloneAndFreeze(values.flatMap((value, index) => {
|
|
46
46
|
if (value === 0) return [];
|
|
47
47
|
const resolvedValue = layout === "fill" ? value / total : value;
|
|
@@ -78,6 +78,14 @@ export function resolveSeriesLayoutDomainValues(partitions, layout) {
|
|
|
78
78
|
...partition
|
|
79
79
|
]);
|
|
80
80
|
}
|
|
81
|
+
if (layout === "center") {
|
|
82
|
+
return partitions.flatMap(partition => {
|
|
83
|
+
const segments = layoutSeriesPartition(partition, layout);
|
|
84
|
+
return segments.length === 0
|
|
85
|
+
? [DEFAULT_SERIES_BASELINE]
|
|
86
|
+
: segments.flatMap(segment => [segment.start, segment.end]);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
81
89
|
return partitions.flatMap(partition =>
|
|
82
90
|
layoutSeriesPartition(partition, layout).flatMap(segment => [
|
|
83
91
|
segment.start,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
3
|
+
|
|
4
|
+
export const TIME_UNITS = cloneAndFreeze([
|
|
5
|
+
"year",
|
|
6
|
+
"quarter",
|
|
7
|
+
"month",
|
|
8
|
+
"day",
|
|
9
|
+
"hour",
|
|
10
|
+
"minute",
|
|
11
|
+
"second"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as"]);
|
|
15
|
+
|
|
16
|
+
function requireField(value, label) {
|
|
17
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rejectUnknownKeys(value) {
|
|
24
|
+
const unknown = Object.keys(value).find(key => !TRANSFORM_KEYS.includes(key));
|
|
25
|
+
if (unknown !== undefined) {
|
|
26
|
+
throw new Error(`Unknown time-unit transform property "${unknown}".`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateTimeUnitTransform(transform) {
|
|
31
|
+
if (!isPlainObject(transform)) {
|
|
32
|
+
throw new TypeError("Time-unit transform must be a plain object.");
|
|
33
|
+
}
|
|
34
|
+
rejectUnknownKeys(transform);
|
|
35
|
+
if (transform.type !== "timeUnit") {
|
|
36
|
+
throw new Error(`Unsupported time-unit transform "${transform.type}".`);
|
|
37
|
+
}
|
|
38
|
+
const field = requireField(transform.field, "Time-unit field");
|
|
39
|
+
const output = requireField(transform.as, "Time-unit output field");
|
|
40
|
+
if (field === output) {
|
|
41
|
+
throw new Error("Time-unit input and output fields must be distinct.");
|
|
42
|
+
}
|
|
43
|
+
if (!TIME_UNITS.includes(transform.unit)) {
|
|
44
|
+
throw new Error(`Unsupported time unit "${transform.unit}".`);
|
|
45
|
+
}
|
|
46
|
+
return transform;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeTimeUnitTransform({ field, unit, as } = {}) {
|
|
50
|
+
const transform = { type: "timeUnit", field, unit, as };
|
|
51
|
+
validateTimeUnitTransform(transform);
|
|
52
|
+
return cloneAndFreeze(transform);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function utcTimestamp(year, month = 0, day = 1, hour = 0, minute = 0) {
|
|
56
|
+
const date = new Date(0);
|
|
57
|
+
date.setUTCFullYear(year, month, day);
|
|
58
|
+
date.setUTCHours(hour, minute, 0, 0);
|
|
59
|
+
return date.getTime();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function floorUtcTimeUnit(timestamp, unit) {
|
|
63
|
+
if (!Number.isFinite(timestamp)) {
|
|
64
|
+
throw new TypeError("Time-unit timestamp must be finite.");
|
|
65
|
+
}
|
|
66
|
+
if (!TIME_UNITS.includes(unit)) {
|
|
67
|
+
throw new Error(`Unsupported time unit "${unit}".`);
|
|
68
|
+
}
|
|
69
|
+
const date = new Date(timestamp);
|
|
70
|
+
if (!Number.isFinite(date.getTime())) {
|
|
71
|
+
throw new TypeError("Time-unit timestamp must represent a valid date.");
|
|
72
|
+
}
|
|
73
|
+
const year = date.getUTCFullYear();
|
|
74
|
+
const month = date.getUTCMonth();
|
|
75
|
+
if (unit === "year") return utcTimestamp(year);
|
|
76
|
+
if (unit === "quarter") return utcTimestamp(year, Math.floor(month / 3) * 3);
|
|
77
|
+
if (unit === "month") return utcTimestamp(year, month);
|
|
78
|
+
const day = date.getUTCDate();
|
|
79
|
+
if (unit === "day") return utcTimestamp(year, month, day);
|
|
80
|
+
const hour = date.getUTCHours();
|
|
81
|
+
if (unit === "hour") return utcTimestamp(year, month, day, hour);
|
|
82
|
+
const minute = date.getUTCMinutes();
|
|
83
|
+
if (unit === "minute") return utcTimestamp(year, month, day, hour, minute);
|
|
84
|
+
return Math.floor(timestamp / 1000) * 1000;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function deriveTimeUnitRows(rows, transform) {
|
|
88
|
+
validateTimeUnitTransform(transform);
|
|
89
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
90
|
+
throw new TypeError("Time-unit source must contain plain row objects.");
|
|
91
|
+
}
|
|
92
|
+
const values = rows.map((row, index) => {
|
|
93
|
+
if (!Object.hasOwn(row, transform.field)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Time-unit source does not contain field "${transform.field}" at row ${index}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (Object.hasOwn(row, transform.as)) {
|
|
99
|
+
throw new Error(`Time-unit output field "${transform.as}" already exists.`);
|
|
100
|
+
}
|
|
101
|
+
const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index);
|
|
102
|
+
return {
|
|
103
|
+
...row,
|
|
104
|
+
[transform.as]: floorUtcTimeUnit(timestamp, transform.unit)
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
return cloneAndFreeze(values);
|
|
108
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { validateMarkFilterTransform } from "./markFilter.js";
|
|
19
19
|
import { validateRegressionTransform } from "./regression/index.js";
|
|
20
20
|
import { validateWindowTransform } from "./window.js";
|
|
21
|
+
import { validateTimeUnitTransform } from "./timeUnit.js";
|
|
21
22
|
|
|
22
23
|
function requestedDensityTransform(transform) {
|
|
23
24
|
const { resolved: _resolved, ...requested } = transform;
|
|
@@ -94,6 +95,11 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
94
95
|
materializeOp: "materializeRegressionData",
|
|
95
96
|
facetTopology: "statistical"
|
|
96
97
|
}),
|
|
98
|
+
timeUnit: Object.freeze({
|
|
99
|
+
validate: validateTimeUnitTransform,
|
|
100
|
+
materializeOp: "materializeTimeUnitData",
|
|
101
|
+
facetTopology: "rowPreserving"
|
|
102
|
+
}),
|
|
97
103
|
window: Object.freeze({
|
|
98
104
|
validate: validateWindowTransform,
|
|
99
105
|
materializeOp: "materializeWindowData",
|
package/src/grammar/window.js
CHANGED
|
@@ -6,10 +6,13 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
6
|
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
7
7
|
const ORDER_VALUES = Object.freeze(["ascending", "descending"]);
|
|
8
8
|
const OPERATION_VALUES = Object.freeze([
|
|
9
|
-
"rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead"
|
|
9
|
+
"rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead",
|
|
10
|
+
"movingMean", "movingSum"
|
|
10
11
|
]);
|
|
11
12
|
const POSITION_OPERATIONS = new Set(["rowNumber", "rank", "denseRank"]);
|
|
12
13
|
const OFFSET_OPERATIONS = new Set(["lag", "lead"]);
|
|
14
|
+
const MOVING_OPERATIONS = new Set(["movingMean", "movingSum"]);
|
|
15
|
+
const FRAME_KEYS = Object.freeze(["preceding", "following"]);
|
|
13
16
|
|
|
14
17
|
function requireField(value, label) {
|
|
15
18
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -63,9 +66,29 @@ function operationKeys(operation) {
|
|
|
63
66
|
if (OFFSET_OPERATIONS.has(operation.op)) {
|
|
64
67
|
return ["op", "field", "as", "offset", "default"];
|
|
65
68
|
}
|
|
69
|
+
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
70
|
+
return ["op", "field", "as", "frame"];
|
|
71
|
+
}
|
|
66
72
|
return ["op"];
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
function validateFrame(frame, operation) {
|
|
76
|
+
if (!isPlainObject(frame)) {
|
|
77
|
+
throw new TypeError(`Window ${operation} frame must be a plain object.`);
|
|
78
|
+
}
|
|
79
|
+
rejectUnknownKeys(frame, FRAME_KEYS, `window ${operation} frame`);
|
|
80
|
+
if (!Number.isInteger(frame.preceding) || frame.preceding < 0) {
|
|
81
|
+
throw new RangeError(
|
|
82
|
+
`Window ${operation} frame preceding must be a non-negative integer.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isInteger(frame.following) || frame.following < 0) {
|
|
86
|
+
throw new RangeError(
|
|
87
|
+
`Window ${operation} frame following must be a non-negative integer.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
function validateOperations(operations, sortBy) {
|
|
70
93
|
if (!Array.isArray(operations) || operations.length === 0) {
|
|
71
94
|
throw new TypeError("Window operations must be a non-empty array.");
|
|
@@ -98,6 +121,9 @@ function validateOperations(operations, sortBy) {
|
|
|
98
121
|
throw new TypeError(`Window ${operation.op} requires a default value.`);
|
|
99
122
|
}
|
|
100
123
|
}
|
|
124
|
+
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
125
|
+
validateFrame(operation.frame, operation.op);
|
|
126
|
+
}
|
|
101
127
|
});
|
|
102
128
|
}
|
|
103
129
|
|
|
@@ -132,14 +158,26 @@ function normalizeSortBy(value) {
|
|
|
132
158
|
function normalizeOperations(value) {
|
|
133
159
|
if (!Array.isArray(value)) return value;
|
|
134
160
|
return value.map(operation => {
|
|
135
|
-
if (!isPlainObject(operation)
|
|
161
|
+
if (!isPlainObject(operation)) {
|
|
136
162
|
return operation;
|
|
137
163
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
if (OFFSET_OPERATIONS.has(operation.op)) {
|
|
165
|
+
return {
|
|
166
|
+
...operation,
|
|
167
|
+
offset: operation.offset ?? 1,
|
|
168
|
+
default: Object.hasOwn(operation, "default") ? operation.default : null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (MOVING_OPERATIONS.has(operation.op) && isPlainObject(operation.frame)) {
|
|
172
|
+
return {
|
|
173
|
+
...operation,
|
|
174
|
+
frame: {
|
|
175
|
+
...operation.frame,
|
|
176
|
+
following: operation.frame.following ?? 0
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return operation;
|
|
143
181
|
});
|
|
144
182
|
}
|
|
145
183
|
|
|
@@ -305,6 +343,34 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
305
343
|
}
|
|
306
344
|
return;
|
|
307
345
|
}
|
|
346
|
+
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
347
|
+
partition.forEach((entry, index) => {
|
|
348
|
+
const first = Math.max(0, index - operation.frame.preceding);
|
|
349
|
+
const last = Math.min(
|
|
350
|
+
partition.length - 1,
|
|
351
|
+
index + operation.frame.following
|
|
352
|
+
);
|
|
353
|
+
let total = 0;
|
|
354
|
+
for (let peerIndex = first; peerIndex <= last; peerIndex += 1) {
|
|
355
|
+
const value = partition[peerIndex].row[operation.field];
|
|
356
|
+
if (!Number.isFinite(value)) {
|
|
357
|
+
throw new TypeError(
|
|
358
|
+
`Window ${operation.op} field "${operation.field}" must contain finite numbers.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
total += value;
|
|
362
|
+
if (!Number.isFinite(total)) {
|
|
363
|
+
throw new RangeError(
|
|
364
|
+
`Window ${operation.op} output "${operation.as}" must be finite.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
entry.row[operation.as] = operation.op === "movingMean"
|
|
369
|
+
? total / (last - first + 1)
|
|
370
|
+
: total;
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
308
374
|
const direction = operation.op === "lag" ? -1 : 1;
|
|
309
375
|
partition.forEach((entry, index) => {
|
|
310
376
|
const peer = partition[index + direction * operation.offset];
|