ggaction 0.0.12 → 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 +127 -0
- package/README.md +20 -11
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -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 -3
- package/src/actions/basic.js +4 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +13 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +18 -6
- package/src/actions/charts/area.js +51 -0
- package/src/actions/charts/bar.js +13 -6
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +48 -0
- package/src/actions/charts/ecdf.js +186 -0
- package/src/actions/charts/endpoints.js +473 -0
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +73 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +172 -0
- package/src/actions/charts/line.js +8 -6
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +52 -0
- package/src/actions/charts/polar.js +134 -0
- package/src/actions/charts/radar.js +305 -0
- package/src/actions/charts/radial.js +50 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +283 -0
- package/src/actions/charts/scatter.js +11 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +129 -3
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +111 -15
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +226 -30
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +38 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +19 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +30 -23
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +124 -34
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +6 -0
- package/src/actions/encodings/offset.js +30 -29
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +63 -11
- package/src/actions/encodings/position/index.js +52 -6
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +57 -13
- package/src/actions/encodings/position/resolve.js +88 -26
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +44 -34
- package/src/actions/encodings/ruleAppearance.js +24 -32
- package/src/actions/encodings/seriesLayout.js +105 -0
- package/src/actions/encodings/shared.js +107 -37
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +41 -8
- package/src/actions/errorBands/edit.js +360 -25
- package/src/actions/errorBars/create.js +56 -17
- package/src/actions/errorBars/edit.js +372 -21
- package/src/actions/facets/actions.js +619 -41
- package/src/actions/facets/derive.js +139 -25
- package/src/actions/gradientPlots/components.js +43 -13
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +8 -8
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +28 -19
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +184 -28
- package/src/actions/guides/axes/lines.js +13 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +50 -25
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +15 -8
- package/src/actions/guides/axes/ticks.js +11 -6
- package/src/actions/guides/axes/titles.js +32 -11
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +17 -4
- package/src/actions/guides/guides.js +54 -31
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +309 -279
- package/src/actions/guides/legends/categorical/components.js +21 -17
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +123 -354
- package/src/actions/guides/legends/categorical/options.js +32 -16
- package/src/actions/guides/legends/categorical/recipes.js +114 -9
- package/src/actions/guides/legends/categorical/resolve.js +30 -15
- package/src/actions/guides/legends/categorical/symbols.js +87 -33
- package/src/actions/guides/legends/continuous/common.js +99 -26
- package/src/actions/guides/legends/continuous/gradient.js +120 -89
- package/src/actions/guides/legends/continuous/index.js +8 -2
- package/src/actions/guides/legends/continuous/interval.js +79 -95
- package/src/actions/guides/legends/continuous/opacity.js +113 -89
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +40 -0
- package/src/actions/guides/legends/edit.js +321 -90
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +149 -102
- package/src/actions/guides/legends/lifecycle.js +143 -0
- package/src/actions/guides/legends/remove.js +43 -78
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +213 -116
- package/src/actions/guides/legends/strokeWidth.js +121 -73
- package/src/actions/guides/legends/target.js +123 -0
- package/src/actions/guides/legends/transition.js +192 -0
- package/src/actions/guides/polar/axes/facade.js +81 -66
- package/src/actions/guides/polar/axes/labels.js +28 -8
- package/src/actions/guides/polar/axes/lines.js +12 -5
- package/src/actions/guides/polar/axes/shared.js +22 -0
- package/src/actions/guides/polar/axes/ticks.js +6 -5
- package/src/actions/guides/polar/axes/titles.js +6 -5
- package/src/actions/guides/polar/grids.js +8 -8
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +15 -2
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +78 -20
- package/src/actions/marks/area/actions.js +51 -12
- package/src/actions/marks/area/materialize.js +22 -3
- 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 +33 -9
- package/src/actions/marks/index.js +14 -0
- package/src/actions/marks/line/actions.js +71 -113
- package/src/actions/marks/line/materialize.js +57 -56
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +29 -21
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +154 -16
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +658 -0
- package/src/actions/marks/remove.js +87 -4
- package/src/actions/marks/rule/actions.js +77 -9
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +13 -13
- package/src/actions/marks/text/actions.js +296 -13
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +156 -8
- 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/createGraphics.js +3 -1
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +3 -2
- package/src/actions/primitives/semanticAction.js +120 -10
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +23 -1
- package/src/actions/primitives/semanticValidation/scale.js +38 -8
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +11 -3
- package/src/actions/regression/edit.js +40 -13
- package/src/actions/regression/resolve.js +1 -1
- 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 +21 -54
- package/src/actions/scales/consumers/families.js +28 -14
- package/src/actions/scales/consumers/index.js +42 -4
- package/src/actions/scales/consumers/seriesLayout.js +57 -12
- package/src/actions/scales/create.js +58 -26
- package/src/actions/scales/definitions.js +109 -39
- package/src/actions/scales/edit.js +101 -34
- package/src/actions/scales/editPolicy.js +95 -35
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +14 -49
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +140 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +98 -0
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +16 -0
- package/src/actions/theme/reconcile.js +1130 -0
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +90 -43
- package/src/actions/violinPlots/edit.js +192 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +32 -2
- package/src/core/compositionState.js +141 -8
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +37 -13
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +226 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +351 -0
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +123 -52
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/impute.js +340 -0
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +110 -0
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +109 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +8 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +63 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +8 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +57 -0
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +42 -5
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +1 -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 +23 -4
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +245 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +234 -13
- package/src/grammar/transformTopology.js +10 -0
- package/src/grammar/transforms.js +418 -16
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +232 -21
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +100 -12
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/labels.js +343 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +56 -147
- package/src/materialization/bars/histogram.js +104 -20
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/composition.js +3 -1
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +93 -16
- package/src/materialization/encodings.js +1 -1
- 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 +23 -66
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +28 -2
- package/src/materialization/legends.js +11 -2
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +68 -27
- package/src/materialization/marks/policies.js +4 -1
- package/src/materialization/rect.js +55 -42
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +6 -3
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +5 -1
- package/src/materialization/scales/resolve.js +65 -17
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +42 -71
- package/src/materialization/selection/items/common.js +27 -6
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +35 -8
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +324 -14
- package/src/mcp/adapter.js +2 -2
- 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 +44 -0
- package/src/selectors/layers.js +7 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +140 -0
- package/types/basic.d.ts +17 -2
- package/types/index.d.ts +211 -0
- package/types/program.d.ts +2182 -204
- package/src/materialization/bars/grouped.js +0 -144
package/src/grammar/density.js
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
requireFiniteResult,
|
|
11
11
|
stableFiniteDeviation
|
|
12
12
|
} from "./numeric.js";
|
|
13
|
+
import {
|
|
14
|
+
normalizeStatisticalWeight,
|
|
15
|
+
readStatisticalWeights,
|
|
16
|
+
summarizeStatisticalWeights,
|
|
17
|
+
validateWeightedNumericFields,
|
|
18
|
+
weightedBandwidth,
|
|
19
|
+
weightedEntriesExtent,
|
|
20
|
+
weightedKernelEstimate
|
|
21
|
+
} from "./weightedStatistics.js";
|
|
13
22
|
|
|
14
23
|
const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
|
|
15
24
|
|
|
@@ -37,6 +46,25 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
|
|
|
37
46
|
"shared", "independent"
|
|
38
47
|
]);
|
|
39
48
|
|
|
49
|
+
export function normalizeDensityTransform(args = {}) {
|
|
50
|
+
const transform = {
|
|
51
|
+
type: "density",
|
|
52
|
+
field: args.field,
|
|
53
|
+
...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
|
|
54
|
+
bandwidth: args.bandwidth ?? "auto",
|
|
55
|
+
extent: args.extent ?? "auto",
|
|
56
|
+
steps: args.steps ?? 100,
|
|
57
|
+
kernel: args.kernel ?? "gaussian",
|
|
58
|
+
normalization: args.normalization ?? "unit",
|
|
59
|
+
as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
|
|
60
|
+
resolve: args.resolve ?? "shared",
|
|
61
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
62
|
+
...(args.placement === undefined ? {} : { placement: args.placement })
|
|
63
|
+
};
|
|
64
|
+
validateDensityTransform(transform);
|
|
65
|
+
return cloneAndFreeze(transform);
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
export function validateDensitySteps(value, label = "Density steps") {
|
|
41
69
|
if (!Number.isInteger(value) || value < 2) {
|
|
42
70
|
throw new RangeError(`${label} must be an integer of at least 2.`);
|
|
@@ -261,10 +289,88 @@ function validateStoredDensityPlacement(value, groupBy) {
|
|
|
261
289
|
return value;
|
|
262
290
|
}
|
|
263
291
|
|
|
292
|
+
function validateResolvedBandwidths(value, transform) {
|
|
293
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
294
|
+
throw new TypeError(
|
|
295
|
+
"Density resolved bandwidths must be a non-empty array."
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const normalized = value.map((item, index) => {
|
|
299
|
+
if (!isPlainObject(item)) {
|
|
300
|
+
throw new TypeError(
|
|
301
|
+
`Density resolved bandwidth ${index} must be a plain object.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const unknown = Object.keys(item).find(
|
|
305
|
+
key => !["group", "split", "bandwidth"].includes(key)
|
|
306
|
+
);
|
|
307
|
+
if (unknown !== undefined) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Unknown density resolved bandwidth property "${unknown}".`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isFinite(item.bandwidth) || item.bandwidth <= 0) {
|
|
313
|
+
throw new RangeError(
|
|
314
|
+
`Density resolved bandwidth ${index} must be positive and finite.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (transform.groupBy === undefined) {
|
|
318
|
+
if (Object.hasOwn(item, "group")) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Density resolved bandwidth cannot store a group without groupBy."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else if (!Object.hasOwn(item, "group") || !isNominalValue(item.group)) {
|
|
324
|
+
throw new TypeError(
|
|
325
|
+
`Density resolved bandwidth ${index} requires a nominal group.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (transform.placement?.split === undefined) {
|
|
329
|
+
if (Object.hasOwn(item, "split")) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"Density resolved bandwidth cannot store a split without split placement."
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} else if (!Object.hasOwn(item, "split") || !isNominalValue(item.split)) {
|
|
335
|
+
throw new TypeError(
|
|
336
|
+
`Density resolved bandwidth ${index} requires a nominal split.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return item;
|
|
340
|
+
});
|
|
341
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
342
|
+
const duplicate = normalized.slice(0, index).some(item =>
|
|
343
|
+
Object.is(item.group, normalized[index].group) &&
|
|
344
|
+
Object.is(item.split, normalized[index].split)
|
|
345
|
+
);
|
|
346
|
+
if (duplicate) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Density resolved bandwidth ${index} duplicates an earlier profile.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function hasValidResolvedBandwidthState(resolved, transform) {
|
|
356
|
+
const hasBandwidth = Object.hasOwn(resolved, "bandwidth");
|
|
357
|
+
const hasBandwidths = Object.hasOwn(resolved, "bandwidths");
|
|
358
|
+
if (hasBandwidth === hasBandwidths) return false;
|
|
359
|
+
if (hasBandwidth) {
|
|
360
|
+
return Number.isFinite(resolved.bandwidth) && resolved.bandwidth > 0;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
validateResolvedBandwidths(resolved.bandwidths, transform);
|
|
364
|
+
return true;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
264
370
|
export function validateDensityTransform(transform) {
|
|
265
371
|
const supported = [
|
|
266
372
|
"type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
|
|
267
|
-
"resolve", "kernel", "normalization", "placement", "resolved"
|
|
373
|
+
"resolve", "kernel", "normalization", "placement", "weight", "resolved"
|
|
268
374
|
];
|
|
269
375
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
270
376
|
if (unknown !== undefined) {
|
|
@@ -274,6 +380,9 @@ export function validateDensityTransform(transform) {
|
|
|
274
380
|
throw new Error(`Unsupported density transform "${transform.type}".`);
|
|
275
381
|
}
|
|
276
382
|
requireField(transform.field, "Density field");
|
|
383
|
+
if (transform.weight !== undefined) {
|
|
384
|
+
normalizeStatisticalWeight(transform.weight, "Density weight");
|
|
385
|
+
}
|
|
277
386
|
if (transform.groupBy !== undefined) {
|
|
278
387
|
requireField(transform.groupBy, "Density groupBy");
|
|
279
388
|
}
|
|
@@ -301,10 +410,9 @@ export function validateDensityTransform(transform) {
|
|
|
301
410
|
typeof resolved !== "object" ||
|
|
302
411
|
Array.isArray(resolved) ||
|
|
303
412
|
Object.keys(resolved).some(
|
|
304
|
-
key => !["bandwidth", "extent", "splitDomain"].includes(key)
|
|
413
|
+
key => !["bandwidth", "bandwidths", "extent", "splitDomain"].includes(key)
|
|
305
414
|
) ||
|
|
306
|
-
!
|
|
307
|
-
resolved.bandwidth <= 0 ||
|
|
415
|
+
!hasValidResolvedBandwidthState(resolved, transform) ||
|
|
308
416
|
!Array.isArray(resolved.extent) ||
|
|
309
417
|
resolved.extent.length !== 2 ||
|
|
310
418
|
!resolved.extent.every(Number.isFinite) ||
|
|
@@ -319,7 +427,7 @@ export function validateDensityTransform(transform) {
|
|
|
319
427
|
})())
|
|
320
428
|
) {
|
|
321
429
|
throw new TypeError(
|
|
322
|
-
"Density resolved provenance requires
|
|
430
|
+
"Density resolved provenance requires one positive bandwidth or profile bandwidth list, an ascending finite extent, and an optional two-value split domain."
|
|
323
431
|
);
|
|
324
432
|
}
|
|
325
433
|
}
|
|
@@ -363,22 +471,26 @@ export function estimateDensityBandwidth(values) {
|
|
|
363
471
|
return bandwidth;
|
|
364
472
|
}
|
|
365
473
|
|
|
366
|
-
function resolveBandwidth(value, sourceValues) {
|
|
474
|
+
function resolveBandwidth(value, sourceValues, weightSummary, field) {
|
|
367
475
|
if (value === undefined || value === "auto") {
|
|
368
|
-
return
|
|
476
|
+
return weightSummary === undefined
|
|
477
|
+
? estimateDensityBandwidth(sourceValues)
|
|
478
|
+
: weightedBandwidth(weightSummary, field);
|
|
369
479
|
}
|
|
370
480
|
return validateDensityBandwidth(value);
|
|
371
481
|
}
|
|
372
482
|
|
|
373
|
-
function resolveExtent(value, sourceValues) {
|
|
483
|
+
function resolveExtent(value, sourceValues, weightSummary, field) {
|
|
374
484
|
if (value === undefined || value === "auto") {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
485
|
+
const extent = weightSummary === undefined
|
|
486
|
+
? sourceValues.reduce(
|
|
487
|
+
([lower, upper], sourceValue) => [
|
|
488
|
+
Math.min(lower, sourceValue),
|
|
489
|
+
Math.max(upper, sourceValue)
|
|
490
|
+
],
|
|
491
|
+
[Infinity, -Infinity]
|
|
492
|
+
)
|
|
493
|
+
: weightedEntriesExtent(weightSummary, field);
|
|
382
494
|
if (extent[0] === extent[1]) {
|
|
383
495
|
throw new Error("Density observed extent requires varying finite values.");
|
|
384
496
|
}
|
|
@@ -407,6 +519,17 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
|
|
|
407
519
|
return requireFiniteResult(stable, "Density estimate");
|
|
408
520
|
}
|
|
409
521
|
|
|
522
|
+
function estimateWeightedAt(sample, summary, field, bandwidth, kernel, normalization) {
|
|
523
|
+
return weightedKernelEstimate(
|
|
524
|
+
summary,
|
|
525
|
+
field,
|
|
526
|
+
sample,
|
|
527
|
+
bandwidth,
|
|
528
|
+
KERNEL_FUNCTIONS[kernel],
|
|
529
|
+
normalization
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
410
533
|
export function deriveKernelDensity(values, {
|
|
411
534
|
field,
|
|
412
535
|
groupBy,
|
|
@@ -416,7 +539,8 @@ export function deriveKernelDensity(values, {
|
|
|
416
539
|
kernel = "gaussian",
|
|
417
540
|
normalization = "unit",
|
|
418
541
|
as,
|
|
419
|
-
placement
|
|
542
|
+
placement,
|
|
543
|
+
weight
|
|
420
544
|
} = {}) {
|
|
421
545
|
if (!Array.isArray(values)) {
|
|
422
546
|
throw new TypeError("Density values must be an array.");
|
|
@@ -437,29 +561,66 @@ export function deriveKernelDensity(values, {
|
|
|
437
561
|
)];
|
|
438
562
|
const resolvedKernel = validateDensityKernel(kernel);
|
|
439
563
|
const resolvedNormalization = validateDensityNormalization(normalization);
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
(
|
|
445
|
-
|
|
446
|
-
|
|
564
|
+
const statisticalWeights = weight === undefined
|
|
565
|
+
? undefined
|
|
566
|
+
: readStatisticalWeights(values, weight, "Density");
|
|
567
|
+
if (statisticalWeights !== undefined) {
|
|
568
|
+
validateWeightedNumericFields(
|
|
569
|
+
statisticalWeights.entries,
|
|
570
|
+
[sourceField],
|
|
571
|
+
"Density"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const validEntries = statisticalWeights === undefined
|
|
575
|
+
? undefined
|
|
576
|
+
: statisticalWeights.entries.filter(entry =>
|
|
577
|
+
(groupField === undefined || isNominalValue(entry.row[groupField])) &&
|
|
578
|
+
(placement?.split === undefined ||
|
|
579
|
+
isNominalValue(entry.row[placement.split.field]))
|
|
580
|
+
);
|
|
581
|
+
const validRows = statisticalWeights === undefined
|
|
582
|
+
? values.filter(row =>
|
|
583
|
+
row !== null &&
|
|
584
|
+
typeof row === "object" &&
|
|
585
|
+
Number.isFinite(row[sourceField]) &&
|
|
586
|
+
(groupField === undefined || isNominalValue(row[groupField])) &&
|
|
587
|
+
(placement?.split === undefined || isNominalValue(row[placement.split.field]))
|
|
588
|
+
)
|
|
589
|
+
: validEntries.map(entry => entry.row);
|
|
447
590
|
if (validRows.length === 0) {
|
|
448
591
|
throw new Error("Density requires at least one valid field/group row.");
|
|
449
592
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
593
|
+
const globalWeightSummary = statisticalWeights === undefined
|
|
594
|
+
? undefined
|
|
595
|
+
: summarizeStatisticalWeights(
|
|
596
|
+
validEntries,
|
|
597
|
+
statisticalWeights.definition.kind,
|
|
598
|
+
"Density data"
|
|
599
|
+
);
|
|
600
|
+
validateWorkLimit(
|
|
601
|
+
(globalWeightSummary?.positive.length ?? validRows.length) * steps,
|
|
602
|
+
"Density computation"
|
|
603
|
+
);
|
|
604
|
+
const sourceValues = globalWeightSummary === undefined
|
|
605
|
+
? validRows.map(row => row[sourceField])
|
|
606
|
+
: globalWeightSummary.positive.map(entry => entry.row[sourceField]);
|
|
607
|
+
const resolvedExtent = resolveExtent(
|
|
608
|
+
extent,
|
|
609
|
+
sourceValues,
|
|
610
|
+
globalWeightSummary,
|
|
611
|
+
sourceField
|
|
612
|
+
);
|
|
454
613
|
const groupedValues = new Map();
|
|
455
|
-
|
|
614
|
+
const groupedSource = statisticalWeights === undefined ? validRows : validEntries;
|
|
615
|
+
for (const source of groupedSource) {
|
|
616
|
+
const row = statisticalWeights === undefined ? source : source.row;
|
|
456
617
|
const group = groupField === undefined ? undefined : row[groupField];
|
|
457
618
|
const split = placement?.split === undefined
|
|
458
619
|
? undefined
|
|
459
620
|
: row[placement.split.field];
|
|
460
621
|
const bySplit = groupedValues.get(group) ?? new Map();
|
|
461
622
|
const groupValues = bySplit.get(split) ?? [];
|
|
462
|
-
groupValues.push(row[sourceField]);
|
|
623
|
+
groupValues.push(statisticalWeights === undefined ? row[sourceField] : source);
|
|
463
624
|
bySplit.set(split, groupValues);
|
|
464
625
|
groupedValues.set(group, bySplit);
|
|
465
626
|
}
|
|
@@ -492,6 +653,16 @@ export function deriveKernelDensity(values, {
|
|
|
492
653
|
outputGroups * steps,
|
|
493
654
|
"Density generated row count"
|
|
494
655
|
);
|
|
656
|
+
const profileBandwidths = statisticalWeights !== undefined &&
|
|
657
|
+
bandwidth === "auto" && outputGroups > 1;
|
|
658
|
+
const resolvedBandwidth = profileBandwidths
|
|
659
|
+
? undefined
|
|
660
|
+
: resolveBandwidth(
|
|
661
|
+
bandwidth,
|
|
662
|
+
sourceValues,
|
|
663
|
+
globalWeightSummary,
|
|
664
|
+
sourceField
|
|
665
|
+
);
|
|
495
666
|
const extentSpan = resolvedExtent[1] - resolvedExtent[0];
|
|
496
667
|
const sampleStep = extentSpan / (steps - 1);
|
|
497
668
|
const samples = Array.from(
|
|
@@ -516,10 +687,30 @@ export function deriveKernelDensity(values, {
|
|
|
516
687
|
);
|
|
517
688
|
}
|
|
518
689
|
const rows = [];
|
|
690
|
+
const resolvedBandwidths = [];
|
|
519
691
|
for (const group of groups) {
|
|
520
692
|
for (const split of splits) {
|
|
521
693
|
const groupValues = groupedValues.get(group).get(split) ?? [];
|
|
522
694
|
if (groupValues.length === 0) continue;
|
|
695
|
+
const groupWeightSummary = statisticalWeights === undefined
|
|
696
|
+
? undefined
|
|
697
|
+
: summarizeStatisticalWeights(
|
|
698
|
+
groupValues,
|
|
699
|
+
statisticalWeights.definition.kind,
|
|
700
|
+
"Density group"
|
|
701
|
+
);
|
|
702
|
+
const groupBandwidth = resolvedBandwidth ?? weightedBandwidth(
|
|
703
|
+
groupWeightSummary,
|
|
704
|
+
sourceField,
|
|
705
|
+
"Density group auto bandwidth"
|
|
706
|
+
);
|
|
707
|
+
if (profileBandwidths) {
|
|
708
|
+
resolvedBandwidths.push({
|
|
709
|
+
...(groupField === undefined ? {} : { group }),
|
|
710
|
+
...(placement?.split === undefined ? {} : { split }),
|
|
711
|
+
bandwidth: groupBandwidth
|
|
712
|
+
});
|
|
713
|
+
}
|
|
523
714
|
for (const sample of samples) {
|
|
524
715
|
rows.push({
|
|
525
716
|
...(groupField === undefined
|
|
@@ -531,13 +722,22 @@ export function deriveKernelDensity(values, {
|
|
|
531
722
|
? {}
|
|
532
723
|
: { [placement.split.field]: split }),
|
|
533
724
|
[outputFields[0]]: sample,
|
|
534
|
-
[outputFields[1]]:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
725
|
+
[outputFields[1]]: groupWeightSummary === undefined
|
|
726
|
+
? estimateAt(
|
|
727
|
+
sample,
|
|
728
|
+
groupValues,
|
|
729
|
+
groupBandwidth,
|
|
730
|
+
resolvedKernel,
|
|
731
|
+
resolvedNormalization
|
|
732
|
+
)
|
|
733
|
+
: estimateWeightedAt(
|
|
734
|
+
sample,
|
|
735
|
+
groupWeightSummary,
|
|
736
|
+
sourceField,
|
|
737
|
+
groupBandwidth,
|
|
738
|
+
resolvedKernel,
|
|
739
|
+
resolvedNormalization
|
|
740
|
+
)
|
|
541
741
|
});
|
|
542
742
|
}
|
|
543
743
|
}
|
|
@@ -551,7 +751,9 @@ export function deriveKernelDensity(values, {
|
|
|
551
751
|
},
|
|
552
752
|
groups,
|
|
553
753
|
...(splitDomain === undefined ? {} : { splitDomain }),
|
|
554
|
-
|
|
754
|
+
...(resolvedBandwidth === undefined
|
|
755
|
+
? { bandwidths: resolvedBandwidths }
|
|
756
|
+
: { bandwidth: resolvedBandwidth }),
|
|
555
757
|
kernel: resolvedKernel,
|
|
556
758
|
normalization: resolvedNormalization,
|
|
557
759
|
extent: resolvedExtent,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const ENTRY_KEYS = Object.freeze(["value", "label"]);
|
|
4
|
+
|
|
5
|
+
export function sameDisplayLabelValue(left, right) {
|
|
6
|
+
return left === right;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isDisplayLabelValue(value) {
|
|
10
|
+
return value === null || typeof value === "string" ||
|
|
11
|
+
typeof value === "boolean" ||
|
|
12
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeDisplayLabelMap(value, label = "labelMap") {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
throw new TypeError(`${label} must be an array.`);
|
|
18
|
+
}
|
|
19
|
+
const normalized = [];
|
|
20
|
+
for (const [index, entry] of value.entries()) {
|
|
21
|
+
if (!isPlainObject(entry)) {
|
|
22
|
+
throw new TypeError(`${label}[${index}] must be a plain object.`);
|
|
23
|
+
}
|
|
24
|
+
const keys = Reflect.ownKeys(entry);
|
|
25
|
+
if (keys.length !== ENTRY_KEYS.length ||
|
|
26
|
+
ENTRY_KEYS.some(key => !Object.hasOwn(entry, key))) {
|
|
27
|
+
throw new Error(`${label}[${index}] must contain exactly value and label.`);
|
|
28
|
+
}
|
|
29
|
+
if (!isDisplayLabelValue(entry.value)) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`${label}[${index}].value must be a string, boolean, null, or finite number.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (typeof entry.label !== "string") {
|
|
35
|
+
throw new TypeError(`${label}[${index}].label must be a string.`);
|
|
36
|
+
}
|
|
37
|
+
if (normalized.some(previous =>
|
|
38
|
+
sameDisplayLabelValue(previous.value, entry.value))) {
|
|
39
|
+
throw new Error(`${label} contains a duplicate typed value.`);
|
|
40
|
+
}
|
|
41
|
+
normalized.push(Object.freeze({ value: entry.value, label: entry.label }));
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(normalized);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveDisplayLabel(value, map, fallback) {
|
|
47
|
+
if (map !== undefined) {
|
|
48
|
+
for (const entry of map) {
|
|
49
|
+
if (sameDisplayLabelValue(entry.value, value)) return entry.label;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const resolved = fallback(value);
|
|
53
|
+
if (typeof resolved !== "string") {
|
|
54
|
+
throw new TypeError("Display label fallback must return a string.");
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { stableDecimal } from "./numeric.js";
|
|
3
|
+
|
|
4
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
5
|
+
"type", "field", "groupBy", "weight", "missing", "as", "resolved"
|
|
6
|
+
]);
|
|
7
|
+
const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
|
|
8
|
+
|
|
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
|
+
|
|
15
|
+
function normalizeGroupBy(groupBy) {
|
|
16
|
+
const fields = groupBy === undefined
|
|
17
|
+
? []
|
|
18
|
+
: Array.isArray(groupBy) ? [...groupBy] : [groupBy];
|
|
19
|
+
fields.forEach(field => nonEmptyString(field, "ECDF groupBy field"));
|
|
20
|
+
if (new Set(fields).size !== fields.length) {
|
|
21
|
+
throw new Error("ECDF groupBy fields must be distinct.");
|
|
22
|
+
}
|
|
23
|
+
return fields;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateOutputs(as, occupied) {
|
|
27
|
+
if (!isPlainObject(as)) throw new TypeError("ECDF as must be a plain object.");
|
|
28
|
+
const keys = Object.keys(as);
|
|
29
|
+
if (keys.length !== OUTPUT_KEYS.length ||
|
|
30
|
+
!OUTPUT_KEYS.every(key => Object.hasOwn(as, key))) {
|
|
31
|
+
throw new Error("ECDF as requires exactly value, cumulative, and probability.");
|
|
32
|
+
}
|
|
33
|
+
const values = OUTPUT_KEYS.map(key => as[key]);
|
|
34
|
+
values.forEach((field, index) => nonEmptyString(field, `ECDF as.${OUTPUT_KEYS[index]}`));
|
|
35
|
+
if (new Set(values).size !== values.length) {
|
|
36
|
+
throw new Error("ECDF output fields must be distinct.");
|
|
37
|
+
}
|
|
38
|
+
if (values.some(field => occupied.has(field))) {
|
|
39
|
+
throw new Error("ECDF output fields must not collide with input or grouping fields.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateResolved(resolved, groupBy) {
|
|
44
|
+
if (resolved === undefined) return;
|
|
45
|
+
if (!isPlainObject(resolved) || !Array.isArray(resolved.groups)) {
|
|
46
|
+
throw new TypeError("ECDF resolved provenance must contain a groups array.");
|
|
47
|
+
}
|
|
48
|
+
for (const group of resolved.groups) {
|
|
49
|
+
if (!isPlainObject(group) || !isPlainObject(group.keys) ||
|
|
50
|
+
!Number.isFinite(group.denominator) || group.denominator <= 0 ||
|
|
51
|
+
!Number.isInteger(group.validCount) || group.validCount <= 0 ||
|
|
52
|
+
Object.keys(group.keys).length !== groupBy.length ||
|
|
53
|
+
!groupBy.every(field => Object.hasOwn(group.keys, field))) {
|
|
54
|
+
throw new TypeError("ECDF resolved group provenance is invalid.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateECDFTransform(transform) {
|
|
60
|
+
if (!isPlainObject(transform)) {
|
|
61
|
+
throw new TypeError("ECDF transform must be a plain object.");
|
|
62
|
+
}
|
|
63
|
+
const unknown = Object.keys(transform).find(key => !TRANSFORM_KEYS.includes(key));
|
|
64
|
+
if (unknown !== undefined) {
|
|
65
|
+
throw new Error(`Unknown ECDF transform property "${unknown}".`);
|
|
66
|
+
}
|
|
67
|
+
if (transform.type !== "ecdf") {
|
|
68
|
+
throw new Error(`Unsupported ECDF transform "${transform.type}".`);
|
|
69
|
+
}
|
|
70
|
+
nonEmptyString(transform.field, "ECDF field");
|
|
71
|
+
const groupBy = normalizeGroupBy(transform.groupBy);
|
|
72
|
+
if (groupBy.includes(transform.field)) {
|
|
73
|
+
throw new Error("ECDF field must not also be a grouping field.");
|
|
74
|
+
}
|
|
75
|
+
if (transform.weight !== undefined) {
|
|
76
|
+
nonEmptyString(transform.weight, "ECDF weight field");
|
|
77
|
+
if (transform.weight === transform.field || groupBy.includes(transform.weight)) {
|
|
78
|
+
throw new Error("ECDF weight field must be distinct from value and grouping fields.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!["drop", "error"].includes(transform.missing)) {
|
|
82
|
+
throw new Error('ECDF missing must be "drop" or "error".');
|
|
83
|
+
}
|
|
84
|
+
validateOutputs(
|
|
85
|
+
transform.as,
|
|
86
|
+
new Set([transform.field, transform.weight, ...groupBy].filter(Boolean))
|
|
87
|
+
);
|
|
88
|
+
validateResolved(transform.resolved, groupBy);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop", as }) {
|
|
92
|
+
const transform = {
|
|
93
|
+
type: "ecdf",
|
|
94
|
+
field,
|
|
95
|
+
groupBy: normalizeGroupBy(groupBy),
|
|
96
|
+
...(weight === undefined ? {} : { weight }),
|
|
97
|
+
missing,
|
|
98
|
+
as
|
|
99
|
+
};
|
|
100
|
+
validateECDFTransform(transform);
|
|
101
|
+
return cloneAndFreeze(transform);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function groupValue(value) {
|
|
105
|
+
return typeof value === "string" || typeof value === "boolean" ||
|
|
106
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function rejectOrDrop(transform, index, reason) {
|
|
110
|
+
if (transform.missing === "error") {
|
|
111
|
+
throw new TypeError(`ECDF row ${index} ${reason}.`);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function deriveECDF(rows, transform) {
|
|
117
|
+
if (!Array.isArray(rows)) throw new TypeError("ECDF rows must be an array.");
|
|
118
|
+
validateECDFTransform(transform);
|
|
119
|
+
const groups = new Map();
|
|
120
|
+
|
|
121
|
+
rows.forEach((row, index) => {
|
|
122
|
+
if (!isPlainObject(row)) {
|
|
123
|
+
rejectOrDrop(transform, index, "must be a plain object");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const value = row[transform.field];
|
|
127
|
+
if (!Number.isFinite(value)) {
|
|
128
|
+
rejectOrDrop(transform, index, `has an invalid value in "${transform.field}"`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const keys = transform.groupBy.map(field => row[field]);
|
|
132
|
+
if (!keys.every(groupValue)) {
|
|
133
|
+
rejectOrDrop(transform, index, "has an invalid grouping value");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const weight = transform.weight === undefined ? 1 : row[transform.weight];
|
|
137
|
+
if (!Number.isFinite(weight)) {
|
|
138
|
+
rejectOrDrop(transform, index, `has an invalid weight in "${transform.weight}"`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (weight < 0) {
|
|
142
|
+
throw new RangeError(`ECDF row ${index} has a negative weight.`);
|
|
143
|
+
}
|
|
144
|
+
const key = JSON.stringify(keys);
|
|
145
|
+
if (!groups.has(key)) {
|
|
146
|
+
groups.set(key, {
|
|
147
|
+
keys: Object.fromEntries(transform.groupBy.map((field, offset) => [field, keys[offset]])),
|
|
148
|
+
values: new Map(),
|
|
149
|
+
denominator: 0,
|
|
150
|
+
validCount: 0
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const group = groups.get(key);
|
|
154
|
+
if (weight === 0) return;
|
|
155
|
+
group.values.set(value, (group.values.get(value) ?? 0) + weight);
|
|
156
|
+
group.denominator += weight;
|
|
157
|
+
group.validCount += 1;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (groups.size === 0) {
|
|
161
|
+
throw new RangeError("ECDF denominator must be greater than zero.");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const values = [];
|
|
165
|
+
const provenance = [];
|
|
166
|
+
for (const group of groups.values()) {
|
|
167
|
+
if (!(group.denominator > 0) || !Number.isFinite(group.denominator)) {
|
|
168
|
+
throw new RangeError("ECDF denominator must be finite and greater than zero.");
|
|
169
|
+
}
|
|
170
|
+
const support = [...group.values.keys()].sort((left, right) => left - right);
|
|
171
|
+
values.push({
|
|
172
|
+
...group.keys,
|
|
173
|
+
[transform.as.value]: support[0],
|
|
174
|
+
[transform.as.cumulative]: 0,
|
|
175
|
+
[transform.as.probability]: 0
|
|
176
|
+
});
|
|
177
|
+
let cumulative = 0;
|
|
178
|
+
for (const value of support) {
|
|
179
|
+
cumulative += group.values.get(value);
|
|
180
|
+
values.push({
|
|
181
|
+
...group.keys,
|
|
182
|
+
[transform.as.value]: value,
|
|
183
|
+
[transform.as.cumulative]: stableDecimal(cumulative),
|
|
184
|
+
[transform.as.probability]: stableDecimal(cumulative / group.denominator)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
provenance.push({
|
|
188
|
+
keys: group.keys,
|
|
189
|
+
denominator: stableDecimal(group.denominator),
|
|
190
|
+
validCount: group.validCount
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return cloneAndFreeze({ values, resolved: { groups: provenance } });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveECDFTransform(result, transform) {
|
|
197
|
+
return cloneAndFreeze([{ ...transform, resolved: result.resolved }]);
|
|
198
|
+
}
|