ggaction 0.0.13 → 0.0.15
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 +78 -0
- package/README.md +20 -11
- 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 -3
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +11 -3
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/categoryOrder/index.js +15 -4
- package/src/actions/charts/area.js +5 -1
- package/src/actions/charts/bar.js +2 -1
- package/src/actions/charts/density.js +6 -2
- package/src/actions/charts/ecdf.js +4 -1
- package/src/actions/charts/endpoints.js +25 -3
- package/src/actions/charts/heatmap.js +2 -1
- package/src/actions/charts/histogram.js +4 -3
- package/src/actions/charts/horizon.js +3 -1
- package/src/actions/charts/interval-regression.js +5 -2
- package/src/actions/charts/line.js +3 -1
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +10 -2
- package/src/actions/charts/polar.js +15 -4
- package/src/actions/charts/radar.js +8 -2
- package/src/actions/charts/radial.js +10 -2
- package/src/actions/charts/rug-strip.js +6 -2
- package/src/actions/charts/scatter.js +3 -1
- package/src/actions/composition/actions.js +13 -4
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +47 -17
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +14 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +14 -0
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +16 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/summary.js +1 -1
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +2 -1
- package/src/actions/encodings/density.js +5 -2
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +20 -13
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +10 -2
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/remove.js +9 -4
- package/src/actions/encodings/ruleAppearance.js +1 -23
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/errorBands/create.js +23 -3
- package/src/actions/errorBands/edit.js +21 -6
- package/src/actions/errorBars/create.js +35 -7
- package/src/actions/errorBars/edit.js +19 -4
- package/src/actions/facets/actions.js +237 -57
- package/src/actions/facets/derive.js +91 -26
- package/src/actions/gradientPlots/components.js +10 -2
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +3 -1
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/labels.js +58 -12
- package/src/actions/guides/axes/lines.js +6 -2
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/tickGroups.js +2 -1
- package/src/actions/guides/axes/ticks.js +6 -2
- package/src/actions/guides/axes/titles.js +11 -3
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +19 -3
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +134 -48
- package/src/actions/guides/legends/categorical/components.js +17 -12
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +97 -15
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +85 -30
- package/src/actions/guides/legends/continuous/common.js +30 -12
- package/src/actions/guides/legends/continuous/gradient.js +10 -7
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +8 -3
- package/src/actions/guides/legends/continuous/opacity.js +8 -5
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +158 -59
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +54 -19
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +13 -3
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +93 -40
- package/src/actions/guides/legends/strokeWidth.js +34 -28
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +24 -5
- package/src/actions/guides/polar/axes/lines.js +10 -4
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +1 -1
- package/src/actions/guides/polar/axes/titles.js +1 -1
- package/src/actions/guides/polar/grids.js +1 -1
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +14 -3
- package/src/actions/marks/arc/actions.js +77 -19
- package/src/actions/marks/area/actions.js +34 -9
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +22 -2
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +37 -14
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +18 -6
- package/src/actions/marks/point/materialize.js +49 -15
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +601 -30
- package/src/actions/marks/remove.js +71 -1
- package/src/actions/marks/rule/actions.js +47 -10
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +74 -9
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +154 -6
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +2 -1
- package/src/actions/primitives/semanticAction.js +83 -10
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +10 -2
- package/src/actions/regression/edit.js +21 -7
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +54 -22
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +66 -11
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +4 -3
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +68 -13
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +315 -67
- package/src/actions/theme/state.js +160 -0
- package/src/actions/violinPlots/create.js +5 -2
- package/src/actions/violinPlots/edit.js +1 -0
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +6 -1
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +340 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +343 -0
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +19 -4
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/svg.js +19 -0
- package/src/selectors/datasets.js +32 -1
- package/src/selectors/layers.js +4 -0
- package/src/theme/defaults.js +101 -0
- package/types/basic.d.ts +6 -0
- package/types/index.d.ts +85 -0
- package/types/program.d.ts +910 -117
|
@@ -58,12 +58,14 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
58
58
|
category: categoryValue,
|
|
59
59
|
...(identity === undefined ? {} : { series: seriesValue }),
|
|
60
60
|
...(colorValues === undefined ? {} : { color: colorValue }),
|
|
61
|
-
rows: []
|
|
61
|
+
rows: [],
|
|
62
|
+
sourceIndices: []
|
|
62
63
|
};
|
|
63
64
|
if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
|
|
64
65
|
throw new Error("Bar color requires one categorical value within each aggregate cell.");
|
|
65
66
|
}
|
|
66
67
|
group.rows.push(rows[index]);
|
|
68
|
+
group.sourceIndices.push(index);
|
|
67
69
|
groups.set(key, group);
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -83,6 +85,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
83
85
|
y: channels.category === "y" ? group.category : value,
|
|
84
86
|
...(color === undefined ? {} : { color: aggregateColor }),
|
|
85
87
|
...(identity === undefined ? {} : { series: group.series }),
|
|
88
|
+
sourceIndices: group.sourceIndices,
|
|
86
89
|
count: group.rows.length
|
|
87
90
|
}];
|
|
88
91
|
});
|
|
@@ -45,6 +45,50 @@ export function normalizeOffsetPadding(options = {}, existing, channel = "xOffse
|
|
|
45
45
|
return cloneAndFreeze({ paddingInner, paddingOuter });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function normalizeOffsetScalePolicy(
|
|
49
|
+
options = {},
|
|
50
|
+
existing,
|
|
51
|
+
channel = "xOffset"
|
|
52
|
+
) {
|
|
53
|
+
const hasPadding = Object.hasOwn(options, "padding");
|
|
54
|
+
const hasInner = Object.hasOwn(options, "paddingInner");
|
|
55
|
+
const hasOuter = Object.hasOwn(options, "paddingOuter");
|
|
56
|
+
if (hasPadding && (hasInner || hasOuter)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${channel} padding cannot be combined with paddingInner or paddingOuter.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const padding = hasPadding ? options.padding : undefined;
|
|
62
|
+
if (hasPadding && (!Number.isFinite(padding) || padding < 0 || padding >= 1)) {
|
|
63
|
+
throw new RangeError(
|
|
64
|
+
`${channel} padding must be from 0 (inclusive) to 1 (exclusive).`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const paddingInner = hasPadding
|
|
68
|
+
? padding
|
|
69
|
+
: hasInner ? options.paddingInner : existing?.paddingInner ?? 0;
|
|
70
|
+
const paddingOuter = hasPadding
|
|
71
|
+
? padding
|
|
72
|
+
: hasOuter ? options.paddingOuter : existing?.paddingOuter ?? 0;
|
|
73
|
+
const align = Object.hasOwn(options, "align")
|
|
74
|
+
? options.align
|
|
75
|
+
: existing?.align ?? 0.5;
|
|
76
|
+
if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
|
|
77
|
+
throw new RangeError(
|
|
78
|
+
`${channel} paddingInner must be from 0 (inclusive) to 1 (exclusive).`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
|
|
82
|
+
throw new RangeError(
|
|
83
|
+
`${channel} paddingOuter must be a non-negative finite number.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isFinite(align) || align < 0 || align > 1) {
|
|
87
|
+
throw new RangeError(`${channel} align must be between 0 and 1.`);
|
|
88
|
+
}
|
|
89
|
+
return cloneAndFreeze({ paddingInner, paddingOuter, align });
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
export function resolveBarWidth(config, slotBandwidth) {
|
|
49
93
|
if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
|
|
50
94
|
throw new Error("Bar width requires a positive resolved slot bandwidth.");
|
package/src/grammar/bin.js
CHANGED
|
@@ -4,10 +4,18 @@ import {
|
|
|
4
4
|
normalizeHistogramBin,
|
|
5
5
|
resolveHistogramBins
|
|
6
6
|
} from "./histogram.js";
|
|
7
|
+
import {
|
|
8
|
+
normalizeStatisticalWeight,
|
|
9
|
+
readStatisticalWeights,
|
|
10
|
+
sumEntryWeights,
|
|
11
|
+
summarizeStatisticalWeights,
|
|
12
|
+
validateWeightedNumericFields,
|
|
13
|
+
weightedRows
|
|
14
|
+
} from "./weightedStatistics.js";
|
|
7
15
|
|
|
8
16
|
const TRANSFORM_KEYS = Object.freeze([
|
|
9
17
|
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
10
|
-
"members", "as", "resolved"
|
|
18
|
+
"members", "as", "weight", "resolved"
|
|
11
19
|
]);
|
|
12
20
|
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
13
21
|
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
@@ -50,7 +58,8 @@ export function normalizeBinTransform({
|
|
|
50
58
|
zero,
|
|
51
59
|
includeEmpty,
|
|
52
60
|
members,
|
|
53
|
-
as
|
|
61
|
+
as,
|
|
62
|
+
weight
|
|
54
63
|
} = {}) {
|
|
55
64
|
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
56
65
|
if (selected.length > 1) {
|
|
@@ -71,7 +80,10 @@ export function normalizeBinTransform({
|
|
|
71
80
|
zero: zero ?? false,
|
|
72
81
|
includeEmpty: includeEmpty ?? true,
|
|
73
82
|
members: resolvedMembers,
|
|
74
|
-
as: normalizeAs(field, as, resolvedMembers)
|
|
83
|
+
as: normalizeAs(field, as, resolvedMembers),
|
|
84
|
+
...(weight === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { weight: normalizeStatisticalWeight(weight, "Bin weight") })
|
|
75
87
|
};
|
|
76
88
|
validateBinTransform(transform);
|
|
77
89
|
return cloneAndFreeze(transform);
|
|
@@ -86,6 +98,9 @@ export function validateBinTransform(transform) {
|
|
|
86
98
|
throw new Error(`Unsupported bin transform "${transform.type}".`);
|
|
87
99
|
}
|
|
88
100
|
requireField(transform.field, "Bin field");
|
|
101
|
+
if (transform.weight !== undefined) {
|
|
102
|
+
normalizeStatisticalWeight(transform.weight, "Bin weight");
|
|
103
|
+
}
|
|
89
104
|
normalizeHistogramBin(transform.bin);
|
|
90
105
|
if (
|
|
91
106
|
transform.extent !== "auto" &&
|
|
@@ -140,30 +155,60 @@ function readValues(rows, field) {
|
|
|
140
155
|
export function deriveBinRows(rows, transform) {
|
|
141
156
|
validateBinTransform(transform);
|
|
142
157
|
const values = readValues(rows, transform.field);
|
|
158
|
+
const weights = transform.weight === undefined
|
|
159
|
+
? undefined
|
|
160
|
+
: readStatisticalWeights(rows, transform.weight, "Bin");
|
|
161
|
+
if (weights !== undefined) {
|
|
162
|
+
validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
|
|
163
|
+
}
|
|
164
|
+
const weightSummary = weights === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: summarizeStatisticalWeights(
|
|
167
|
+
weights.entries,
|
|
168
|
+
weights.definition.kind,
|
|
169
|
+
"Bin data"
|
|
170
|
+
);
|
|
171
|
+
const contributingValues = weightSummary === undefined
|
|
172
|
+
? values
|
|
173
|
+
: weightSummary.positive.map(entry => entry.row[transform.field]);
|
|
143
174
|
if (
|
|
144
175
|
transform.extent !== "auto" &&
|
|
145
|
-
|
|
176
|
+
contributingValues.some(value =>
|
|
177
|
+
value < transform.extent[0] || value > transform.extent[1]
|
|
178
|
+
)
|
|
146
179
|
) {
|
|
147
180
|
throw new RangeError("Bin extent must contain the histogram data extent.");
|
|
148
181
|
}
|
|
149
182
|
const resolved = resolveHistogramBins({
|
|
150
|
-
values,
|
|
183
|
+
values: contributingValues,
|
|
151
184
|
bin: transform.bin,
|
|
152
185
|
domain: transform.extent,
|
|
153
186
|
nice: transform.nice,
|
|
154
187
|
zero: transform.zero
|
|
155
188
|
});
|
|
156
189
|
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
157
|
-
|
|
190
|
+
const entries = weightSummary === undefined
|
|
191
|
+
? rows.map((row, index) => ({ row, index }))
|
|
192
|
+
: weightSummary.positive;
|
|
193
|
+
entries.forEach(entry => {
|
|
194
|
+
const value = entry.row[transform.field];
|
|
158
195
|
const bin = findHistogramBinIndex(value, resolved.boundaries);
|
|
159
|
-
if (bin !== -1) members[bin].push(
|
|
196
|
+
if (bin !== -1) members[bin].push(entry);
|
|
160
197
|
});
|
|
161
198
|
const output = members.flatMap((group, index) =>
|
|
162
199
|
!transform.includeEmpty && group.length === 0 ? [] : [{
|
|
163
200
|
[transform.as.lower]: resolved.boundaries[index],
|
|
164
201
|
[transform.as.upper]: resolved.boundaries[index + 1],
|
|
165
|
-
[transform.as.count]:
|
|
166
|
-
|
|
202
|
+
[transform.as.count]: weightSummary === undefined
|
|
203
|
+
? group.length
|
|
204
|
+
: sumEntryWeights(group, weightSummary, "Bin mass"),
|
|
205
|
+
...(transform.members
|
|
206
|
+
? {
|
|
207
|
+
[transform.as.members]: weightSummary === undefined
|
|
208
|
+
? group.map(entry => entry.row)
|
|
209
|
+
: weightedRows({ positive: group })
|
|
210
|
+
}
|
|
211
|
+
: {})
|
|
167
212
|
}]
|
|
168
213
|
);
|
|
169
214
|
return {
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
4
|
+
"type", "key", "groupBy", "values", "sequence", "fill", "members"
|
|
5
|
+
]);
|
|
6
|
+
const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
|
|
7
|
+
const MAX_OUTPUT_ROWS = 10_000;
|
|
8
|
+
|
|
9
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
10
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
11
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requireField(value, label) {
|
|
15
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
16
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scalarType(value, label) {
|
|
22
|
+
if (value === null) return "null";
|
|
23
|
+
if (typeof value === "string" || typeof value === "boolean") return typeof value;
|
|
24
|
+
if (typeof value === "number" && Number.isFinite(value)) return "number";
|
|
25
|
+
throw new TypeError(`${label} must be a JSON-safe scalar.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function scalarKey(value, label) {
|
|
29
|
+
const type = scalarType(value, label);
|
|
30
|
+
if (type === "number") return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
31
|
+
if (type === "string") return `string:${value.length}:${value}`;
|
|
32
|
+
return `${type}:${value}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeGroupBy(value) {
|
|
36
|
+
if (value === undefined) return [];
|
|
37
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateGroupBy(groupBy, key) {
|
|
41
|
+
if (!Array.isArray(groupBy) || groupBy.some(field =>
|
|
42
|
+
typeof field !== "string" || field.length === 0
|
|
43
|
+
)) {
|
|
44
|
+
throw new TypeError("Complete groupBy must contain field names.");
|
|
45
|
+
}
|
|
46
|
+
if (new Set(groupBy).size !== groupBy.length) {
|
|
47
|
+
throw new Error("Complete groupBy fields must be unique.");
|
|
48
|
+
}
|
|
49
|
+
if (groupBy.includes(key)) {
|
|
50
|
+
throw new Error("Complete key must be distinct from groupBy fields.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateValues(values) {
|
|
55
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
56
|
+
throw new TypeError("Complete values must be a non-empty array.");
|
|
57
|
+
}
|
|
58
|
+
let type;
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
values.forEach((value, index) => {
|
|
61
|
+
const current = scalarType(value, `Complete values[${index}]`);
|
|
62
|
+
if (type === undefined) type = current;
|
|
63
|
+
else if (type !== current) {
|
|
64
|
+
throw new TypeError("Complete values must contain one scalar type.");
|
|
65
|
+
}
|
|
66
|
+
const identity = scalarKey(value, `Complete values[${index}]`);
|
|
67
|
+
if (seen.has(identity)) throw new Error("Complete values must be unique.");
|
|
68
|
+
seen.add(identity);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateSequence(sequence) {
|
|
73
|
+
if (!isPlainObject(sequence)) {
|
|
74
|
+
throw new TypeError("Complete sequence must be a plain object.");
|
|
75
|
+
}
|
|
76
|
+
rejectUnknownKeys(sequence, SEQUENCE_KEYS, "complete sequence");
|
|
77
|
+
if (![sequence.start, sequence.end, sequence.step].every(Number.isFinite)) {
|
|
78
|
+
throw new TypeError("Complete sequence must contain finite start, end, and step values.");
|
|
79
|
+
}
|
|
80
|
+
if (sequence.start > sequence.end) {
|
|
81
|
+
throw new RangeError("Complete sequence start must be at or before end.");
|
|
82
|
+
}
|
|
83
|
+
if (sequence.step <= 0) {
|
|
84
|
+
throw new RangeError("Complete sequence step must be positive.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateFill(fill, protectedFields) {
|
|
89
|
+
if (!isPlainObject(fill)) throw new TypeError("Complete fill must be a plain object.");
|
|
90
|
+
for (const [field, value] of Object.entries(fill)) {
|
|
91
|
+
requireField(field, "Complete fill field");
|
|
92
|
+
if (protectedFields.has(field)) {
|
|
93
|
+
throw new Error(`Complete fill cannot replace protected field "${field}".`);
|
|
94
|
+
}
|
|
95
|
+
scalarType(value, `Complete fill field "${field}"`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function validateCompleteTransform(transform) {
|
|
100
|
+
if (!isPlainObject(transform)) {
|
|
101
|
+
throw new TypeError("Complete transform must be a plain object.");
|
|
102
|
+
}
|
|
103
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "complete transform");
|
|
104
|
+
if (transform.type !== "complete") {
|
|
105
|
+
throw new Error(`Unsupported complete transform "${transform.type}".`);
|
|
106
|
+
}
|
|
107
|
+
const key = requireField(transform.key, "Complete key");
|
|
108
|
+
validateGroupBy(transform.groupBy, key);
|
|
109
|
+
const hasValues = Object.hasOwn(transform, "values");
|
|
110
|
+
const hasSequence = Object.hasOwn(transform, "sequence");
|
|
111
|
+
if (hasValues && hasSequence) {
|
|
112
|
+
throw new Error("Complete values and sequence are mutually exclusive.");
|
|
113
|
+
}
|
|
114
|
+
if (hasValues) validateValues(transform.values);
|
|
115
|
+
if (hasSequence) validateSequence(transform.sequence);
|
|
116
|
+
const members = Object.hasOwn(transform, "members")
|
|
117
|
+
? requireField(transform.members, "Complete members field")
|
|
118
|
+
: undefined;
|
|
119
|
+
validateFill(transform.fill, new Set([key, ...transform.groupBy, members].filter(Boolean)));
|
|
120
|
+
return transform;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeCompleteTransform(args = {}) {
|
|
124
|
+
const transform = {
|
|
125
|
+
type: "complete",
|
|
126
|
+
key: args.key,
|
|
127
|
+
groupBy: normalizeGroupBy(args.groupBy),
|
|
128
|
+
fill: isPlainObject(args.fill) ? { ...args.fill } : args.fill ?? {}
|
|
129
|
+
};
|
|
130
|
+
if (Object.hasOwn(args, "values")) {
|
|
131
|
+
transform.values = Array.isArray(args.values) ? [...args.values] : args.values;
|
|
132
|
+
}
|
|
133
|
+
if (Object.hasOwn(args, "sequence")) {
|
|
134
|
+
transform.sequence = isPlainObject(args.sequence) ? { ...args.sequence } : args.sequence;
|
|
135
|
+
}
|
|
136
|
+
if (Object.hasOwn(args, "members")) transform.members = args.members;
|
|
137
|
+
validateCompleteTransform(transform);
|
|
138
|
+
return cloneAndFreeze(transform);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function define(record, field, value) {
|
|
142
|
+
Object.defineProperty(record, field, {
|
|
143
|
+
value, enumerable: true, writable: true, configurable: true
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sequenceDomain(sequence) {
|
|
148
|
+
const domain = [];
|
|
149
|
+
for (let index = 0; index <= MAX_OUTPUT_ROWS; index += 1) {
|
|
150
|
+
const value = sequence.start + index * sequence.step;
|
|
151
|
+
if (!Number.isFinite(value)) {
|
|
152
|
+
throw new RangeError("Complete sequence value is outside the finite numeric range.");
|
|
153
|
+
}
|
|
154
|
+
if (value > sequence.end) return domain;
|
|
155
|
+
domain.push(value === 0 ? 0 : value);
|
|
156
|
+
}
|
|
157
|
+
throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function observedDomain(rows, key) {
|
|
161
|
+
const domain = [];
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
let type;
|
|
164
|
+
rows.forEach((row, index) => {
|
|
165
|
+
const value = row[key];
|
|
166
|
+
const current = scalarType(value, `Complete key "${key}" at row ${index}`);
|
|
167
|
+
if (type === undefined) type = current;
|
|
168
|
+
else if (type !== current) {
|
|
169
|
+
throw new TypeError(`Complete key "${key}" must contain one scalar type.`);
|
|
170
|
+
}
|
|
171
|
+
const identity = scalarKey(value, `Complete key "${key}" at row ${index}`);
|
|
172
|
+
if (!seen.has(identity)) {
|
|
173
|
+
seen.add(identity);
|
|
174
|
+
domain.push(value);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return domain;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateRows(rows, transform) {
|
|
181
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
182
|
+
throw new TypeError("Complete source rows must be plain objects.");
|
|
183
|
+
}
|
|
184
|
+
const required = [transform.key, ...transform.groupBy];
|
|
185
|
+
const groupTypes = new Map();
|
|
186
|
+
const fields = [];
|
|
187
|
+
const fieldSet = new Set();
|
|
188
|
+
rows.forEach((row, index) => {
|
|
189
|
+
Object.keys(row).forEach(field => {
|
|
190
|
+
if (!fieldSet.has(field)) {
|
|
191
|
+
fieldSet.add(field);
|
|
192
|
+
fields.push(field);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
required.forEach(field => {
|
|
196
|
+
if (!Object.hasOwn(row, field)) {
|
|
197
|
+
throw new Error(`Complete source does not contain field "${field}" at row ${index}.`);
|
|
198
|
+
}
|
|
199
|
+
const type = scalarType(row[field], `Complete field "${field}" at row ${index}`);
|
|
200
|
+
const previous = groupTypes.get(field);
|
|
201
|
+
if (previous !== undefined && previous !== type) {
|
|
202
|
+
throw new TypeError(`Complete field "${field}" must contain one scalar type.`);
|
|
203
|
+
}
|
|
204
|
+
groupTypes.set(field, type);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
if (transform.members !== undefined && fieldSet.has(transform.members)) {
|
|
208
|
+
throw new Error(`Complete members field "${transform.members}" already exists.`);
|
|
209
|
+
}
|
|
210
|
+
return fields;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function groupRows(rows, transform) {
|
|
214
|
+
if (rows.length === 0) return transform.groupBy.length === 0
|
|
215
|
+
? [{ values: [], tuple: [] }]
|
|
216
|
+
: [];
|
|
217
|
+
const groups = new Map();
|
|
218
|
+
rows.forEach((row, index) => {
|
|
219
|
+
const tuple = transform.groupBy.map(field => row[field]);
|
|
220
|
+
const identity = tuple.map((value, fieldIndex) =>
|
|
221
|
+
scalarKey(value, `Complete group field "${transform.groupBy[fieldIndex]}"`)
|
|
222
|
+
).join("\0");
|
|
223
|
+
if (!groups.has(identity)) groups.set(identity, { values: [], tuple });
|
|
224
|
+
groups.get(identity).values.push({ row, index });
|
|
225
|
+
});
|
|
226
|
+
return [...groups.values()];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function syntheticRow(fields, group, keyValue, transform) {
|
|
230
|
+
const row = {};
|
|
231
|
+
for (const field of [...fields, ...Object.keys(transform.fill)]) define(row, field, null);
|
|
232
|
+
transform.groupBy.forEach((field, index) => define(row, field, group.tuple[index]));
|
|
233
|
+
define(row, transform.key, keyValue);
|
|
234
|
+
for (const [field, value] of Object.entries(transform.fill)) define(row, field, value);
|
|
235
|
+
if (transform.members !== undefined) define(row, transform.members, []);
|
|
236
|
+
return row;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function deriveCompleteRows(rows, transform) {
|
|
240
|
+
validateCompleteTransform(transform);
|
|
241
|
+
const fields = validateRows(rows, transform);
|
|
242
|
+
const domain = transform.values !== undefined
|
|
243
|
+
? [...transform.values]
|
|
244
|
+
: transform.sequence !== undefined
|
|
245
|
+
? sequenceDomain(transform.sequence)
|
|
246
|
+
: observedDomain(rows, transform.key);
|
|
247
|
+
if (domain.length > 0) validateValues(domain);
|
|
248
|
+
const allowed = new Set(domain.map(value => scalarKey(value, "Complete domain value")));
|
|
249
|
+
const groups = groupRows(rows, transform);
|
|
250
|
+
if (groups.length !== 0 && domain.length > Math.floor(MAX_OUTPUT_ROWS / groups.length)) {
|
|
251
|
+
throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
252
|
+
}
|
|
253
|
+
const output = [];
|
|
254
|
+
for (const group of groups) {
|
|
255
|
+
const lookup = new Map();
|
|
256
|
+
for (const entry of group.values) {
|
|
257
|
+
const identity = scalarKey(entry.row[transform.key], `Complete key "${transform.key}"`);
|
|
258
|
+
if (!allowed.has(identity)) {
|
|
259
|
+
throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
|
|
260
|
+
}
|
|
261
|
+
if (lookup.has(identity)) {
|
|
262
|
+
throw new Error("Complete source contains a duplicate group and key combination.");
|
|
263
|
+
}
|
|
264
|
+
lookup.set(identity, entry);
|
|
265
|
+
}
|
|
266
|
+
for (const keyValue of domain) {
|
|
267
|
+
const identity = scalarKey(keyValue, "Complete domain value");
|
|
268
|
+
const entry = lookup.get(identity);
|
|
269
|
+
if (entry === undefined) {
|
|
270
|
+
output.push(syntheticRow(fields, group, keyValue, transform));
|
|
271
|
+
} else {
|
|
272
|
+
const row = { ...entry.row };
|
|
273
|
+
if (transform.members !== undefined) define(row, transform.members, [entry.index]);
|
|
274
|
+
output.push(row);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return cloneAndFreeze(output);
|
|
279
|
+
}
|