ggaction 0.0.18 → 0.0.20
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 +56 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/color/continuous.js +7 -1
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +32 -9
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/area/materialize.js +1 -190
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/consumers/common.js +7 -1
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/aggregate.js +3 -2
- package/src/grammar/areaSeries.js +1 -415
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/interval.js +22 -6
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pathSeries.js +2 -2
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +6 -3
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/statistics/confidenceInterval.js +1 -101
- package/src/grammar/summary.js +1 -320
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/grouping.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +220 -64
package/src/grammar/summary.js
CHANGED
|
@@ -1,320 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
3
|
-
import { requireStringValue as requireField } from "../core/validation.js";
|
|
4
|
-
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
5
|
-
import {
|
|
6
|
-
aggregateRows,
|
|
7
|
-
isScalarAggregate,
|
|
8
|
-
validateAggregate,
|
|
9
|
-
validateAggregateFieldValues
|
|
10
|
-
} from "./aggregate.js";
|
|
11
|
-
import {
|
|
12
|
-
calculateWeightedAggregate,
|
|
13
|
-
normalizeStatisticalWeight,
|
|
14
|
-
readStatisticalWeights,
|
|
15
|
-
summarizeStatisticalWeights,
|
|
16
|
-
validateWeightedAggregate,
|
|
17
|
-
validateWeightedNumericFields,
|
|
18
|
-
weightedRows
|
|
19
|
-
} from "./weightedStatistics.js";
|
|
20
|
-
|
|
21
|
-
const TRANSFORM_KEYS = Object.freeze([
|
|
22
|
-
"type", "groupBy", "aggregates", "members", "weight", "missing", "empty"
|
|
23
|
-
]);
|
|
24
|
-
const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
|
|
25
|
-
const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
|
|
26
|
-
const MAX_OUTPUT_ROWS = 10_000;
|
|
27
|
-
|
|
28
|
-
function normalizeGroupBy(value) {
|
|
29
|
-
if (value === undefined) return [];
|
|
30
|
-
return Array.isArray(value) ? [...value] : [value];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeAggregate(value) {
|
|
34
|
-
if (!isPlainObject(value)) return value;
|
|
35
|
-
return {
|
|
36
|
-
...value,
|
|
37
|
-
op: validateAggregate(value.op),
|
|
38
|
-
...(Object.hasOwn(value, "field") ? { field: value.field } : {}),
|
|
39
|
-
as: value.as
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function normalizeSummaryTransform({ groupBy, aggregates, members, weight, missing, empty } = {}) {
|
|
44
|
-
const transform = {
|
|
45
|
-
type: "summary",
|
|
46
|
-
groupBy: normalizeGroupBy(groupBy),
|
|
47
|
-
aggregates: Array.isArray(aggregates)
|
|
48
|
-
? aggregates.map(normalizeAggregate)
|
|
49
|
-
: aggregates,
|
|
50
|
-
...(members === undefined ? {} : { members }),
|
|
51
|
-
...(weight === undefined
|
|
52
|
-
? {}
|
|
53
|
-
: { weight: normalizeStatisticalWeight(weight, "Summary weight") }),
|
|
54
|
-
...(missing === undefined ? {} : { missing }),
|
|
55
|
-
...(empty === undefined ? {} : { empty })
|
|
56
|
-
};
|
|
57
|
-
validateSummaryTransform(transform);
|
|
58
|
-
return cloneAndFreeze(transform);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateSummaryTransform(transform) {
|
|
62
|
-
if (!isPlainObject(transform)) {
|
|
63
|
-
throw new TypeError("Summary transform must be a plain object.");
|
|
64
|
-
}
|
|
65
|
-
rejectUnknownKeys(transform, TRANSFORM_KEYS, "summary transform");
|
|
66
|
-
if (transform.type !== "summary") {
|
|
67
|
-
throw new Error(`Unsupported summary transform "${transform.type}".`);
|
|
68
|
-
}
|
|
69
|
-
if (
|
|
70
|
-
!Array.isArray(transform.groupBy) ||
|
|
71
|
-
transform.groupBy.some(field => typeof field !== "string" || field.length === 0)
|
|
72
|
-
) {
|
|
73
|
-
throw new TypeError("Summary groupBy must contain field names.");
|
|
74
|
-
}
|
|
75
|
-
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
76
|
-
throw new Error("Summary groupBy fields must be unique.");
|
|
77
|
-
}
|
|
78
|
-
if (!Array.isArray(transform.aggregates) || transform.aggregates.length === 0) {
|
|
79
|
-
throw new TypeError("Summary aggregates must be a non-empty array.");
|
|
80
|
-
}
|
|
81
|
-
if (transform.aggregates.length > 64) {
|
|
82
|
-
throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");
|
|
83
|
-
}
|
|
84
|
-
const outputs = new Set(transform.groupBy);
|
|
85
|
-
transform.aggregates.forEach((aggregate, index) => {
|
|
86
|
-
if (!isPlainObject(aggregate)) {
|
|
87
|
-
throw new TypeError(`Summary aggregate ${index} must be a plain object.`);
|
|
88
|
-
}
|
|
89
|
-
rejectUnknownKeys(aggregate, AGGREGATE_KEYS, `summary aggregate ${index}`);
|
|
90
|
-
const operation = validateAggregate(aggregate.op);
|
|
91
|
-
if (transform.weight !== undefined) validateWeightedAggregate(operation);
|
|
92
|
-
if (operation === "count") {
|
|
93
|
-
if (Object.hasOwn(aggregate, "field")) {
|
|
94
|
-
throw new Error("Summary count does not accept a field.");
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
requireField(aggregate.field, `Summary aggregate ${index} field`);
|
|
98
|
-
}
|
|
99
|
-
const output = requireField(aggregate.as, `Summary aggregate ${index} as`);
|
|
100
|
-
if (outputs.has(output)) {
|
|
101
|
-
throw new Error(`Summary output field "${output}" collides with another output.`);
|
|
102
|
-
}
|
|
103
|
-
outputs.add(output);
|
|
104
|
-
});
|
|
105
|
-
if (transform.members !== undefined) {
|
|
106
|
-
const members = requireField(transform.members, "Summary members");
|
|
107
|
-
if (outputs.has(members)) {
|
|
108
|
-
throw new Error(`Summary members field "${members}" collides with another output.`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (transform.weight !== undefined) {
|
|
112
|
-
normalizeStatisticalWeight(transform.weight, "Summary weight");
|
|
113
|
-
}
|
|
114
|
-
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
115
|
-
throw new Error('Summary missing must be "error" or "drop".');
|
|
116
|
-
}
|
|
117
|
-
if (transform.empty !== undefined && !["null", "identity"].includes(transform.empty)) {
|
|
118
|
-
throw new Error('Summary empty must be "null" or "identity".');
|
|
119
|
-
}
|
|
120
|
-
return transform;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function requireSourceFields(rows, transform) {
|
|
124
|
-
if (rows.length === 0) return;
|
|
125
|
-
const fields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
126
|
-
for (const field of transform.groupBy) {
|
|
127
|
-
if (!fields.has(field)) {
|
|
128
|
-
throw new Error(`Summary source does not contain group field "${field}".`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (const aggregate of transform.aggregates) {
|
|
132
|
-
if (aggregate.field !== undefined && !fields.has(aggregate.field)) {
|
|
133
|
-
throw new Error(`Summary source does not contain aggregate field "${aggregate.field}".`);
|
|
134
|
-
}
|
|
135
|
-
const orderBy = isPlainObject(aggregate.op) ? aggregate.op.orderBy : undefined;
|
|
136
|
-
if (orderBy !== undefined && !fields.has(orderBy)) {
|
|
137
|
-
throw new Error(`Summary source does not contain order field "${orderBy}".`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (transform.weight !== undefined && !fields.has(transform.weight.field)) {
|
|
141
|
-
throw new Error(`Summary source does not contain weight field "${transform.weight.field}".`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function validateAggregateValues(rows, aggregate) {
|
|
146
|
-
if (aggregate.op === "count") return;
|
|
147
|
-
if (isScalarAggregate(aggregate.op) && NOMINAL_OPERATIONS.has(aggregate.op)) {
|
|
148
|
-
validateAggregateFieldValues(rows, aggregate.field, "nominal");
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
validateAggregateFieldValues(rows, aggregate.field, "quantitative");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function deriveSummaryRows(rows, transform) {
|
|
155
|
-
validateSummaryTransform(transform);
|
|
156
|
-
requireSourceFields(rows, transform);
|
|
157
|
-
const explicitWeighted = transform.weight !== undefined && transform.missing !== undefined;
|
|
158
|
-
let weightEntries;
|
|
159
|
-
if (transform.weight === undefined) {
|
|
160
|
-
for (const aggregate of transform.aggregates) {
|
|
161
|
-
validateAggregateValues(rows, aggregate);
|
|
162
|
-
}
|
|
163
|
-
} else if (!explicitWeighted) {
|
|
164
|
-
weightEntries = readStatisticalWeights(rows, transform.weight, "Summary").entries;
|
|
165
|
-
validateWeightedNumericFields(
|
|
166
|
-
weightEntries,
|
|
167
|
-
[...new Set(transform.aggregates.flatMap(aggregate =>
|
|
168
|
-
aggregate.field === undefined ? [] : [aggregate.field]
|
|
169
|
-
))],
|
|
170
|
-
"Summary"
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
// Explicit missing policy is evaluated per measure. Validate every
|
|
174
|
-
// nonmissing value now, while leaving nullish value/weight pairs for the
|
|
175
|
-
// reason-priority accounting below.
|
|
176
|
-
readStatisticalWeights(
|
|
177
|
-
rows.filter(row => row[transform.weight.field] !== null && row[transform.weight.field] !== undefined),
|
|
178
|
-
transform.weight,
|
|
179
|
-
"Summary"
|
|
180
|
-
);
|
|
181
|
-
for (const aggregate of transform.aggregates) {
|
|
182
|
-
if (aggregate.field === undefined) continue;
|
|
183
|
-
validateAggregateValues(
|
|
184
|
-
rows.filter(row => row[aggregate.field] !== null && row[aggregate.field] !== undefined),
|
|
185
|
-
aggregate
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (transform.missing === "error") {
|
|
190
|
-
if (transform.weight !== undefined) {
|
|
191
|
-
const weightIndex = rows.findIndex(row =>
|
|
192
|
-
row[transform.weight.field] === null || row[transform.weight.field] === undefined
|
|
193
|
-
);
|
|
194
|
-
if (weightIndex !== -1) {
|
|
195
|
-
throw new TypeError(`Summary weight "${transform.weight.field}" is missing at row ${weightIndex}.`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
for (const aggregate of transform.aggregates) {
|
|
199
|
-
if (aggregate.field === undefined) continue;
|
|
200
|
-
const index = rows.findIndex(row => row[aggregate.field] === null || row[aggregate.field] === undefined);
|
|
201
|
-
if (index !== -1) throw new TypeError(`Summary field "${aggregate.field}" is missing at row ${index}.`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const groups = new Map();
|
|
206
|
-
if (transform.groupBy.length === 0) {
|
|
207
|
-
groups.set("all", {
|
|
208
|
-
values: {},
|
|
209
|
-
rows,
|
|
210
|
-
...(weightEntries === undefined ? {} : { entries: weightEntries })
|
|
211
|
-
});
|
|
212
|
-
} else {
|
|
213
|
-
rows.forEach((row, index) => {
|
|
214
|
-
const values = Object.fromEntries(transform.groupBy.map(field => [field, row[field]]));
|
|
215
|
-
const key = transform.groupBy.map(field =>
|
|
216
|
-
scalarKey(row[field], `Summary group field "${field}"`)
|
|
217
|
-
).join("|");
|
|
218
|
-
const group = groups.get(key) ?? {
|
|
219
|
-
values,
|
|
220
|
-
rows: [],
|
|
221
|
-
...(weightEntries === undefined ? {} : { entries: [] })
|
|
222
|
-
};
|
|
223
|
-
group.rows.push(row);
|
|
224
|
-
if (weightEntries !== undefined) group.entries.push(weightEntries[index]);
|
|
225
|
-
groups.set(key, group);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
if (groups.size > MAX_OUTPUT_ROWS) {
|
|
229
|
-
throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const units = [];
|
|
233
|
-
const values = [...groups.values()].map(group => {
|
|
234
|
-
const sharedWeightSummary = transform.weight === undefined || explicitWeighted
|
|
235
|
-
? undefined
|
|
236
|
-
: summarizeStatisticalWeights(
|
|
237
|
-
group.entries,
|
|
238
|
-
transform.weight.kind,
|
|
239
|
-
"Summary group"
|
|
240
|
-
);
|
|
241
|
-
const aggregated = Object.fromEntries(transform.aggregates.map(aggregate => {
|
|
242
|
-
let missingValue = 0;
|
|
243
|
-
let missingWeight = 0;
|
|
244
|
-
let zeroWeightRows = 0;
|
|
245
|
-
let measureWeightSummary = sharedWeightSummary;
|
|
246
|
-
if (explicitWeighted) {
|
|
247
|
-
const eligible = group.rows.filter(row => {
|
|
248
|
-
if (aggregate.field !== undefined &&
|
|
249
|
-
(row[aggregate.field] === null || row[aggregate.field] === undefined)) {
|
|
250
|
-
missingValue += 1;
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
if (row[transform.weight.field] === null || row[transform.weight.field] === undefined) {
|
|
254
|
-
missingWeight += 1;
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return true;
|
|
258
|
-
});
|
|
259
|
-
const entries = readStatisticalWeights(eligible, transform.weight, "Summary").entries;
|
|
260
|
-
zeroWeightRows = entries.filter(entry => entry.weight === 0).length;
|
|
261
|
-
measureWeightSummary = summarizeStatisticalWeights(
|
|
262
|
-
entries,
|
|
263
|
-
transform.weight.kind,
|
|
264
|
-
"Summary group"
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
let value = measureWeightSummary === undefined
|
|
268
|
-
? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
|
|
269
|
-
: calculateWeightedAggregate(
|
|
270
|
-
measureWeightSummary,
|
|
271
|
-
aggregate.field,
|
|
272
|
-
aggregate.op,
|
|
273
|
-
`Summary aggregate "${aggregate.as}"`
|
|
274
|
-
);
|
|
275
|
-
if (value === undefined && transform.empty !== undefined) {
|
|
276
|
-
const op = typeof aggregate.op === "string" ? aggregate.op : aggregate.op.op;
|
|
277
|
-
value = transform.empty === "identity" && op === "sum" ? 0 : null;
|
|
278
|
-
}
|
|
279
|
-
if (transform.missing !== undefined || transform.empty !== undefined) {
|
|
280
|
-
const missing = explicitWeighted
|
|
281
|
-
? missingValue + missingWeight
|
|
282
|
-
: aggregate.field === undefined ? 0 : group.rows.filter(row => row[aggregate.field] === null || row[aggregate.field] === undefined).length;
|
|
283
|
-
units.push({
|
|
284
|
-
role: aggregate.as,
|
|
285
|
-
group: group.values,
|
|
286
|
-
inputRows: group.rows.length,
|
|
287
|
-
usedRows: group.rows.length - missing,
|
|
288
|
-
excludedRows: missing,
|
|
289
|
-
excludedByReason: missing === 0 ? {} : {
|
|
290
|
-
...(missingValue === 0 && explicitWeighted ? {} : { "missing-value": explicitWeighted ? missingValue : missing }),
|
|
291
|
-
...(missingWeight === 0 ? {} : { "missing-weight": missingWeight })
|
|
292
|
-
},
|
|
293
|
-
...(explicitWeighted ? { zeroWeightRows } : {})
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
return [aggregate.as, value];
|
|
297
|
-
}));
|
|
298
|
-
return {
|
|
299
|
-
...group.values,
|
|
300
|
-
...aggregated,
|
|
301
|
-
...(transform.members === undefined
|
|
302
|
-
? {}
|
|
303
|
-
: {
|
|
304
|
-
[transform.members]: sharedWeightSummary === undefined
|
|
305
|
-
? group.rows
|
|
306
|
-
: weightedRows(sharedWeightSummary)
|
|
307
|
-
})
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
if (transform.missing === undefined && transform.empty === undefined) return values;
|
|
311
|
-
return {
|
|
312
|
-
values,
|
|
313
|
-
report: cloneAndFreeze({
|
|
314
|
-
version: 1,
|
|
315
|
-
owner: { kind: "data", id: "pending" },
|
|
316
|
-
inputs: [],
|
|
317
|
-
units
|
|
318
|
-
})
|
|
319
|
-
};
|
|
320
|
-
}
|
|
1
|
+
import{createBootstrapMeanSampler as q,bootstrapMeanBounds as I,validateBootstrapDrawCount as U}from"./statistics/confidenceInterval.js";import{scalarKey as V}from"./transformKeys.js";import{rejectUnknownProperties as W}from"../core/validation.js";import{requireStringValue as E}from"../core/validation.js";import{cloneAndFreeze as v,isPlainObject as g}from"../core/immutable.js";import{aggregateRows as G,isScalarAggregate as K,validateAggregate as B,validateAggregateFieldValues as z}from"./aggregate.js";import{calculateWeightedAggregate as P,normalizeStatisticalWeight as M,readStatisticalWeights as A,summarizeStatisticalWeights as k,validateWeightedAggregate as J,validateWeightedNumericFields as L,weightedRows as Y}from"./weightedStatistics.js";const C=Object.freeze(["type","groupBy","aggregates","members","weight","missing","empty"]),D=Object.freeze(["op","field","as"]),X=new Set(["distinct","valid","missing"]),N=3e4;function H(e){return e===void 0?[]:Array.isArray(e)?[...e]:[e]}function Q(e){return g(e)?{...e,op:B(e.op,{allowBootstrap:!0}),...Object.hasOwn(e,"field")?{field:e.field}:{},as:e.as}:e}function ue({groupBy:e,aggregates:i,members:n,weight:o,missing:u,empty:l}={}){const m={type:"summary",groupBy:H(e),aggregates:Array.isArray(i)?i.map(Q):i,...n===void 0?{}:{members:n},...o===void 0?{}:{weight:M(o,"Summary weight")},...u===void 0?{}:{missing:u},...l===void 0?{}:{empty:l}};return x(m),v(m)}function x(e){if(!g(e))throw new TypeError("Summary transform must be a plain object.");if(W(e,C,"summary transform"),e.type!=="summary")throw new Error(`Unsupported summary transform "${e.type}".`);if(!Array.isArray(e.groupBy)||e.groupBy.some(n=>typeof n!="string"||n.length===0))throw new TypeError("Summary groupBy must contain field names.");if(new Set(e.groupBy).size!==e.groupBy.length)throw new Error("Summary groupBy fields must be unique.");if(!Array.isArray(e.aggregates)||e.aggregates.length===0)throw new TypeError("Summary aggregates must be a non-empty array.");if(e.aggregates.length>64)throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");const i=new Set(e.groupBy);if(e.aggregates.forEach((n,o)=>{if(!g(n))throw new TypeError(`Summary aggregate ${o} must be a plain object.`);W(n,D,`summary aggregate ${o}`);const u=B(n.op,{allowBootstrap:!0});if(e.weight!==void 0&&J(u),u==="count"){if(Object.hasOwn(n,"field"))throw new Error("Summary count does not accept a field.")}else E(n.field,`Summary aggregate ${o} field`);const l=E(n.as,`Summary aggregate ${o} as`);if(i.has(l))throw new Error(`Summary output field "${l}" collides with another output.`);i.add(l)}),e.members!==void 0){const n=E(e.members,"Summary members");if(i.has(n))throw new Error(`Summary members field "${n}" collides with another output.`)}if(e.weight!==void 0&&M(e.weight,"Summary weight"),e.missing!==void 0&&!["error","drop"].includes(e.missing))throw new Error('Summary missing must be "error" or "drop".');if(e.empty!==void 0&&!["null","identity"].includes(e.empty))throw new Error('Summary empty must be "null" or "identity".');return e}function Z(e,i){if(e.length===0)return;const n=new Set(e.flatMap(o=>Object.keys(o)));for(const o of i.groupBy)if(!n.has(o))throw new Error(`Summary source does not contain group field "${o}".`);for(const o of i.aggregates){if(o.field!==void 0&&!n.has(o.field))throw new Error(`Summary source does not contain aggregate field "${o.field}".`);const u=g(o.op)?o.op.orderBy:void 0;if(u!==void 0&&!n.has(u))throw new Error(`Summary source does not contain order field "${u}".`)}if(i.weight!==void 0&&!n.has(i.weight.field))throw new Error(`Summary source does not contain weight field "${i.weight.field}".`)}function F(e,i){if(i.op!=="count"){if(K(i.op)&&X.has(i.op)){z(e,i.field,"nominal");return}z(e,i.field,"quantitative")}}function se(e,i){x(i),Z(e,i);const n=i.aggregates.map(t=>({...t,op:B(t.op,{allowBootstrap:!0})})),o=i.weight!==void 0&&i.missing!==void 0;let u;if(i.weight===void 0)for(const t of n)F(e,t);else if(!o)u=A(e,i.weight,"Summary").entries,L(u,[...new Set(n.flatMap(t=>t.field===void 0?[]:[t.field]))],"Summary");else{A(e.filter(t=>t[i.weight.field]!==null&&t[i.weight.field]!==void 0),i.weight,"Summary");for(const t of n)t.field!==void 0&&F(e.filter(d=>d[t.field]!==null&&d[t.field]!==void 0),t)}if(i.missing==="error"){if(i.weight!==void 0){const t=e.findIndex(d=>d[i.weight.field]===null||d[i.weight.field]===void 0);if(t!==-1)throw new TypeError(`Summary weight "${i.weight.field}" is missing at row ${t}.`)}for(const t of n){if(t.field===void 0)continue;const d=e.findIndex(f=>f[t.field]===null||f[t.field]===void 0);if(d!==-1)throw new TypeError(`Summary field "${t.field}" is missing at row ${d}.`)}}const l=new Map;if(i.groupBy.length===0?l.set("all",{values:{},rows:e,...u===void 0?{}:{entries:u}}):e.forEach((t,d)=>{const f=Object.fromEntries(i.groupBy.map(a=>[a,t[a]])),c=i.groupBy.map(a=>V(t[a],`Summary group field "${a}"`)).join("|"),r=l.get(c)??{values:f,rows:[],...u===void 0?{}:{entries:[]}};r.rows.push(t),u!==void 0&&r.entries.push(u[d]),l.set(c,r)}),l.size>N)throw new RangeError(`Summary output cannot exceed ${N} groups.`);const m=new Map;let O=0;for(const t of n){const d=t.op?.method;if(d?.type!=="bootstrap")continue;const f=JSON.stringify([t.field,d.samples,d.seed]);m.has(f)||(O+=d.samples*e.filter(c=>Number.isFinite(c[t.field])).length,U(O),m.set(f,q(d)))}const $=[],R=[...l.values()].map(t=>{const d=new Map,f=i.weight===void 0||o?void 0:k(t.entries,i.weight.kind,"Summary group"),c=Object.fromEntries(n.map(r=>{let a=0,h=0,j=0,S=f;if(o){const s=t.rows.filter(p=>r.field!==void 0&&(p[r.field]===null||p[r.field]===void 0)?(a+=1,!1):p[i.weight.field]===null||p[i.weight.field]===void 0?(h+=1,!1):!0),y=A(s,i.weight,"Summary").entries;j=y.filter(p=>p.weight===0).length,S=k(y,i.weight.kind,"Summary group")}const w=r.op?.method;let T;if(w?.type==="bootstrap"){const s=JSON.stringify([r.field,w.samples,w.seed]);if(!d.has(s)){const p=t.rows.map(_=>_[r.field]).filter(Number.isFinite);d.set(s,m.get(s)(p))}T=I(d.get(s),r.op.level)?.[r.op.op==="ciLower"?"lower":"upper"]}let b=w?.type==="bootstrap"?T:S===void 0?G(t.rows,r.field??"__row",r.op):P(S,r.field,r.op,`Summary aggregate "${r.as}"`);if(b===void 0&&i.empty!==void 0){const s=typeof r.op=="string"?r.op:r.op.op;b=i.empty==="identity"&&s==="sum"?0:null}if(i.missing!==void 0||i.empty!==void 0){const s=o?a+h:r.field===void 0?0:t.rows.filter(y=>y[r.field]===null||y[r.field]===void 0).length;$.push({role:r.as,group:t.values,inputRows:t.rows.length,usedRows:t.rows.length-s,excludedRows:s,excludedByReason:s===0?{}:{...a===0&&o?{}:{"missing-value":o?a:s},...h===0?{}:{"missing-weight":h}},...o?{zeroWeightRows:j}:{}})}return[r.as,b]}));return{...t.values,...c,...i.members===void 0?{}:{[i.members]:f===void 0?t.rows:Y(f)}}});return i.missing===void 0&&i.empty===void 0?R:{values:R,report:v({version:1,owner:{kind:"data",id:"pending"},inputs:[],units:$})}}export{se as deriveSummaryRows,ue as normalizeSummaryTransform,x as validateSummaryTransform};
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -52,6 +52,7 @@ function formatter(timeZone) {
|
|
|
52
52
|
try {
|
|
53
53
|
value = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
54
54
|
timeZone,
|
|
55
|
+
era: "short",
|
|
55
56
|
year: "numeric",
|
|
56
57
|
month: "2-digit",
|
|
57
58
|
day: "2-digit",
|
|
@@ -68,7 +69,7 @@ function formatter(timeZone) {
|
|
|
68
69
|
return CALENDAR_FORMATTERS.get(timeZone);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function validateTimeZone(timeZone) {
|
|
72
|
+
export function validateTimeZone(timeZone) {
|
|
72
73
|
if (typeof timeZone !== "string" || timeZone.length === 0) {
|
|
73
74
|
throw new TypeError("Time-unit timeZone must be a non-empty IANA time-zone name.");
|
|
74
75
|
}
|
|
@@ -178,17 +179,18 @@ export function calendarParts(timestamp, timeZone) {
|
|
|
178
179
|
validateTimeZone(timeZone);
|
|
179
180
|
const parts = Object.fromEntries(formatter(timeZone).formatToParts(timestamp)
|
|
180
181
|
.filter(part => part.type !== "literal")
|
|
181
|
-
.map(part => [part.type, Number(part.value)]));
|
|
182
|
+
.map(part => [part.type, part.type === "era" ? part.value : Number(part.value)]));
|
|
183
|
+
const year = parts.era === "BC" ? 1 - parts.year : parts.year;
|
|
182
184
|
const millisecond = ((timestamp % 1000) + 1000) % 1000;
|
|
183
185
|
return {
|
|
184
|
-
year
|
|
186
|
+
year,
|
|
185
187
|
month: parts.month,
|
|
186
188
|
day: parts.day,
|
|
187
189
|
hour: parts.hour,
|
|
188
190
|
minute: parts.minute,
|
|
189
191
|
second: parts.second,
|
|
190
192
|
millisecond,
|
|
191
|
-
weekday: weekdayFromDate(
|
|
193
|
+
weekday: weekdayFromDate(year, parts.month, parts.day)
|
|
192
194
|
};
|
|
193
195
|
}
|
|
194
196
|
|
|
@@ -16,6 +16,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
|
|
|
16
16
|
markFilter: Object.freeze({ provenanceTransparent: true }),
|
|
17
17
|
normalize: Object.freeze({ facetTopology: "statistical" }),
|
|
18
18
|
regression: Object.freeze({ facetTopology: "statistical" }),
|
|
19
|
+
regressionStatistics: Object.freeze({ facetTopology: "statistical" }),
|
|
19
20
|
sort: Object.freeze({ facetTopology: "statistical" }),
|
|
20
21
|
summary: Object.freeze({ facetTopology: "statistical" }),
|
|
21
22
|
stack: Object.freeze({ facetTopology: "statistical" }),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateRegressionStatisticsTransform } from "./regression/statistics.js";
|
|
2
3
|
import { validateBoxTransform } from "./boxPlot.js";
|
|
3
4
|
import { normalizeBinTransform, validateBinTransform } from "./bin.js";
|
|
4
5
|
import {
|
|
@@ -203,7 +204,7 @@ function normalizeRegressionEdit(transform, patch) {
|
|
|
203
204
|
delete base.span;
|
|
204
205
|
delete base.robustIterations;
|
|
205
206
|
}
|
|
206
|
-
if (
|
|
207
|
+
if (["loess", "exp"].includes(method) || ["loess", "exp"].includes(base.method)) {
|
|
207
208
|
for (const key of [
|
|
208
209
|
"confidenceMethod", "level", "confidence", "interval"
|
|
209
210
|
]) delete base[key];
|
|
@@ -446,6 +447,15 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
446
447
|
normalizeEdit: normalizeRegressionEdit,
|
|
447
448
|
outputRoles: () => ({})
|
|
448
449
|
}),
|
|
450
|
+
regressionStatistics: Object.freeze({
|
|
451
|
+
...findTransformTopology("regressionStatistics"),
|
|
452
|
+
validate: validateRegressionStatisticsTransform,
|
|
453
|
+
materializeOp: "materializeRegressionStatisticsData",
|
|
454
|
+
replayTransform: requestedTransform,
|
|
455
|
+
editable: true,
|
|
456
|
+
normalizeEdit: (transform, patch) => validateRegressionStatisticsTransform({ ...transform, ...patch }),
|
|
457
|
+
outputRoles: () => ({ count: "count", intercept: "intercept", slope: "slope", rSquared: "rSquared" })
|
|
458
|
+
}),
|
|
449
459
|
sort: Object.freeze({
|
|
450
460
|
...findTransformTopology("sort"),
|
|
451
461
|
validate: validateSortTransform,
|