ggaction 0.0.16 → 0.0.18
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 +66 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -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 +20 -7
- 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 +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- 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 +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.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 +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { requireStringValue as requireName } from "../core/validation.js";
|
|
2
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
3
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
4
|
+
|
|
5
|
+
const STORAGE_TYPES = new Set([
|
|
6
|
+
"number", "string", "boolean", "array", "object", "unknown", "mixed"
|
|
7
|
+
]);
|
|
8
|
+
const DECLARED_STORAGE_TYPES = new Set([
|
|
9
|
+
"number", "string", "boolean", "array", "object"
|
|
10
|
+
]);
|
|
11
|
+
const FIELD_KEYS = Object.freeze([
|
|
12
|
+
"name", "storageType", "nullable", "optional", "lineage"
|
|
13
|
+
]);
|
|
14
|
+
const SOURCE_FIELD_KEYS = Object.freeze([
|
|
15
|
+
"name", "storageType", "nullable", "optional"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function rejectUnknownKeys(value, keys, label) {
|
|
19
|
+
const unknown = Object.keys(value).find(key => !keys.includes(key));
|
|
20
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function valueStorageType(value) {
|
|
25
|
+
if (typeof value === "number") return "number";
|
|
26
|
+
if (typeof value === "string") return "string";
|
|
27
|
+
if (typeof value === "boolean") return "boolean";
|
|
28
|
+
if (Array.isArray(value)) return "array";
|
|
29
|
+
if (isPlainObject(value)) return "object";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeLineage(value, label) {
|
|
34
|
+
if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object.`);
|
|
35
|
+
rejectUnknownKeys(value, ["inputs", "owner", "role"], label);
|
|
36
|
+
if (!Array.isArray(value.inputs)) throw new TypeError(`${label} inputs must be an array.`);
|
|
37
|
+
const inputs = value.inputs.map((input, index) => {
|
|
38
|
+
if (!isPlainObject(input)) throw new TypeError(`${label} input ${index} must be a plain object.`);
|
|
39
|
+
rejectUnknownKeys(input, ["data", "field"], `${label} input ${index}`);
|
|
40
|
+
return {
|
|
41
|
+
data: requireName(input.data, `${label} input ${index} data`),
|
|
42
|
+
field: requireName(input.field, `${label} input ${index} field`)
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
inputs,
|
|
47
|
+
owner: requireName(value.owner, `${label} owner`),
|
|
48
|
+
role: requireName(value.role, `${label} role`)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeField(value, index, { declared = false } = {}) {
|
|
53
|
+
if (!isPlainObject(value)) throw new TypeError(`Dataset schema field ${index} must be a plain object.`);
|
|
54
|
+
rejectUnknownKeys(
|
|
55
|
+
value,
|
|
56
|
+
declared ? SOURCE_FIELD_KEYS : FIELD_KEYS,
|
|
57
|
+
`dataset schema field ${index}`
|
|
58
|
+
);
|
|
59
|
+
// Stored row keys are lossless; action field references still require non-empty names.
|
|
60
|
+
const name = value.name === "" ? "" : requireName(value.name, `Dataset schema field ${index} name`);
|
|
61
|
+
const allowed = declared ? DECLARED_STORAGE_TYPES : STORAGE_TYPES;
|
|
62
|
+
if (!allowed.has(value.storageType)) {
|
|
63
|
+
throw new Error(`Unsupported dataset schema storageType "${value.storageType}".`);
|
|
64
|
+
}
|
|
65
|
+
for (const key of ["nullable", "optional"]) {
|
|
66
|
+
if (value[key] !== undefined && typeof value[key] !== "boolean") {
|
|
67
|
+
throw new TypeError(`Dataset schema field ${index} ${key} must be a boolean.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
storageType: value.storageType,
|
|
73
|
+
nullable: value.nullable ?? false,
|
|
74
|
+
optional: value.optional ?? false,
|
|
75
|
+
...(value.lineage === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { lineage: normalizeLineage(value.lineage, `Dataset schema field ${index} lineage`) })
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeSourceSchema(value) {
|
|
82
|
+
if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
|
|
83
|
+
rejectUnknownKeys(value, ["fields"], "dataset schema");
|
|
84
|
+
if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
|
|
85
|
+
const fields = value.fields.map((field, index) => normalizeField(field, index, { declared: true }));
|
|
86
|
+
if (new Set(fields.map(field => field.name)).size !== fields.length) {
|
|
87
|
+
throw new Error("Dataset schema field names must be unique.");
|
|
88
|
+
}
|
|
89
|
+
return cloneAndFreeze({
|
|
90
|
+
version: 1,
|
|
91
|
+
completeness: "known",
|
|
92
|
+
origin: "declared",
|
|
93
|
+
fields
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateDatasetSchema(value) {
|
|
98
|
+
if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
|
|
99
|
+
rejectUnknownKeys(value, ["version", "completeness", "origin", "fields"], "dataset schema");
|
|
100
|
+
if (value.version !== 1) throw new Error(`Unsupported dataset schema version "${value.version}".`);
|
|
101
|
+
if (!["known", "unknown"].includes(value.completeness)) {
|
|
102
|
+
throw new Error(`Unsupported dataset schema completeness "${value.completeness}".`);
|
|
103
|
+
}
|
|
104
|
+
if (!["declared", "inferred", "derived"].includes(value.origin)) {
|
|
105
|
+
throw new Error(`Unsupported dataset schema origin "${value.origin}".`);
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
|
|
108
|
+
const fields = value.fields.map((field, index) => normalizeField(field, index));
|
|
109
|
+
if (new Set(fields.map(field => field.name)).size !== fields.length) {
|
|
110
|
+
throw new Error("Dataset schema field names must be unique.");
|
|
111
|
+
}
|
|
112
|
+
if (value.completeness === "unknown" && fields.length > 0) {
|
|
113
|
+
throw new Error("Unknown dataset schema cannot contain known fields.");
|
|
114
|
+
}
|
|
115
|
+
return cloneAndFreeze({
|
|
116
|
+
version: 1,
|
|
117
|
+
completeness: value.completeness,
|
|
118
|
+
origin: value.origin,
|
|
119
|
+
fields
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function inferDatasetSchema(values, { origin = "inferred" } = {}) {
|
|
124
|
+
if (!Array.isArray(values)) throw new TypeError("Dataset schema inference requires row values.");
|
|
125
|
+
if (values.length === 0) {
|
|
126
|
+
return cloneAndFreeze({ version: 1, completeness: "unknown", origin, fields: [] });
|
|
127
|
+
}
|
|
128
|
+
const records = new Map();
|
|
129
|
+
for (const row of values) {
|
|
130
|
+
for (const name of Object.keys(row)) {
|
|
131
|
+
if (!records.has(name)) records.set(name, { types: new Set(), nullable: false, present: 0 });
|
|
132
|
+
const record = records.get(name);
|
|
133
|
+
const value = row[name];
|
|
134
|
+
if (value !== undefined) record.present += 1;
|
|
135
|
+
if (value === null) record.nullable = true;
|
|
136
|
+
else if (value !== undefined) record.types.add(valueStorageType(value));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const fields = [...records].map(([name, record]) => ({
|
|
140
|
+
name,
|
|
141
|
+
storageType: record.types.size === 0
|
|
142
|
+
? "unknown"
|
|
143
|
+
: record.types.size === 1 ? [...record.types][0] : "mixed",
|
|
144
|
+
nullable: record.nullable,
|
|
145
|
+
optional: record.present < values.length
|
|
146
|
+
}));
|
|
147
|
+
return cloneAndFreeze({ version: 1, completeness: "known", origin, fields });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function validateRowsAgainstSchema(values, schema, label = "Dataset") {
|
|
151
|
+
const normalized = validateDatasetSchema(schema);
|
|
152
|
+
if (normalized.completeness !== "known") return normalized;
|
|
153
|
+
const fields = new Map(normalized.fields.map(field => [field.name, field]));
|
|
154
|
+
for (let rowIndex = 0; rowIndex < values.length; rowIndex += 1) {
|
|
155
|
+
const row = values[rowIndex];
|
|
156
|
+
if (normalized.origin === "declared") {
|
|
157
|
+
const unknown = Object.keys(row).find(name => !fields.has(name));
|
|
158
|
+
if (unknown !== undefined) throw new Error(`${label} row ${rowIndex} contains undeclared field "${unknown}".`);
|
|
159
|
+
}
|
|
160
|
+
for (const field of normalized.fields) {
|
|
161
|
+
const present = Object.hasOwn(row, field.name) && row[field.name] !== undefined;
|
|
162
|
+
if (!present) {
|
|
163
|
+
if (!field.optional) throw new Error(`${label} row ${rowIndex} requires field "${field.name}".`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const value = row[field.name];
|
|
167
|
+
if (value === null) {
|
|
168
|
+
if (!field.nullable) throw new TypeError(`${label} field "${field.name}" cannot be null at row ${rowIndex}.`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!["unknown", "mixed"].includes(field.storageType) && valueStorageType(value) !== field.storageType) {
|
|
172
|
+
throw new TypeError(`${label} field "${field.name}" must contain ${field.storageType} values; invalid row ${rowIndex}.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function schemaField(schema, name) {
|
|
180
|
+
return validateDatasetSchema(schema).fields.find(field => field.name === name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function assertFieldsAvailable(schema, fields, { data, operation } = {}) {
|
|
184
|
+
const normalized = validateDatasetSchema(schema);
|
|
185
|
+
const unique = [...new Set(fields.filter(field => typeof field === "string" && field.length > 0))];
|
|
186
|
+
for (const field of unique) {
|
|
187
|
+
if (normalized.completeness !== "known" || schemaField(normalized, field) === undefined) {
|
|
188
|
+
throw annotateError(new Error(
|
|
189
|
+
`Dataset "${data ?? "unknown"}" does not contain field "${field}".`
|
|
190
|
+
), {
|
|
191
|
+
code: "missing-resource",
|
|
192
|
+
operation,
|
|
193
|
+
optionPath: "field",
|
|
194
|
+
resourceId: data
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function expressionFields(expression, output = []) {
|
|
202
|
+
if (!isPlainObject(expression)) return output;
|
|
203
|
+
if (typeof expression.field === "string") output.push(expression.field);
|
|
204
|
+
for (const value of Object.values(expression)) {
|
|
205
|
+
if (Array.isArray(value)) value.forEach(item => expressionFields(item, output));
|
|
206
|
+
else if (isPlainObject(value)) expressionFields(value, output);
|
|
207
|
+
}
|
|
208
|
+
return output;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function transformInputFields(transform) {
|
|
212
|
+
const result = [];
|
|
213
|
+
const add = value => {
|
|
214
|
+
if (typeof value === "string" && value.length > 0) result.push(value);
|
|
215
|
+
else if (Array.isArray(value)) value.forEach(add);
|
|
216
|
+
};
|
|
217
|
+
add(transform.field);
|
|
218
|
+
add(transform.x);
|
|
219
|
+
add(transform.x?.field);
|
|
220
|
+
add(transform.y);
|
|
221
|
+
add(transform.y?.field);
|
|
222
|
+
add(transform.category);
|
|
223
|
+
add(transform.group);
|
|
224
|
+
add(transform.value);
|
|
225
|
+
add(transform.key);
|
|
226
|
+
add(transform.groupBy);
|
|
227
|
+
add(transform.partitionBy);
|
|
228
|
+
add(transform.fields);
|
|
229
|
+
add(transform.keys);
|
|
230
|
+
add(transform.weight?.field ?? transform.weight);
|
|
231
|
+
add(transform.placement?.categoryField);
|
|
232
|
+
add(transform.placement?.split?.field);
|
|
233
|
+
add(transform.sortBy?.map(item => item.field));
|
|
234
|
+
if (transform.type === "window") {
|
|
235
|
+
const generated = new Set();
|
|
236
|
+
for (const item of transform.operations ?? []) {
|
|
237
|
+
if (typeof item.field === "string" && !generated.has(item.field)) add(item.field);
|
|
238
|
+
add(item.orderBy);
|
|
239
|
+
if (typeof item.as === "string") generated.add(item.as);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
add(transform.operations?.flatMap(item => [item.field, item.orderBy]));
|
|
243
|
+
}
|
|
244
|
+
add(transform.aggregates?.flatMap(item => [item.field, item.op?.orderBy]));
|
|
245
|
+
add(expressionFields(transform.expression));
|
|
246
|
+
return [...new Set(result)];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function inheritedField(schema, name) {
|
|
250
|
+
const field = schema.fields.find(item => item.name === name);
|
|
251
|
+
return field === undefined ? undefined : { ...field };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function generatedField(name, storageType, transform, role, inputs = []) {
|
|
255
|
+
return {
|
|
256
|
+
name,
|
|
257
|
+
storageType,
|
|
258
|
+
nullable: !["count", "valid", "missing", "rowNumber", "rank", "denseRank"].includes(role),
|
|
259
|
+
optional: false,
|
|
260
|
+
lineage: {
|
|
261
|
+
inputs: inputs
|
|
262
|
+
.filter(field => typeof field === "string" && field.length > 0)
|
|
263
|
+
.map(field => ({ data: transform.__sourceId ?? "source", field })),
|
|
264
|
+
owner: transform.__ownerId ?? transform.type,
|
|
265
|
+
role
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
|
|
271
|
+
const input = validateDatasetSchema(inputSchema);
|
|
272
|
+
const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
|
|
273
|
+
const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
|
|
274
|
+
let fields = retain.includes(transform.type) ? input.fields.map(field => ({ ...field })) : [];
|
|
275
|
+
const push = field => {
|
|
276
|
+
if (field === undefined) return;
|
|
277
|
+
fields = fields.filter(item => item.name !== field.name);
|
|
278
|
+
fields.push(field);
|
|
279
|
+
};
|
|
280
|
+
if (transform.type === "summary") {
|
|
281
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
282
|
+
for (const item of transform.aggregates) {
|
|
283
|
+
const op = typeof item.op === "string" ? item.op : item.op.op;
|
|
284
|
+
push(generatedField(item.as, "number", definition, op, item.field ? [item.field] : []));
|
|
285
|
+
}
|
|
286
|
+
if (transform.members !== undefined) push(generatedField(transform.members, "array", definition, "members"));
|
|
287
|
+
} else if (transform.type === "bin") {
|
|
288
|
+
fields = [
|
|
289
|
+
generatedField(transform.as.lower, "number", definition, "lower", [transform.field]),
|
|
290
|
+
generatedField(transform.as.upper, "number", definition, "upper", [transform.field]),
|
|
291
|
+
generatedField(transform.as.count, "number", definition, "count", [transform.field])
|
|
292
|
+
];
|
|
293
|
+
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
294
|
+
} else if (transform.type === "bin2d") {
|
|
295
|
+
fields = [
|
|
296
|
+
generatedField(transform.as.x0, "number", definition, "x0", [transform.x]),
|
|
297
|
+
generatedField(transform.as.x1, "number", definition, "x1", [transform.x]),
|
|
298
|
+
generatedField(transform.as.y0, "number", definition, "y0", [transform.y]),
|
|
299
|
+
generatedField(transform.as.y1, "number", definition, "y1", [transform.y]),
|
|
300
|
+
generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
|
|
301
|
+
];
|
|
302
|
+
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
303
|
+
} else if (transform.type === "regression") {
|
|
304
|
+
fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
|
|
305
|
+
if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
|
|
306
|
+
if (transform.interval !== false && transform.method !== "loess") {
|
|
307
|
+
push(generatedField("__regression_ci_lower", "number", definition, "lower", [transform.x, transform.y]));
|
|
308
|
+
push(generatedField("__regression_ci_upper", "number", definition, "upper", [transform.x, transform.y]));
|
|
309
|
+
}
|
|
310
|
+
} else if (transform.type === "density") {
|
|
311
|
+
fields = [
|
|
312
|
+
...(transform.groupBy === undefined ? [] : [inheritedField(input, transform.groupBy)]),
|
|
313
|
+
...(transform.placement?.split === undefined ? [] : [inheritedField(input, transform.placement.split.field)]),
|
|
314
|
+
generatedField(transform.as[0], "number", definition, "value", [transform.field]),
|
|
315
|
+
generatedField(transform.as[1], "number", definition, "density", [transform.field])
|
|
316
|
+
].filter(Boolean);
|
|
317
|
+
if (transform.groupBy === undefined && transform.placement !== undefined) {
|
|
318
|
+
push(generatedField(transform.placement.categoryField, "string", definition, "category"));
|
|
319
|
+
}
|
|
320
|
+
} else if (transform.type === "interval") {
|
|
321
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
322
|
+
push(generatedField(transform.as.center, "number", definition, "center", [transform.field]));
|
|
323
|
+
push(generatedField(transform.as.lower, "number", definition, "lower", [transform.field]));
|
|
324
|
+
push(generatedField(transform.as.upper, "number", definition, "upper", [transform.field]));
|
|
325
|
+
} else if (transform.type === "ecdf") {
|
|
326
|
+
fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
|
|
327
|
+
push(generatedField(transform.as.value, "number", definition, "value", [transform.field]));
|
|
328
|
+
push(generatedField(transform.as.cumulative, "number", definition, "cumulative", [transform.field]));
|
|
329
|
+
push(generatedField(transform.as.probability, "number", definition, "probability", [transform.field]));
|
|
330
|
+
} else if (transform.type === "computed") {
|
|
331
|
+
push(generatedField(transform.as, "unknown", definition, "value", expressionFields(transform.expression)));
|
|
332
|
+
} else if (transform.type === "timeUnit") {
|
|
333
|
+
push(generatedField(transform.as, "number", definition, "value", [transform.field]));
|
|
334
|
+
} else if (transform.type === "window") {
|
|
335
|
+
for (const item of transform.operations) {
|
|
336
|
+
push(generatedField(
|
|
337
|
+
item.as,
|
|
338
|
+
["lag", "lead"].includes(item.op) ? "unknown" : "number",
|
|
339
|
+
definition,
|
|
340
|
+
item.op,
|
|
341
|
+
item.field ? [item.field] : []
|
|
342
|
+
));
|
|
343
|
+
}
|
|
344
|
+
} else if (transform.type === "normalize") {
|
|
345
|
+
push(generatedField(transform.as, "number", definition, "value", [transform.field]));
|
|
346
|
+
} else if (transform.type === "stack") {
|
|
347
|
+
for (const [role, name] of Object.entries(transform.as ?? {})) {
|
|
348
|
+
push(generatedField(name, "number", definition, role, [transform.field]));
|
|
349
|
+
}
|
|
350
|
+
} else if (transform.type === "fold") {
|
|
351
|
+
fields = input.fields.filter(field => !transform.fields.includes(field.name)).map(field => ({ ...field }));
|
|
352
|
+
push(generatedField(transform.as.key, "string", definition, "key", transform.fields));
|
|
353
|
+
push(generatedField(transform.as.value, "unknown", definition, "value", transform.fields));
|
|
354
|
+
}
|
|
355
|
+
const inferred = Array.isArray(values) && values.length > 0
|
|
356
|
+
? inferDatasetSchema(values, { origin: "derived" })
|
|
357
|
+
: undefined;
|
|
358
|
+
if (inferred !== undefined) {
|
|
359
|
+
const actual = new Map(inferred.fields.map(field => [field.name, field]));
|
|
360
|
+
fields = fields.map(planned => {
|
|
361
|
+
const field = actual.get(planned.name);
|
|
362
|
+
if (field === undefined) {
|
|
363
|
+
return ["complete", "impute"].includes(transform.type)
|
|
364
|
+
? { ...planned, optional: true }
|
|
365
|
+
: planned;
|
|
366
|
+
}
|
|
367
|
+
if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
|
|
368
|
+
return planned;
|
|
369
|
+
}
|
|
370
|
+
return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
|
|
371
|
+
});
|
|
372
|
+
for (const field of inferred.fields) {
|
|
373
|
+
if (!fields.some(planned => planned.name === field.name)) fields.push(field);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return cloneAndFreeze({
|
|
377
|
+
version: 1,
|
|
378
|
+
completeness: input.completeness === "known" || fields.length > 0 ? "known" : "unknown",
|
|
379
|
+
origin: "derived",
|
|
380
|
+
fields: input.completeness === "known" || fields.length > 0 ? fields : []
|
|
381
|
+
});
|
|
382
|
+
}
|
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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateNonEmptyString } from "../core/validation.js";
|
|
2
3
|
|
|
3
4
|
const ENTRY_KEYS = Object.freeze(["value", "label"]);
|
|
4
5
|
|
|
@@ -55,3 +56,30 @@ export function resolveDisplayLabel(value, map, fallback) {
|
|
|
55
56
|
}
|
|
56
57
|
return resolved;
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export function normalizeDisplayColorMap(value, label = "colorMap") {
|
|
62
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
|
|
63
|
+
const normalized = [];
|
|
64
|
+
for (const [index, entry] of value.entries()) {
|
|
65
|
+
if (!isPlainObject(entry)) throw new TypeError(`${label}[${index}] must be a plain object.`);
|
|
66
|
+
if (Reflect.ownKeys(entry).length !== 2 ||
|
|
67
|
+
!Object.hasOwn(entry, "value") || !Object.hasOwn(entry, "color")) {
|
|
68
|
+
throw new Error(`${label}[${index}] must contain exactly value and color.`);
|
|
69
|
+
}
|
|
70
|
+
if (!isDisplayLabelValue(entry.value)) {
|
|
71
|
+
throw new TypeError(`${label}[${index}].value must be a string, boolean, null, or finite number.`);
|
|
72
|
+
}
|
|
73
|
+
validateNonEmptyString(entry.color, `${label}[${index}].color`);
|
|
74
|
+
if (normalized.some(previous => sameDisplayLabelValue(previous.value, entry.value))) {
|
|
75
|
+
throw new Error(`${label} contains a duplicate typed value.`);
|
|
76
|
+
}
|
|
77
|
+
normalized.push(Object.freeze({ value: entry.value, color: entry.color }));
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze(normalized);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveDisplayLabelColor(value, map, fallback) {
|
|
83
|
+
const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
|
|
84
|
+
return entry?.color ?? fallback;
|
|
85
|
+
}
|
package/src/grammar/ecdf.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isGroupValue as groupValue } from "./transformKeys.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import { stableDecimal } from "./numeric.js";
|
|
3
5
|
|
|
@@ -6,11 +8,6 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
8
|
]);
|
|
7
9
|
const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
|
|
8
10
|
|
|
9
|
-
function nonEmptyString(value, label) {
|
|
10
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
11
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
11
|
|
|
15
12
|
function normalizeGroupBy(groupBy) {
|
|
16
13
|
const fields = groupBy === undefined
|
|
@@ -101,10 +98,6 @@ export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop
|
|
|
101
98
|
return cloneAndFreeze(transform);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
function groupValue(value) {
|
|
105
|
-
return typeof value === "string" || typeof value === "boolean" ||
|
|
106
|
-
(typeof value === "number" && Number.isFinite(value));
|
|
107
|
-
}
|
|
108
101
|
|
|
109
102
|
function rejectOrDrop(transform, index, reason) {
|
|
110
103
|
if (transform.missing === "error") {
|
|
@@ -6,12 +6,13 @@ import { resolveRectMode } from "../rects.js";
|
|
|
6
6
|
import { resolveRuleMode } from "../rules.js";
|
|
7
7
|
|
|
8
8
|
const CARTESIAN_MARKS = new Set([
|
|
9
|
-
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
9
|
+
"point", "line", "area", "bar", "rule", "tick", "rect", "text"
|
|
10
10
|
]);
|
|
11
11
|
const FACET_BAR_GRAINS = new Set([
|
|
12
12
|
BAR_GRAINS.histogram,
|
|
13
13
|
BAR_GRAINS.aggregate,
|
|
14
|
-
BAR_GRAINS.ranged
|
|
14
|
+
BAR_GRAINS.ranged,
|
|
15
|
+
BAR_GRAINS.centered
|
|
15
16
|
]);
|
|
16
17
|
const ORDINARY_SCALE_CHANNELS = Object.freeze([
|
|
17
18
|
["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
|
|
@@ -66,6 +67,9 @@ function classifyPrimaryLayer(semanticSpec, layer) {
|
|
|
66
67
|
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
67
68
|
);
|
|
68
69
|
}
|
|
70
|
+
if (type === "text" && layer.encoding?.text === undefined) {
|
|
71
|
+
throw new Error(`Facet text layer "${layer.id}" requires a text encoding.`);
|
|
72
|
+
}
|
|
69
73
|
const complete = (
|
|
70
74
|
layer.encoding?.x?.scale !== undefined &&
|
|
71
75
|
layer.encoding?.y?.scale !== undefined
|
|
@@ -165,6 +165,11 @@ function scaleIds(config) {
|
|
|
165
165
|
export function resolveSharedFacetLegends(children) {
|
|
166
166
|
const normalized = requireLegendChildren(children);
|
|
167
167
|
const first = normalized[0];
|
|
168
|
+
for (const child of normalized.slice(1)) {
|
|
169
|
+
if (!sameValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
|
|
170
|
+
throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
168
173
|
const kinds = Object.keys(first.legends);
|
|
169
174
|
const entries = kinds.map(kind => {
|
|
170
175
|
const family = LEGEND_FAMILIES[kind];
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
} from "../../core/validation.js";
|
|
7
7
|
import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
|
|
8
8
|
import { readNominalField } from "../scales/index.js";
|
|
9
|
-
const MAX_FACET_CHILDREN = 100;
|
|
10
9
|
|
|
11
10
|
function requireFacetField(field, label = "facet") {
|
|
12
11
|
if (typeof field !== "string" || field.length === 0) {
|
|
@@ -100,8 +99,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
100
99
|
const values = resolveValues(observed, options.values);
|
|
101
100
|
validateGeneratedItemLimit(
|
|
102
101
|
values.length,
|
|
103
|
-
"Facet child count"
|
|
104
|
-
MAX_FACET_CHILDREN
|
|
102
|
+
"Facet child count"
|
|
105
103
|
);
|
|
106
104
|
validateWorkLimit(
|
|
107
105
|
dataset.values.length * values.length,
|
|
@@ -175,8 +173,7 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
|
|
|
175
173
|
}
|
|
176
174
|
validateGeneratedItemLimit(
|
|
177
175
|
selected.length,
|
|
178
|
-
"Facet grid child count"
|
|
179
|
-
MAX_FACET_CHILDREN
|
|
176
|
+
"Facet grid child count"
|
|
180
177
|
);
|
|
181
178
|
validateWorkLimit(
|
|
182
179
|
dataset.values.length * selected.length,
|