ggaction 0.0.4 → 0.0.6

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.
Files changed (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -1,29 +1,118 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
2
  import { validateBoxTransform } from "./boxPlot.js";
3
+ import {
4
+ requestedBin2DTransform,
5
+ validateBin2DTransform
6
+ } from "./bin2d.js";
3
7
  import { validateDensityTransform } from "./density.js";
4
8
  import { validateFilterTransform } from "./filter.js";
9
+ import {
10
+ requestedGradientProfileTransform,
11
+ validateGradientProfileTransform
12
+ } from "./gradientProfile.js";
5
13
  import { validateIntervalTransform } from "./interval.js";
14
+ import {
15
+ requestedHorizonTransform,
16
+ validateHorizonTransform
17
+ } from "./horizon.js";
6
18
  import { validateMarkFilterTransform } from "./markFilter.js";
7
19
  import { validateRegressionTransform } from "./regression/index.js";
20
+ import { validateWindowTransform } from "./window.js";
8
21
 
9
- const TRANSFORM_VALIDATORS = Object.freeze({
10
- boxOutlier: validateBoxTransform,
11
- boxSummary: validateBoxTransform,
12
- density: validateDensityTransform,
13
- filter: validateFilterTransform,
14
- interval: validateIntervalTransform,
15
- markFilter: validateMarkFilterTransform,
16
- regression: validateRegressionTransform
22
+ function requestedDensityTransform(transform) {
23
+ const { resolved: _resolved, ...requested } = transform;
24
+ return requested;
25
+ }
26
+
27
+ function facetHorizonTransform(transform, { scales = {} } = {}) {
28
+ if (
29
+ transform.extent !== "auto" ||
30
+ (scales.y ?? "shared") !== "shared" ||
31
+ !Array.isArray(transform.resolved?.extents)
32
+ ) return transform;
33
+ const extent = Math.max(...transform.resolved.extents.map(item => item.extent));
34
+ if (!(extent > 0)) return transform;
35
+ return {
36
+ ...requestedHorizonTransform(transform),
37
+ extent
38
+ };
39
+ }
40
+
41
+ const TRANSFORM_POLICIES = Object.freeze({
42
+ bin2d: Object.freeze({
43
+ validate: validateBin2DTransform,
44
+ materializeOp: "materializeBin2DData",
45
+ facetTopology: "statistical",
46
+ replayTransform: requestedBin2DTransform
47
+ }),
48
+ boxOutlier: Object.freeze({
49
+ validate: validateBoxTransform,
50
+ materializeOp: "materializeBoxOutlierData",
51
+ facetTopology: "statistical"
52
+ }),
53
+ boxSummary: Object.freeze({
54
+ validate: validateBoxTransform,
55
+ materializeOp: "materializeBoxSummaryData",
56
+ facetTopology: "statistical"
57
+ }),
58
+ density: Object.freeze({
59
+ validate: validateDensityTransform,
60
+ materializeOp: "materializeDensityData",
61
+ facetTopology: "statistical",
62
+ replayTransform: requestedDensityTransform
63
+ }),
64
+ filter: Object.freeze({
65
+ validate: validateFilterTransform,
66
+ materializeOp: "materializeFilteredData",
67
+ facetTopology: "rowPreserving"
68
+ }),
69
+ gradientProfile: Object.freeze({
70
+ validate: validateGradientProfileTransform,
71
+ materializeOp: "materializeGradientProfileData",
72
+ facetTopology: "statistical",
73
+ replayTransform: requestedGradientProfileTransform
74
+ }),
75
+ horizon: Object.freeze({
76
+ validate: validateHorizonTransform,
77
+ materializeOp: "materializeHorizonData",
78
+ facetTopology: "statistical",
79
+ replayTransform: requestedHorizonTransform,
80
+ facetReplayTransform: facetHorizonTransform
81
+ }),
82
+ interval: Object.freeze({
83
+ validate: validateIntervalTransform,
84
+ materializeOp: "materializeIntervalData",
85
+ facetTopology: "statistical"
86
+ }),
87
+ markFilter: Object.freeze({
88
+ validate: validateMarkFilterTransform,
89
+ materializeOp: "materializeMarkFilteredData",
90
+ provenanceTransparent: true
91
+ }),
92
+ regression: Object.freeze({
93
+ validate: validateRegressionTransform,
94
+ materializeOp: "materializeRegressionData",
95
+ facetTopology: "statistical"
96
+ }),
97
+ window: Object.freeze({
98
+ validate: validateWindowTransform,
99
+ materializeOp: "materializeWindowData",
100
+ facetTopology: "statistical"
101
+ })
17
102
  });
18
103
 
104
+ export function findTransformPolicy(type) {
105
+ return TRANSFORM_POLICIES[type];
106
+ }
107
+
19
108
  export function findTransformValidator(type) {
20
- return TRANSFORM_VALIDATORS[type];
109
+ return findTransformPolicy(type)?.validate;
21
110
  }
22
111
 
23
112
  export function validateDatasetTransforms(value) {
24
- if (!Array.isArray(value) || value.length === 0 || !value.every(isPlainObject)) {
113
+ if (!Array.isArray(value) || value.length !== 1 || !isPlainObject(value[0])) {
25
114
  throw new TypeError(
26
- "Dataset transform must be a non-empty array of plain objects."
115
+ "Dataset transform must contain exactly one plain object."
27
116
  );
28
117
  }
29
118
  for (const transform of value) {
@@ -0,0 +1,339 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const TRANSFORM_KEYS = Object.freeze([
4
+ "type", "partitionBy", "sortBy", "operations"
5
+ ]);
6
+ const SORT_KEYS = Object.freeze(["field", "order"]);
7
+ const ORDER_VALUES = Object.freeze(["ascending", "descending"]);
8
+ const OPERATION_VALUES = Object.freeze([
9
+ "rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead"
10
+ ]);
11
+ const POSITION_OPERATIONS = new Set(["rowNumber", "rank", "denseRank"]);
12
+ const OFFSET_OPERATIONS = new Set(["lag", "lead"]);
13
+
14
+ function requireField(value, label) {
15
+ if (typeof value !== "string" || value.length === 0) {
16
+ throw new TypeError(`${label} must be a non-empty string.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function rejectUnknownKeys(value, supported, label) {
22
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
23
+ if (unknown !== undefined) {
24
+ throw new Error(`Unknown ${label} property "${unknown}".`);
25
+ }
26
+ }
27
+
28
+ function validateFieldList(value, label) {
29
+ if (
30
+ !Array.isArray(value) ||
31
+ value.some(field => typeof field !== "string" || field.length === 0)
32
+ ) {
33
+ throw new TypeError(`${label} must contain field names.`);
34
+ }
35
+ if (new Set(value).size !== value.length) {
36
+ throw new Error(`${label} fields must be unique.`);
37
+ }
38
+ }
39
+
40
+ function validateSortBy(sortBy) {
41
+ if (!Array.isArray(sortBy)) {
42
+ throw new TypeError("Window sortBy must be an array.");
43
+ }
44
+ const fields = [];
45
+ sortBy.forEach((sort, index) => {
46
+ if (!isPlainObject(sort)) {
47
+ throw new TypeError(`Window sortBy[${index}] must be a plain object.`);
48
+ }
49
+ rejectUnknownKeys(sort, SORT_KEYS, `window sortBy[${index}]`);
50
+ fields.push(requireField(sort.field, `Window sortBy[${index}].field`));
51
+ if (!ORDER_VALUES.includes(sort.order)) {
52
+ throw new Error(`Unsupported window sort order "${sort.order}".`);
53
+ }
54
+ });
55
+ if (new Set(fields).size !== fields.length) {
56
+ throw new Error("Window sortBy fields must be unique.");
57
+ }
58
+ }
59
+
60
+ function operationKeys(operation) {
61
+ if (POSITION_OPERATIONS.has(operation.op)) return ["op", "as"];
62
+ if (operation.op === "cumulativeSum") return ["op", "field", "as"];
63
+ if (OFFSET_OPERATIONS.has(operation.op)) {
64
+ return ["op", "field", "as", "offset", "default"];
65
+ }
66
+ return ["op"];
67
+ }
68
+
69
+ function validateOperations(operations, sortBy) {
70
+ if (!Array.isArray(operations) || operations.length === 0) {
71
+ throw new TypeError("Window operations must be a non-empty array.");
72
+ }
73
+ const outputs = new Set();
74
+ operations.forEach((operation, index) => {
75
+ if (!isPlainObject(operation)) {
76
+ throw new TypeError(`Window operation ${index} must be a plain object.`);
77
+ }
78
+ if (!OPERATION_VALUES.includes(operation.op)) {
79
+ throw new Error(`Unsupported window operation "${operation.op}".`);
80
+ }
81
+ rejectUnknownKeys(operation, operationKeys(operation), `window operation ${index}`);
82
+ const output = requireField(operation.as, `Window operation ${index} as`);
83
+ if (outputs.has(output)) {
84
+ throw new Error(`Window output field "${output}" must be unique.`);
85
+ }
86
+ outputs.add(output);
87
+ if (["rank", "denseRank"].includes(operation.op) && sortBy.length === 0) {
88
+ throw new Error(`${operation.op} requires a non-empty window sortBy.`);
89
+ }
90
+ if (!POSITION_OPERATIONS.has(operation.op)) {
91
+ requireField(operation.field, `Window ${operation.op} field`);
92
+ }
93
+ if (OFFSET_OPERATIONS.has(operation.op)) {
94
+ if (!Number.isInteger(operation.offset) || operation.offset <= 0) {
95
+ throw new RangeError(`Window ${operation.op} offset must be a positive integer.`);
96
+ }
97
+ if (!Object.hasOwn(operation, "default")) {
98
+ throw new TypeError(`Window ${operation.op} requires a default value.`);
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ export function validateWindowTransform(transform) {
105
+ if (!isPlainObject(transform)) {
106
+ throw new TypeError("Window transform must be a plain object.");
107
+ }
108
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "window transform");
109
+ if (transform.type !== "window") {
110
+ throw new Error(`Unsupported window transform "${transform.type}".`);
111
+ }
112
+ validateFieldList(transform.partitionBy, "Window partitionBy");
113
+ validateSortBy(transform.sortBy);
114
+ validateOperations(transform.operations, transform.sortBy);
115
+ return transform;
116
+ }
117
+
118
+ function normalizePartitionBy(value) {
119
+ if (value === undefined) return [];
120
+ return Array.isArray(value) ? [...value] : [value];
121
+ }
122
+
123
+ function normalizeSortBy(value) {
124
+ if (value === undefined) return [];
125
+ if (!Array.isArray(value)) return value;
126
+ return value.map(sort => isPlainObject(sort)
127
+ ? { field: sort.field, order: sort.order ?? "ascending" }
128
+ : sort
129
+ );
130
+ }
131
+
132
+ function normalizeOperations(value) {
133
+ if (!Array.isArray(value)) return value;
134
+ return value.map(operation => {
135
+ if (!isPlainObject(operation) || !OFFSET_OPERATIONS.has(operation.op)) {
136
+ return operation;
137
+ }
138
+ return {
139
+ ...operation,
140
+ offset: operation.offset ?? 1,
141
+ default: Object.hasOwn(operation, "default") ? operation.default : null
142
+ };
143
+ });
144
+ }
145
+
146
+ export function normalizeWindowTransform({
147
+ partitionBy,
148
+ sortBy,
149
+ operations
150
+ } = {}) {
151
+ const transform = {
152
+ type: "window",
153
+ partitionBy: normalizePartitionBy(partitionBy),
154
+ sortBy: normalizeSortBy(sortBy),
155
+ operations: normalizeOperations(operations)
156
+ };
157
+ validateWindowTransform(transform);
158
+ return cloneAndFreeze(transform);
159
+ }
160
+
161
+ function scalarKey(value, label) {
162
+ if (value === null) return "null";
163
+ if (typeof value === "string") return `string:${value.length}:${value}`;
164
+ if (typeof value === "boolean") return `boolean:${value}`;
165
+ if (typeof value === "number" && Number.isFinite(value)) {
166
+ return `number:${Object.is(value, -0) ? 0 : value}`;
167
+ }
168
+ throw new TypeError(
169
+ `${label} must contain null, strings, booleans, or finite numbers.`
170
+ );
171
+ }
172
+
173
+ function compareScalar(left, right, label) {
174
+ const leftMissing = left === null || left === undefined;
175
+ const rightMissing = right === null || right === undefined;
176
+ if (leftMissing || rightMissing) {
177
+ if (leftMissing && rightMissing) return 0;
178
+ return leftMissing ? 1 : -1;
179
+ }
180
+ if (typeof left !== typeof right || !["number", "string", "boolean"].includes(
181
+ typeof left
182
+ )) {
183
+ throw new TypeError(`${label} must contain one comparable primitive type.`);
184
+ }
185
+ if (typeof left === "number" && (!Number.isFinite(left) || !Number.isFinite(right))) {
186
+ throw new TypeError(`${label} must contain finite numbers.`);
187
+ }
188
+ if (left === right) return 0;
189
+ return left < right ? -1 : 1;
190
+ }
191
+
192
+ function compareEntries(left, right, sortBy, stable = true) {
193
+ for (const sort of sortBy) {
194
+ const comparison = compareScalar(
195
+ left.row[sort.field],
196
+ right.row[sort.field],
197
+ `Window sort field "${sort.field}"`
198
+ );
199
+ if (comparison !== 0) {
200
+ return sort.order === "ascending" ? comparison : -comparison;
201
+ }
202
+ }
203
+ return stable ? left.index - right.index : 0;
204
+ }
205
+
206
+ function validateSourceFields(rows, transform) {
207
+ const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
208
+ for (const field of [...transform.partitionBy, ...transform.sortBy.map(sort => sort.field)]) {
209
+ if (!sourceFields.has(field)) {
210
+ throw new Error(`Window source does not contain field "${field}".`);
211
+ }
212
+ }
213
+ const available = new Set(sourceFields);
214
+ for (const operation of transform.operations) {
215
+ if (
216
+ !POSITION_OPERATIONS.has(operation.op) &&
217
+ !available.has(operation.field)
218
+ ) {
219
+ throw new Error(`Window source does not contain field "${operation.field}".`);
220
+ }
221
+ if (available.has(operation.as)) {
222
+ throw new Error(`Window output field "${operation.as}" already exists.`);
223
+ }
224
+ available.add(operation.as);
225
+ }
226
+ }
227
+
228
+ function validatePartitionSortValues(partition, transform) {
229
+ for (const field of transform.partitionBy) {
230
+ partition.forEach(entry => scalarKey(
231
+ entry.row[field],
232
+ `Window partition field "${field}"`
233
+ ));
234
+ }
235
+ for (const sort of transform.sortBy) {
236
+ const types = new Set(partition.flatMap(({ row }) => {
237
+ const value = row[sort.field];
238
+ if (value === null || value === undefined) return [];
239
+ if (
240
+ !["number", "string", "boolean"].includes(typeof value) ||
241
+ (typeof value === "number" && !Number.isFinite(value))
242
+ ) {
243
+ throw new TypeError(
244
+ `Window sort field "${sort.field}" must contain comparable primitive values.`
245
+ );
246
+ }
247
+ return [typeof value];
248
+ }));
249
+ if (types.size > 1) {
250
+ throw new TypeError(
251
+ `Window sort field "${sort.field}" must contain one comparable primitive type.`
252
+ );
253
+ }
254
+ }
255
+ }
256
+
257
+ function partitionRows(entries, partitionBy) {
258
+ const partitions = new Map();
259
+ for (const entry of entries) {
260
+ const key = partitionBy.length === 0
261
+ ? "all"
262
+ : partitionBy.map(field => scalarKey(
263
+ entry.row[field],
264
+ `Window partition field "${field}"`
265
+ )).join("\0");
266
+ if (!partitions.has(key)) partitions.set(key, []);
267
+ partitions.get(key).push(entry);
268
+ }
269
+ return [...partitions.values()];
270
+ }
271
+
272
+ function applyOperation(partition, operation, sortBy) {
273
+ if (operation.op === "rowNumber") {
274
+ partition.forEach((entry, index) => {
275
+ entry.row[operation.as] = index + 1;
276
+ });
277
+ return;
278
+ }
279
+ if (["rank", "denseRank"].includes(operation.op)) {
280
+ let rank = 1;
281
+ let denseRank = 1;
282
+ partition.forEach((entry, index) => {
283
+ if (
284
+ index > 0 &&
285
+ compareEntries(entry, partition[index - 1], sortBy, false) !== 0
286
+ ) {
287
+ rank = index + 1;
288
+ denseRank += 1;
289
+ }
290
+ entry.row[operation.as] = operation.op === "rank" ? rank : denseRank;
291
+ });
292
+ return;
293
+ }
294
+ if (operation.op === "cumulativeSum") {
295
+ let total = 0;
296
+ for (const entry of partition) {
297
+ const value = entry.row[operation.field];
298
+ if (!Number.isFinite(value)) {
299
+ throw new TypeError(
300
+ `Window cumulativeSum field "${operation.field}" must contain finite numbers.`
301
+ );
302
+ }
303
+ total += value;
304
+ entry.row[operation.as] = total;
305
+ }
306
+ return;
307
+ }
308
+ const direction = operation.op === "lag" ? -1 : 1;
309
+ partition.forEach((entry, index) => {
310
+ const peer = partition[index + direction * operation.offset];
311
+ entry.row[operation.as] = peer === undefined
312
+ ? operation.default
313
+ : peer.row[operation.field];
314
+ });
315
+ }
316
+
317
+ export function deriveWindowRows(rows, transform) {
318
+ if (!Array.isArray(rows)) {
319
+ throw new TypeError("Window source rows must be an array.");
320
+ }
321
+ if (!rows.every(isPlainObject)) {
322
+ throw new TypeError("Window source rows must be plain objects.");
323
+ }
324
+ validateWindowTransform(transform);
325
+ validateSourceFields(rows, transform);
326
+ const entries = rows.map((row, index) => ({ index, row: { ...row } }));
327
+ const partitions = partitionRows(entries, transform.partitionBy);
328
+ for (const partition of partitions) {
329
+ validatePartitionSortValues(partition, transform);
330
+ partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
331
+ }
332
+ for (const operation of transform.operations) {
333
+ for (const partition of partitions) {
334
+ applyOperation(partition, operation, transform.sortBy);
335
+ }
336
+ }
337
+ entries.sort((left, right) => left.index - right.index);
338
+ return cloneAndFreeze(entries.map(entry => entry.row));
339
+ }