ggaction 0.0.16 → 0.0.17
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 +2 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +8 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +19 -6
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +4 -1
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +30 -2
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +6 -2
- package/src/actions/encodings/color/continuous.js +4 -1
- package/src/actions/encodings/color/index.js +4 -1
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/resolve.js +6 -1
- package/src/actions/encodings/ranged.js +6 -1
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +6 -1
- package/src/actions/marks/bar/create.js +8 -1
- package/src/actions/marks/bar/edit.js +12 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +12 -5
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +23 -8
- package/src/actions/marks/rule/actions.js +24 -8
- package/src/actions/marks/text/actions.js +25 -6
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +22 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +25 -4
- package/src/actions/regression/edit.js +47 -12
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/scales/channels.js +11 -11
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/create.js +2 -1
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +28 -1
- package/src/actions/theme/reconcile.js +2 -0
- package/src/core/action.js +3 -2
- package/src/core/programState.js +5 -1
- package/src/grammar/bin.js +39 -10
- package/src/grammar/datasetSchema.js +386 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/interval.js +32 -1
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/regression/derive.js +57 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +51 -22
- package/src/grammar/regression/parameters.js +56 -4
- package/src/grammar/scales/definition.js +7 -0
- package/src/grammar/schemas/semanticPath.js +3 -2
- package/src/grammar/sort.js +88 -0
- package/src/grammar/summary.js +117 -19
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +21 -11
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/materialization/bars/resolve.js +4 -2
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scales/resolve.js +67 -4
- package/src/materialization/text.js +4 -2
- package/src/persistence.js +79 -6
- package/src/version.js +1 -1
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +147 -27
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
+
|
|
4
|
+
const STORAGE_TYPES = new Set([
|
|
5
|
+
"number", "string", "boolean", "array", "object", "unknown", "mixed"
|
|
6
|
+
]);
|
|
7
|
+
const DECLARED_STORAGE_TYPES = new Set([
|
|
8
|
+
"number", "string", "boolean", "array", "object"
|
|
9
|
+
]);
|
|
10
|
+
const FIELD_KEYS = Object.freeze([
|
|
11
|
+
"name", "storageType", "nullable", "optional", "lineage"
|
|
12
|
+
]);
|
|
13
|
+
const SOURCE_FIELD_KEYS = Object.freeze([
|
|
14
|
+
"name", "storageType", "nullable", "optional"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function rejectUnknownKeys(value, keys, label) {
|
|
18
|
+
const unknown = Object.keys(value).find(key => !keys.includes(key));
|
|
19
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireName(value, label) {
|
|
23
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
24
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function valueStorageType(value) {
|
|
30
|
+
if (typeof value === "number") return "number";
|
|
31
|
+
if (typeof value === "string") return "string";
|
|
32
|
+
if (typeof value === "boolean") return "boolean";
|
|
33
|
+
if (Array.isArray(value)) return "array";
|
|
34
|
+
if (isPlainObject(value)) return "object";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeLineage(value, label) {
|
|
39
|
+
if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object.`);
|
|
40
|
+
rejectUnknownKeys(value, ["inputs", "owner", "role"], label);
|
|
41
|
+
if (!Array.isArray(value.inputs)) throw new TypeError(`${label} inputs must be an array.`);
|
|
42
|
+
const inputs = value.inputs.map((input, index) => {
|
|
43
|
+
if (!isPlainObject(input)) throw new TypeError(`${label} input ${index} must be a plain object.`);
|
|
44
|
+
rejectUnknownKeys(input, ["data", "field"], `${label} input ${index}`);
|
|
45
|
+
return {
|
|
46
|
+
data: requireName(input.data, `${label} input ${index} data`),
|
|
47
|
+
field: requireName(input.field, `${label} input ${index} field`)
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
inputs,
|
|
52
|
+
owner: requireName(value.owner, `${label} owner`),
|
|
53
|
+
role: requireName(value.role, `${label} role`)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeField(value, index, { declared = false } = {}) {
|
|
58
|
+
if (!isPlainObject(value)) throw new TypeError(`Dataset schema field ${index} must be a plain object.`);
|
|
59
|
+
rejectUnknownKeys(
|
|
60
|
+
value,
|
|
61
|
+
declared ? SOURCE_FIELD_KEYS : FIELD_KEYS,
|
|
62
|
+
`dataset schema field ${index}`
|
|
63
|
+
);
|
|
64
|
+
const name = requireName(value.name, `Dataset schema field ${index} name`);
|
|
65
|
+
const allowed = declared ? DECLARED_STORAGE_TYPES : STORAGE_TYPES;
|
|
66
|
+
if (!allowed.has(value.storageType)) {
|
|
67
|
+
throw new Error(`Unsupported dataset schema storageType "${value.storageType}".`);
|
|
68
|
+
}
|
|
69
|
+
for (const key of ["nullable", "optional"]) {
|
|
70
|
+
if (value[key] !== undefined && typeof value[key] !== "boolean") {
|
|
71
|
+
throw new TypeError(`Dataset schema field ${index} ${key} must be a boolean.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
storageType: value.storageType,
|
|
77
|
+
nullable: value.nullable ?? false,
|
|
78
|
+
optional: value.optional ?? false,
|
|
79
|
+
...(value.lineage === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { lineage: normalizeLineage(value.lineage, `Dataset schema field ${index} lineage`) })
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeSourceSchema(value) {
|
|
86
|
+
if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
|
|
87
|
+
rejectUnknownKeys(value, ["fields"], "dataset schema");
|
|
88
|
+
if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
|
|
89
|
+
const fields = value.fields.map((field, index) => normalizeField(field, index, { declared: true }));
|
|
90
|
+
if (new Set(fields.map(field => field.name)).size !== fields.length) {
|
|
91
|
+
throw new Error("Dataset schema field names must be unique.");
|
|
92
|
+
}
|
|
93
|
+
return cloneAndFreeze({
|
|
94
|
+
version: 1,
|
|
95
|
+
completeness: "known",
|
|
96
|
+
origin: "declared",
|
|
97
|
+
fields
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function validateDatasetSchema(value) {
|
|
102
|
+
if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
|
|
103
|
+
rejectUnknownKeys(value, ["version", "completeness", "origin", "fields"], "dataset schema");
|
|
104
|
+
if (value.version !== 1) throw new Error(`Unsupported dataset schema version "${value.version}".`);
|
|
105
|
+
if (!["known", "unknown"].includes(value.completeness)) {
|
|
106
|
+
throw new Error(`Unsupported dataset schema completeness "${value.completeness}".`);
|
|
107
|
+
}
|
|
108
|
+
if (!["declared", "inferred", "derived"].includes(value.origin)) {
|
|
109
|
+
throw new Error(`Unsupported dataset schema origin "${value.origin}".`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
|
|
112
|
+
const fields = value.fields.map((field, index) => normalizeField(field, index));
|
|
113
|
+
if (new Set(fields.map(field => field.name)).size !== fields.length) {
|
|
114
|
+
throw new Error("Dataset schema field names must be unique.");
|
|
115
|
+
}
|
|
116
|
+
if (value.completeness === "unknown" && fields.length > 0) {
|
|
117
|
+
throw new Error("Unknown dataset schema cannot contain known fields.");
|
|
118
|
+
}
|
|
119
|
+
return cloneAndFreeze({
|
|
120
|
+
version: 1,
|
|
121
|
+
completeness: value.completeness,
|
|
122
|
+
origin: value.origin,
|
|
123
|
+
fields
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function inferDatasetSchema(values, { origin = "inferred" } = {}) {
|
|
128
|
+
if (!Array.isArray(values)) throw new TypeError("Dataset schema inference requires row values.");
|
|
129
|
+
if (values.length === 0) {
|
|
130
|
+
return cloneAndFreeze({ version: 1, completeness: "unknown", origin, fields: [] });
|
|
131
|
+
}
|
|
132
|
+
const records = new Map();
|
|
133
|
+
for (const row of values) {
|
|
134
|
+
for (const name of Object.keys(row)) {
|
|
135
|
+
if (!records.has(name)) records.set(name, { types: new Set(), nullable: false, present: 0 });
|
|
136
|
+
const record = records.get(name);
|
|
137
|
+
const value = row[name];
|
|
138
|
+
if (value !== undefined) record.present += 1;
|
|
139
|
+
if (value === null) record.nullable = true;
|
|
140
|
+
else if (value !== undefined) record.types.add(valueStorageType(value));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const fields = [...records].map(([name, record]) => ({
|
|
144
|
+
name,
|
|
145
|
+
storageType: record.types.size === 0
|
|
146
|
+
? "unknown"
|
|
147
|
+
: record.types.size === 1 ? [...record.types][0] : "mixed",
|
|
148
|
+
nullable: record.nullable,
|
|
149
|
+
optional: record.present < values.length
|
|
150
|
+
}));
|
|
151
|
+
return cloneAndFreeze({ version: 1, completeness: "known", origin, fields });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function validateRowsAgainstSchema(values, schema, label = "Dataset") {
|
|
155
|
+
const normalized = validateDatasetSchema(schema);
|
|
156
|
+
if (normalized.completeness !== "known") return normalized;
|
|
157
|
+
const fields = new Map(normalized.fields.map(field => [field.name, field]));
|
|
158
|
+
for (let rowIndex = 0; rowIndex < values.length; rowIndex += 1) {
|
|
159
|
+
const row = values[rowIndex];
|
|
160
|
+
if (normalized.origin === "declared") {
|
|
161
|
+
const unknown = Object.keys(row).find(name => !fields.has(name));
|
|
162
|
+
if (unknown !== undefined) throw new Error(`${label} row ${rowIndex} contains undeclared field "${unknown}".`);
|
|
163
|
+
}
|
|
164
|
+
for (const field of normalized.fields) {
|
|
165
|
+
const present = Object.hasOwn(row, field.name) && row[field.name] !== undefined;
|
|
166
|
+
if (!present) {
|
|
167
|
+
if (!field.optional) throw new Error(`${label} row ${rowIndex} requires field "${field.name}".`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const value = row[field.name];
|
|
171
|
+
if (value === null) {
|
|
172
|
+
if (!field.nullable) throw new TypeError(`${label} field "${field.name}" cannot be null at row ${rowIndex}.`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!["unknown", "mixed"].includes(field.storageType) && valueStorageType(value) !== field.storageType) {
|
|
176
|
+
throw new TypeError(`${label} field "${field.name}" must contain ${field.storageType} values; invalid row ${rowIndex}.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return normalized;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function schemaField(schema, name) {
|
|
184
|
+
return validateDatasetSchema(schema).fields.find(field => field.name === name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function assertFieldsAvailable(schema, fields, { data, operation } = {}) {
|
|
188
|
+
const normalized = validateDatasetSchema(schema);
|
|
189
|
+
const unique = [...new Set(fields.filter(field => typeof field === "string" && field.length > 0))];
|
|
190
|
+
for (const field of unique) {
|
|
191
|
+
if (normalized.completeness !== "known" || schemaField(normalized, field) === undefined) {
|
|
192
|
+
throw annotateError(new Error(
|
|
193
|
+
`Dataset "${data ?? "unknown"}" does not contain field "${field}".`
|
|
194
|
+
), {
|
|
195
|
+
code: "missing-resource",
|
|
196
|
+
operation,
|
|
197
|
+
optionPath: "field",
|
|
198
|
+
resourceId: data
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function expressionFields(expression, output = []) {
|
|
206
|
+
if (!isPlainObject(expression)) return output;
|
|
207
|
+
if (typeof expression.field === "string") output.push(expression.field);
|
|
208
|
+
for (const value of Object.values(expression)) {
|
|
209
|
+
if (Array.isArray(value)) value.forEach(item => expressionFields(item, output));
|
|
210
|
+
else if (isPlainObject(value)) expressionFields(value, output);
|
|
211
|
+
}
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function transformInputFields(transform) {
|
|
216
|
+
const result = [];
|
|
217
|
+
const add = value => {
|
|
218
|
+
if (typeof value === "string" && value.length > 0) result.push(value);
|
|
219
|
+
else if (Array.isArray(value)) value.forEach(add);
|
|
220
|
+
};
|
|
221
|
+
add(transform.field);
|
|
222
|
+
add(transform.x);
|
|
223
|
+
add(transform.x?.field);
|
|
224
|
+
add(transform.y);
|
|
225
|
+
add(transform.y?.field);
|
|
226
|
+
add(transform.category);
|
|
227
|
+
add(transform.group);
|
|
228
|
+
add(transform.value);
|
|
229
|
+
add(transform.key);
|
|
230
|
+
add(transform.groupBy);
|
|
231
|
+
add(transform.partitionBy);
|
|
232
|
+
add(transform.fields);
|
|
233
|
+
add(transform.keys);
|
|
234
|
+
add(transform.weight?.field ?? transform.weight);
|
|
235
|
+
add(transform.placement?.categoryField);
|
|
236
|
+
add(transform.placement?.split?.field);
|
|
237
|
+
add(transform.sortBy?.map(item => item.field));
|
|
238
|
+
if (transform.type === "window") {
|
|
239
|
+
const generated = new Set();
|
|
240
|
+
for (const item of transform.operations ?? []) {
|
|
241
|
+
if (typeof item.field === "string" && !generated.has(item.field)) add(item.field);
|
|
242
|
+
add(item.orderBy);
|
|
243
|
+
if (typeof item.as === "string") generated.add(item.as);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
add(transform.operations?.flatMap(item => [item.field, item.orderBy]));
|
|
247
|
+
}
|
|
248
|
+
add(transform.aggregates?.flatMap(item => [item.field, item.op?.orderBy]));
|
|
249
|
+
add(expressionFields(transform.expression));
|
|
250
|
+
return [...new Set(result)];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function inheritedField(schema, name) {
|
|
254
|
+
const field = schema.fields.find(item => item.name === name);
|
|
255
|
+
return field === undefined ? undefined : { ...field };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function generatedField(name, storageType, transform, role, inputs = []) {
|
|
259
|
+
return {
|
|
260
|
+
name,
|
|
261
|
+
storageType,
|
|
262
|
+
nullable: !["count", "valid", "missing", "rowNumber", "rank", "denseRank"].includes(role),
|
|
263
|
+
optional: false,
|
|
264
|
+
lineage: {
|
|
265
|
+
inputs: inputs
|
|
266
|
+
.filter(field => typeof field === "string" && field.length > 0)
|
|
267
|
+
.map(field => ({ data: transform.__sourceId ?? "source", field })),
|
|
268
|
+
owner: transform.__ownerId ?? transform.type,
|
|
269
|
+
role
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
|
|
275
|
+
const input = validateDatasetSchema(inputSchema);
|
|
276
|
+
const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
|
|
277
|
+
const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
|
|
278
|
+
let fields = retain.includes(transform.type) ? input.fields.map(field => ({ ...field })) : [];
|
|
279
|
+
const push = field => {
|
|
280
|
+
if (field === undefined) return;
|
|
281
|
+
fields = fields.filter(item => item.name !== field.name);
|
|
282
|
+
fields.push(field);
|
|
283
|
+
};
|
|
284
|
+
if (transform.type === "summary") {
|
|
285
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
286
|
+
for (const item of transform.aggregates) {
|
|
287
|
+
const op = typeof item.op === "string" ? item.op : item.op.op;
|
|
288
|
+
push(generatedField(item.as, "number", definition, op, item.field ? [item.field] : []));
|
|
289
|
+
}
|
|
290
|
+
if (transform.members !== undefined) push(generatedField(transform.members, "array", definition, "members"));
|
|
291
|
+
} else if (transform.type === "bin") {
|
|
292
|
+
fields = [
|
|
293
|
+
generatedField(transform.as.lower, "number", definition, "lower", [transform.field]),
|
|
294
|
+
generatedField(transform.as.upper, "number", definition, "upper", [transform.field]),
|
|
295
|
+
generatedField(transform.as.count, "number", definition, "count", [transform.field])
|
|
296
|
+
];
|
|
297
|
+
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
298
|
+
} else if (transform.type === "bin2d") {
|
|
299
|
+
fields = [
|
|
300
|
+
generatedField(transform.as.x0, "number", definition, "x0", [transform.x]),
|
|
301
|
+
generatedField(transform.as.x1, "number", definition, "x1", [transform.x]),
|
|
302
|
+
generatedField(transform.as.y0, "number", definition, "y0", [transform.y]),
|
|
303
|
+
generatedField(transform.as.y1, "number", definition, "y1", [transform.y]),
|
|
304
|
+
generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
|
|
305
|
+
];
|
|
306
|
+
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
307
|
+
} else if (transform.type === "regression") {
|
|
308
|
+
fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
|
|
309
|
+
if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
|
|
310
|
+
if (transform.interval !== false && transform.method !== "loess") {
|
|
311
|
+
push(generatedField("__regression_ci_lower", "number", definition, "lower", [transform.x, transform.y]));
|
|
312
|
+
push(generatedField("__regression_ci_upper", "number", definition, "upper", [transform.x, transform.y]));
|
|
313
|
+
}
|
|
314
|
+
} else if (transform.type === "density") {
|
|
315
|
+
fields = [
|
|
316
|
+
...(transform.groupBy === undefined ? [] : [inheritedField(input, transform.groupBy)]),
|
|
317
|
+
...(transform.placement?.split === undefined ? [] : [inheritedField(input, transform.placement.split.field)]),
|
|
318
|
+
generatedField(transform.as[0], "number", definition, "value", [transform.field]),
|
|
319
|
+
generatedField(transform.as[1], "number", definition, "density", [transform.field])
|
|
320
|
+
].filter(Boolean);
|
|
321
|
+
if (transform.groupBy === undefined && transform.placement !== undefined) {
|
|
322
|
+
push(generatedField(transform.placement.categoryField, "string", definition, "category"));
|
|
323
|
+
}
|
|
324
|
+
} else if (transform.type === "interval") {
|
|
325
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
326
|
+
push(generatedField(transform.as.center, "number", definition, "center", [transform.field]));
|
|
327
|
+
push(generatedField(transform.as.lower, "number", definition, "lower", [transform.field]));
|
|
328
|
+
push(generatedField(transform.as.upper, "number", definition, "upper", [transform.field]));
|
|
329
|
+
} else if (transform.type === "ecdf") {
|
|
330
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
331
|
+
push(generatedField(transform.as.value, "number", definition, "value", [transform.field]));
|
|
332
|
+
push(generatedField(transform.as.cumulative, "number", definition, "cumulative", [transform.field]));
|
|
333
|
+
push(generatedField(transform.as.probability, "number", definition, "probability", [transform.field]));
|
|
334
|
+
} else if (transform.type === "computed") {
|
|
335
|
+
push(generatedField(transform.as, "unknown", definition, "value", expressionFields(transform.expression)));
|
|
336
|
+
} else if (transform.type === "timeUnit") {
|
|
337
|
+
push(generatedField(transform.as, "number", definition, "value", [transform.field]));
|
|
338
|
+
} else if (transform.type === "window") {
|
|
339
|
+
for (const item of transform.operations) {
|
|
340
|
+
push(generatedField(
|
|
341
|
+
item.as,
|
|
342
|
+
["lag", "lead"].includes(item.op) ? "unknown" : "number",
|
|
343
|
+
definition,
|
|
344
|
+
item.op,
|
|
345
|
+
item.field ? [item.field] : []
|
|
346
|
+
));
|
|
347
|
+
}
|
|
348
|
+
} else if (transform.type === "normalize") {
|
|
349
|
+
push(generatedField(transform.as, "number", definition, "value", [transform.field]));
|
|
350
|
+
} else if (transform.type === "stack") {
|
|
351
|
+
for (const [role, name] of Object.entries(transform.as ?? {})) {
|
|
352
|
+
push(generatedField(name, "number", definition, role, [transform.field]));
|
|
353
|
+
}
|
|
354
|
+
} else if (transform.type === "fold") {
|
|
355
|
+
fields = input.fields.filter(field => !transform.fields.includes(field.name)).map(field => ({ ...field }));
|
|
356
|
+
push(generatedField(transform.as.key, "string", definition, "key", transform.fields));
|
|
357
|
+
push(generatedField(transform.as.value, "unknown", definition, "value", transform.fields));
|
|
358
|
+
}
|
|
359
|
+
const inferred = Array.isArray(values) && values.length > 0
|
|
360
|
+
? inferDatasetSchema(values, { origin: "derived" })
|
|
361
|
+
: undefined;
|
|
362
|
+
if (inferred !== undefined) {
|
|
363
|
+
const actual = new Map(inferred.fields.map(field => [field.name, field]));
|
|
364
|
+
fields = fields.map(planned => {
|
|
365
|
+
const field = actual.get(planned.name);
|
|
366
|
+
if (field === undefined) {
|
|
367
|
+
return ["complete", "impute"].includes(transform.type)
|
|
368
|
+
? { ...planned, optional: true }
|
|
369
|
+
: planned;
|
|
370
|
+
}
|
|
371
|
+
if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
|
|
372
|
+
return planned;
|
|
373
|
+
}
|
|
374
|
+
return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
|
|
375
|
+
});
|
|
376
|
+
for (const field of inferred.fields) {
|
|
377
|
+
if (!fields.some(planned => planned.name === field.name)) fields.push(field);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return cloneAndFreeze({
|
|
381
|
+
version: 1,
|
|
382
|
+
completeness: input.completeness === "known" || fields.length > 0 ? "known" : "unknown",
|
|
383
|
+
origin: "derived",
|
|
384
|
+
fields: input.completeness === "known" || fields.length > 0 ? fields : []
|
|
385
|
+
});
|
|
386
|
+
}
|
package/src/grammar/density.js
CHANGED
|
@@ -59,6 +59,7 @@ export function normalizeDensityTransform(args = {}) {
|
|
|
59
59
|
as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
|
|
60
60
|
resolve: args.resolve ?? "shared",
|
|
61
61
|
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
62
|
+
...(args.missing === undefined ? {} : { missing: args.missing }),
|
|
62
63
|
...(args.placement === undefined ? {} : { placement: args.placement })
|
|
63
64
|
};
|
|
64
65
|
validateDensityTransform(transform);
|
|
@@ -370,7 +371,7 @@ function hasValidResolvedBandwidthState(resolved, transform) {
|
|
|
370
371
|
export function validateDensityTransform(transform) {
|
|
371
372
|
const supported = [
|
|
372
373
|
"type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
|
|
373
|
-
"resolve", "kernel", "normalization", "placement", "weight", "resolved"
|
|
374
|
+
"resolve", "kernel", "normalization", "placement", "weight", "missing", "resolved"
|
|
374
375
|
];
|
|
375
376
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
376
377
|
if (unknown !== undefined) {
|
|
@@ -383,6 +384,9 @@ export function validateDensityTransform(transform) {
|
|
|
383
384
|
if (transform.weight !== undefined) {
|
|
384
385
|
normalizeStatisticalWeight(transform.weight, "Density weight");
|
|
385
386
|
}
|
|
387
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
388
|
+
throw new Error('Density missing must be "error" or "drop".');
|
|
389
|
+
}
|
|
386
390
|
if (transform.groupBy !== undefined) {
|
|
387
391
|
requireField(transform.groupBy, "Density groupBy");
|
|
388
392
|
}
|
|
@@ -540,7 +544,8 @@ export function deriveKernelDensity(values, {
|
|
|
540
544
|
normalization = "unit",
|
|
541
545
|
as,
|
|
542
546
|
placement,
|
|
543
|
-
weight
|
|
547
|
+
weight,
|
|
548
|
+
missing
|
|
544
549
|
} = {}) {
|
|
545
550
|
if (!Array.isArray(values)) {
|
|
546
551
|
throw new TypeError("Density values must be an array.");
|
|
@@ -561,9 +566,31 @@ export function deriveKernelDensity(values, {
|
|
|
561
566
|
)];
|
|
562
567
|
const resolvedKernel = validateDensityKernel(kernel);
|
|
563
568
|
const resolvedNormalization = validateDensityNormalization(normalization);
|
|
569
|
+
let missingValueRows = 0;
|
|
570
|
+
let missingWeightRows = 0;
|
|
571
|
+
const inputValues = missing === undefined ? values : values.filter((row, index) => {
|
|
572
|
+
const source = row?.[sourceField];
|
|
573
|
+
if (source === null || source === undefined) {
|
|
574
|
+
if (missing === "error") throw new TypeError(`Density field "${sourceField}" is missing at row ${index}.`);
|
|
575
|
+
missingValueRows += 1;
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
if (!Number.isFinite(source)) {
|
|
579
|
+
throw new TypeError(`Density field "${sourceField}" must contain a finite number at row ${index}.`);
|
|
580
|
+
}
|
|
581
|
+
if (weight !== undefined) {
|
|
582
|
+
const weightValue = row?.[weight.field];
|
|
583
|
+
if (weightValue === null || weightValue === undefined) {
|
|
584
|
+
if (missing === "error") throw new TypeError(`Density weight "${weight.field}" is missing at row ${index}.`);
|
|
585
|
+
missingWeightRows += 1;
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
});
|
|
564
591
|
const statisticalWeights = weight === undefined
|
|
565
592
|
? undefined
|
|
566
|
-
: readStatisticalWeights(
|
|
593
|
+
: readStatisticalWeights(inputValues, weight, "Density");
|
|
567
594
|
if (statisticalWeights !== undefined) {
|
|
568
595
|
validateWeightedNumericFields(
|
|
569
596
|
statisticalWeights.entries,
|
|
@@ -579,7 +606,7 @@ export function deriveKernelDensity(values, {
|
|
|
579
606
|
isNominalValue(entry.row[placement.split.field]))
|
|
580
607
|
);
|
|
581
608
|
const validRows = statisticalWeights === undefined
|
|
582
|
-
?
|
|
609
|
+
? inputValues.filter(row =>
|
|
583
610
|
row !== null &&
|
|
584
611
|
typeof row === "object" &&
|
|
585
612
|
Number.isFinite(row[sourceField]) &&
|
|
@@ -759,6 +786,27 @@ export function deriveKernelDensity(values, {
|
|
|
759
786
|
extent: resolvedExtent,
|
|
760
787
|
steps,
|
|
761
788
|
samples,
|
|
762
|
-
values: rows
|
|
789
|
+
values: rows,
|
|
790
|
+
...(missing === undefined ? {} : {
|
|
791
|
+
report: {
|
|
792
|
+
version: 1,
|
|
793
|
+
owner: { kind: "data", id: "pending" },
|
|
794
|
+
inputs: [],
|
|
795
|
+
units: [{
|
|
796
|
+
role: outputFields[1],
|
|
797
|
+
group: {},
|
|
798
|
+
inputRows: values.length,
|
|
799
|
+
usedRows: validRows.length,
|
|
800
|
+
excludedRows: values.length - validRows.length,
|
|
801
|
+
excludedByReason: {
|
|
802
|
+
...(missingValueRows === 0 ? {} : { "missing-value": missingValueRows }),
|
|
803
|
+
...(missingWeightRows === 0 ? {} : { "missing-weight": missingWeightRows })
|
|
804
|
+
},
|
|
805
|
+
...(statisticalWeights === undefined ? {} : {
|
|
806
|
+
zeroWeightRows: statisticalWeights.entries.filter(entry => entry.weight === 0).length
|
|
807
|
+
})
|
|
808
|
+
}]
|
|
809
|
+
}
|
|
810
|
+
})
|
|
763
811
|
});
|
|
764
812
|
}
|