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
|
@@ -4,18 +4,8 @@ import {
|
|
|
4
4
|
validateGeneratedItemLimit,
|
|
5
5
|
validateWorkLimit
|
|
6
6
|
} from "../../core/validation.js";
|
|
7
|
-
import {
|
|
8
|
-
import { planFacetDependencies } from "./dependencies.js";
|
|
7
|
+
import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
|
|
9
8
|
import { readNominalField } from "../scales/index.js";
|
|
10
|
-
|
|
11
|
-
const SUPPORTED_MARKS = new Set([
|
|
12
|
-
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
13
|
-
]);
|
|
14
|
-
const SUPPORTED_BAR_GRAINS = new Set([
|
|
15
|
-
BAR_GRAINS.histogram,
|
|
16
|
-
BAR_GRAINS.aggregate,
|
|
17
|
-
BAR_GRAINS.ranged
|
|
18
|
-
]);
|
|
19
9
|
const MAX_FACET_CHILDREN = 100;
|
|
20
10
|
|
|
21
11
|
function requireFacetField(field, label = "facet") {
|
|
@@ -25,40 +15,6 @@ function requireFacetField(field, label = "facet") {
|
|
|
25
15
|
return field;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
function requireSupportedLayer(layer) {
|
|
29
|
-
if (!SUPPORTED_MARKS.has(layer.mark?.type)) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`facet does not support mark "${layer.id}" of type ${layer.mark?.type ?? "incomplete"}.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
layer.mark.type === "bar" &&
|
|
36
|
-
!SUPPORTED_BAR_GRAINS.has(resolveBarGrain(layer))
|
|
37
|
-
) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (
|
|
43
|
-
layer.encoding?.x?.scale === undefined ||
|
|
44
|
-
layer.encoding?.y?.scale === undefined
|
|
45
|
-
) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (typeof layer.data !== "string" || layer.data.length === 0) {
|
|
51
|
-
throw new Error(`Facet layer "${layer.id}" requires a dataset.`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function requireSupportedLayers(semanticSpec) {
|
|
56
|
-
if (!Array.isArray(semanticSpec.layers) || semanticSpec.layers.length === 0) {
|
|
57
|
-
throw new Error("facet requires at least one materializable layer.");
|
|
58
|
-
}
|
|
59
|
-
for (const layer of semanticSpec.layers) requireSupportedLayer(layer);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
18
|
function requirePartitionDataset(semanticSpec, id) {
|
|
63
19
|
const dataset = semanticSpec.datasets?.find(candidate => candidate.id === id);
|
|
64
20
|
if (dataset === undefined) {
|
|
@@ -129,9 +85,10 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
129
85
|
}
|
|
130
86
|
const field = requireFacetField(options.field);
|
|
131
87
|
const id = validateUserId(options.id ?? "facet", "Facet id");
|
|
132
|
-
|
|
88
|
+
const family = resolveFacetFamily(semanticSpec);
|
|
133
89
|
const dependencies = planFacetDependencies(semanticSpec, {
|
|
134
90
|
field,
|
|
91
|
+
family,
|
|
135
92
|
...(options.data === undefined ? {} : { data: options.data })
|
|
136
93
|
});
|
|
137
94
|
const data = dependencies.anchor;
|
|
@@ -152,6 +109,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
152
109
|
);
|
|
153
110
|
return cloneAndFreeze({
|
|
154
111
|
id,
|
|
112
|
+
family: family.family,
|
|
155
113
|
data,
|
|
156
114
|
field,
|
|
157
115
|
values,
|
|
@@ -181,9 +139,10 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
|
|
|
181
139
|
throw new Error('facetGrid combinations must be "observed" or "full".');
|
|
182
140
|
}
|
|
183
141
|
const id = validateUserId(options.id ?? "facetGrid", "Facet grid id");
|
|
184
|
-
|
|
142
|
+
const family = resolveFacetFamily(semanticSpec);
|
|
185
143
|
const dependencies = planFacetDependencies(semanticSpec, {
|
|
186
144
|
field: rows.field,
|
|
145
|
+
family,
|
|
187
146
|
...(options.data === undefined ? {} : { data: options.data })
|
|
188
147
|
});
|
|
189
148
|
const data = dependencies.anchor;
|
|
@@ -225,6 +184,7 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
|
|
|
225
184
|
);
|
|
226
185
|
return cloneAndFreeze({
|
|
227
186
|
id,
|
|
187
|
+
family: family.family,
|
|
228
188
|
data,
|
|
229
189
|
dependencies,
|
|
230
190
|
grid: {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
FACET_SCALE_CHANNELS,
|
|
4
4
|
FACET_SCALE_RESOLUTIONS
|
|
5
5
|
} from "../../core/vocabulary.js";
|
|
6
|
+
import { collectFacetScaleBindings } from "./dependencies.js";
|
|
6
7
|
import { resolveHistogramBins } from "../histogram.js";
|
|
7
8
|
import {
|
|
8
9
|
hasOrdinalDomain,
|
|
@@ -63,30 +64,25 @@ function normalizeRequestedPolicies(requested) {
|
|
|
63
64
|
|
|
64
65
|
function usedScaleChannels(semanticSpec, scales) {
|
|
65
66
|
const used = new Map();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`Facet layer "${layer.id}" references missing scale "${id}".`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (!used.has(id)) used.set(id, new Set());
|
|
76
|
-
used.get(id).add(channel);
|
|
67
|
+
const bindings = collectFacetScaleBindings(semanticSpec);
|
|
68
|
+
for (const binding of bindings) {
|
|
69
|
+
if (!scales.has(binding.scaleId)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Facet layer "${binding.layerId}" references missing scale "${binding.scaleId}".`
|
|
72
|
+
);
|
|
77
73
|
}
|
|
74
|
+
if (!used.has(binding.scaleId)) used.set(binding.scaleId, new Set());
|
|
75
|
+
used.get(binding.scaleId).add(binding.policyKey);
|
|
78
76
|
}
|
|
79
|
-
return used;
|
|
77
|
+
return { used, bindings };
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
|
|
83
81
|
const scales = requireScaleDefinitions(semanticSpec);
|
|
84
82
|
const channels = normalizeRequestedPolicies(requested);
|
|
85
|
-
const used = usedScaleChannels(semanticSpec, scales);
|
|
83
|
+
const { used, bindings } = usedScaleChannels(semanticSpec, scales);
|
|
86
84
|
for (const channel of Object.keys(requested)) {
|
|
87
|
-
const applicable =
|
|
88
|
-
layer => layer.encoding?.[channel]?.scale !== undefined
|
|
89
|
-
);
|
|
85
|
+
const applicable = bindings.some(binding => binding.policyKey === channel);
|
|
90
86
|
if (!applicable) {
|
|
91
87
|
throw new Error(
|
|
92
88
|
`Facet scale channel "${channel}" is not used by an affected layer.`
|
|
@@ -107,7 +103,7 @@ export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
|
|
|
107
103
|
channels: attached
|
|
108
104
|
};
|
|
109
105
|
}
|
|
110
|
-
return cloneAndFreeze({ channels, scales: scalePolicies });
|
|
106
|
+
return cloneAndFreeze({ channels, scales: scalePolicies, bindings });
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
function requireChildDomains(resolvedByChild, scaleId) {
|
|
@@ -210,7 +206,8 @@ export function resolveFacetScaleDomains(
|
|
|
210
206
|
}
|
|
211
207
|
return cloneAndFreeze({
|
|
212
208
|
channels: normalized.channels,
|
|
213
|
-
scales: resolved
|
|
209
|
+
scales: resolved,
|
|
210
|
+
bindings: normalized.bindings
|
|
214
211
|
});
|
|
215
212
|
}
|
|
216
213
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
3
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
4
|
+
|
|
5
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
6
|
+
"type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
|
|
7
|
+
]);
|
|
8
|
+
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
9
|
+
const METHODS = Object.freeze(["constant", "forward", "backward", "linear"]);
|
|
10
|
+
|
|
11
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
12
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
13
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireField(value, label) {
|
|
17
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateFieldList(value, label) {
|
|
24
|
+
if (!Array.isArray(value) || value.length === 0 || value.some(field =>
|
|
25
|
+
typeof field !== "string" || field.length === 0
|
|
26
|
+
)) {
|
|
27
|
+
throw new TypeError(`${label} must be a non-empty field array.`);
|
|
28
|
+
}
|
|
29
|
+
if (new Set(value).size !== value.length) throw new Error(`${label} fields must be unique.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeFieldList(value, { allowEmpty = false } = {}) {
|
|
33
|
+
if (value === undefined) return allowEmpty ? [] : value;
|
|
34
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeSortBy(value) {
|
|
38
|
+
if (value === undefined) return [];
|
|
39
|
+
if (!Array.isArray(value)) return value;
|
|
40
|
+
return value.map(sort => isPlainObject(sort)
|
|
41
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
42
|
+
: sort
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateSortBy(sortBy) {
|
|
47
|
+
if (!Array.isArray(sortBy)) throw new TypeError("Impute sortBy must be an array.");
|
|
48
|
+
const fields = [];
|
|
49
|
+
sortBy.forEach((sort, index) => {
|
|
50
|
+
if (!isPlainObject(sort)) {
|
|
51
|
+
throw new TypeError(`Impute sortBy[${index}] must be a plain object.`);
|
|
52
|
+
}
|
|
53
|
+
rejectUnknownKeys(sort, SORT_KEYS, `impute sortBy[${index}]`);
|
|
54
|
+
fields.push(requireField(sort.field, `Impute sortBy[${index}].field`));
|
|
55
|
+
if (!["ascending", "descending"].includes(sort.order)) {
|
|
56
|
+
throw new Error(`Unsupported impute sort order "${sort.order}".`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (new Set(fields).size !== fields.length) {
|
|
60
|
+
throw new Error("Impute sortBy fields must be unique.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scalarType(value, label) {
|
|
65
|
+
if (value === null || value === undefined) return undefined;
|
|
66
|
+
if (typeof value === "string" || typeof value === "boolean") return typeof value;
|
|
67
|
+
if (typeof value === "number" && Number.isFinite(value)) return "number";
|
|
68
|
+
throw new TypeError(`${label} must contain scalar values; NaN and Infinity are invalid.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scalarKey(value, label) {
|
|
72
|
+
const type = scalarType(value, label);
|
|
73
|
+
if (type === undefined) return "null";
|
|
74
|
+
if (type === "number") return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
75
|
+
if (type === "string") return `string:${value.length}:${value}`;
|
|
76
|
+
return `${type}:${value}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateImputeTransform(transform) {
|
|
80
|
+
if (!isPlainObject(transform)) {
|
|
81
|
+
throw new TypeError("Impute transform must be a plain object.");
|
|
82
|
+
}
|
|
83
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "impute transform");
|
|
84
|
+
if (transform.type !== "impute") {
|
|
85
|
+
throw new Error(`Unsupported impute transform "${transform.type}".`);
|
|
86
|
+
}
|
|
87
|
+
validateFieldList(transform.fields, "Impute fields");
|
|
88
|
+
if (!Array.isArray(transform.groupBy) || transform.groupBy.some(field =>
|
|
89
|
+
typeof field !== "string" || field.length === 0
|
|
90
|
+
)) {
|
|
91
|
+
throw new TypeError("Impute groupBy must contain field names.");
|
|
92
|
+
}
|
|
93
|
+
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
94
|
+
throw new Error("Impute groupBy fields must be unique.");
|
|
95
|
+
}
|
|
96
|
+
validateSortBy(transform.sortBy);
|
|
97
|
+
if (!METHODS.includes(transform.method)) {
|
|
98
|
+
throw new Error(`Unsupported impute method "${transform.method}".`);
|
|
99
|
+
}
|
|
100
|
+
const hasValue = Object.hasOwn(transform, "value");
|
|
101
|
+
if (transform.method === "constant") {
|
|
102
|
+
if (!hasValue) throw new TypeError("Impute constant requires a value.");
|
|
103
|
+
if (transform.value === undefined) {
|
|
104
|
+
throw new TypeError("Impute constant value must be a JSON-safe scalar.");
|
|
105
|
+
}
|
|
106
|
+
scalarType(transform.value, "Impute constant value");
|
|
107
|
+
} else {
|
|
108
|
+
if (hasValue) throw new Error(`Impute value is not available for ${transform.method}.`);
|
|
109
|
+
if (transform.sortBy.length === 0) {
|
|
110
|
+
throw new Error(`Impute ${transform.method} requires non-empty sortBy.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (transform.method === "linear") {
|
|
114
|
+
if (transform.sortBy.length !== 1 || transform.sortBy[0].order !== "ascending") {
|
|
115
|
+
throw new Error("Impute linear requires exactly one ascending sortBy field.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!['keep', 'error'].includes(transform.edges)) {
|
|
119
|
+
throw new Error(`Unsupported impute edges policy "${transform.edges}".`);
|
|
120
|
+
}
|
|
121
|
+
if (transform.maxGap !== undefined &&
|
|
122
|
+
(!Number.isSafeInteger(transform.maxGap) || transform.maxGap <= 0)) {
|
|
123
|
+
throw new RangeError("Impute maxGap must be a positive safe integer.");
|
|
124
|
+
}
|
|
125
|
+
return transform;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeImputeTransform(args = {}) {
|
|
129
|
+
const transform = {
|
|
130
|
+
type: "impute",
|
|
131
|
+
fields: normalizeFieldList(args.fields),
|
|
132
|
+
groupBy: normalizeFieldList(args.groupBy, { allowEmpty: true }),
|
|
133
|
+
sortBy: normalizeSortBy(args.sortBy),
|
|
134
|
+
method: args.method,
|
|
135
|
+
edges: args.edges ?? "keep"
|
|
136
|
+
};
|
|
137
|
+
if (Object.hasOwn(args, "value")) transform.value = args.value;
|
|
138
|
+
if (Object.hasOwn(args, "maxGap")) transform.maxGap = args.maxGap;
|
|
139
|
+
validateImputeTransform(transform);
|
|
140
|
+
return cloneAndFreeze(transform);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compareScalar(left, right, label) {
|
|
144
|
+
const leftType = scalarType(left, label);
|
|
145
|
+
const rightType = scalarType(right, label);
|
|
146
|
+
if (leftType === undefined || rightType === undefined) {
|
|
147
|
+
if (leftType === rightType) return 0;
|
|
148
|
+
return leftType === undefined ? 1 : -1;
|
|
149
|
+
}
|
|
150
|
+
if (leftType !== rightType) throw new TypeError(`${label} must contain one scalar type.`);
|
|
151
|
+
if (left === right) return 0;
|
|
152
|
+
return left < right ? -1 : 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compareEntries(left, right, sortBy) {
|
|
156
|
+
for (const sort of sortBy) {
|
|
157
|
+
const comparison = compareScalar(
|
|
158
|
+
left.row[sort.field], right.row[sort.field], `Impute sort field "${sort.field}"`
|
|
159
|
+
);
|
|
160
|
+
if (comparison !== 0) return sort.order === "ascending" ? comparison : -comparison;
|
|
161
|
+
}
|
|
162
|
+
return left.index - right.index;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateRows(rows, transform) {
|
|
166
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
167
|
+
throw new TypeError("Impute source rows must be plain objects.");
|
|
168
|
+
}
|
|
169
|
+
const required = [...transform.fields, ...transform.groupBy,
|
|
170
|
+
...transform.sortBy.map(sort => sort.field)];
|
|
171
|
+
rows.forEach((row, index) => {
|
|
172
|
+
for (const field of required) {
|
|
173
|
+
if (!Object.hasOwn(row, field)) {
|
|
174
|
+
throw new Error(`Impute source does not contain field "${field}" at row ${index}.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const field of transform.fields) {
|
|
178
|
+
scalarType(row[field], `Impute field "${field}" at row ${index}`);
|
|
179
|
+
}
|
|
180
|
+
for (const field of transform.groupBy) {
|
|
181
|
+
scalarKey(row[field], `Impute group field "${field}" at row ${index}`);
|
|
182
|
+
}
|
|
183
|
+
for (const sort of transform.sortBy) {
|
|
184
|
+
scalarType(row[sort.field], `Impute sort field "${sort.field}" at row ${index}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function partitions(entries, transform) {
|
|
190
|
+
const groups = new Map();
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const key = transform.groupBy.length === 0 ? "all" : transform.groupBy.map(field =>
|
|
193
|
+
scalarKey(entry.row[field], `Impute group field "${field}"`)
|
|
194
|
+
).join("\0");
|
|
195
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
196
|
+
groups.get(key).push(entry);
|
|
197
|
+
}
|
|
198
|
+
return [...groups.values()];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateFieldTypes(entries, transform) {
|
|
202
|
+
for (const field of transform.fields) {
|
|
203
|
+
const types = new Set(entries.flatMap(entry => {
|
|
204
|
+
const type = scalarType(entry.row[field], `Impute field "${field}"`);
|
|
205
|
+
return type === undefined ? [] : [type];
|
|
206
|
+
}));
|
|
207
|
+
const constantType = transform.method === "constant"
|
|
208
|
+
? scalarType(transform.value, "Impute constant value")
|
|
209
|
+
: undefined;
|
|
210
|
+
if (constantType !== undefined) types.add(constantType);
|
|
211
|
+
if (types.size > 1) {
|
|
212
|
+
throw new TypeError(`Impute field "${field}" must contain one scalar type.`);
|
|
213
|
+
}
|
|
214
|
+
if (transform.method === "linear" &&
|
|
215
|
+
(types.size > 0 && !types.has("number"))) {
|
|
216
|
+
throw new TypeError(`Impute linear field "${field}" must contain finite numbers.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateSortFieldTypes(partition, transform) {
|
|
222
|
+
for (const sort of transform.sortBy) {
|
|
223
|
+
const types = new Set(partition.flatMap(entry => {
|
|
224
|
+
const type = scalarType(
|
|
225
|
+
entry.row[sort.field],
|
|
226
|
+
`Impute sort field "${sort.field}"`
|
|
227
|
+
);
|
|
228
|
+
return type === undefined ? [] : [type];
|
|
229
|
+
}));
|
|
230
|
+
if (types.size > 1) {
|
|
231
|
+
throw new TypeError(`Impute sort field "${sort.field}" must contain one scalar type.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function linearPositions(partition, transform) {
|
|
237
|
+
if (transform.method !== "linear") return undefined;
|
|
238
|
+
const field = transform.sortBy[0].field;
|
|
239
|
+
const types = new Set(partition.map((entry, index) =>
|
|
240
|
+
scalarType(entry.row[field], `Impute linear sort field "${field}" at row ${index}`)
|
|
241
|
+
));
|
|
242
|
+
if (types.size !== 1 || types.has(undefined) ||
|
|
243
|
+
!["number", "string"].includes([...types][0])) {
|
|
244
|
+
throw new TypeError("Impute linear sort field must contain one numeric or temporal-string type.");
|
|
245
|
+
}
|
|
246
|
+
const type = [...types][0];
|
|
247
|
+
const positions = partition.map((entry, index) => type === "number"
|
|
248
|
+
? entry.row[field]
|
|
249
|
+
: normalizeTemporalValue(entry.row[field], field, index, "auto")
|
|
250
|
+
);
|
|
251
|
+
for (let index = 1; index < positions.length; index += 1) {
|
|
252
|
+
if (positions[index] === positions[index - 1]) {
|
|
253
|
+
throw new Error("Impute linear sort positions must be unique.");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return positions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function missingRuns(partition, field) {
|
|
260
|
+
const runs = [];
|
|
261
|
+
let start;
|
|
262
|
+
for (let index = 0; index <= partition.length; index += 1) {
|
|
263
|
+
const missing = index < partition.length &&
|
|
264
|
+
(partition[index].row[field] === null || partition[index].row[field] === undefined);
|
|
265
|
+
if (missing && start === undefined) start = index;
|
|
266
|
+
if (!missing && start !== undefined) {
|
|
267
|
+
runs.push([start, index - 1]);
|
|
268
|
+
start = undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return runs;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function setField(row, field, value) {
|
|
275
|
+
Object.defineProperty(row, field, {
|
|
276
|
+
value, enumerable: true, writable: true, configurable: true
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function fillRun(partition, field, run, transform, positions) {
|
|
281
|
+
const [start, end] = run;
|
|
282
|
+
const length = end - start + 1;
|
|
283
|
+
if (transform.maxGap !== undefined && length > transform.maxGap) return;
|
|
284
|
+
if (transform.method === "constant") {
|
|
285
|
+
for (let index = start; index <= end; index += 1) {
|
|
286
|
+
setField(partition[index].row, field, transform.value);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const left = start - 1;
|
|
291
|
+
const right = end + 1;
|
|
292
|
+
const hasLeft = left >= 0;
|
|
293
|
+
const hasRight = right < partition.length;
|
|
294
|
+
const usable = transform.method === "forward" ? hasLeft
|
|
295
|
+
: transform.method === "backward" ? hasRight
|
|
296
|
+
: hasLeft && hasRight;
|
|
297
|
+
if (!usable) {
|
|
298
|
+
if (transform.edges === "error") {
|
|
299
|
+
throw new RangeError(`Impute ${transform.method} cannot fill an edge run in field "${field}".`);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (transform.method === "forward" || transform.method === "backward") {
|
|
304
|
+
const anchor = transform.method === "forward" ? left : right;
|
|
305
|
+
const value = partition[anchor].row[field];
|
|
306
|
+
for (let index = start; index <= end; index += 1) {
|
|
307
|
+
setField(partition[index].row, field, value);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const leftValue = partition[left].row[field];
|
|
312
|
+
const rightValue = partition[right].row[field];
|
|
313
|
+
for (let index = start; index <= end; index += 1) {
|
|
314
|
+
const proportion = inverseLerp(positions[index], positions[left], positions[right]);
|
|
315
|
+
setField(
|
|
316
|
+
partition[index].row,
|
|
317
|
+
field,
|
|
318
|
+
interpolateNumber(leftValue, rightValue, proportion)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function deriveImputedRows(rows, transform) {
|
|
324
|
+
validateImputeTransform(transform);
|
|
325
|
+
validateRows(rows, transform);
|
|
326
|
+
const entries = rows.map((row, index) => ({ index, row: { ...row } }));
|
|
327
|
+
validateFieldTypes(entries, transform);
|
|
328
|
+
const groups = partitions(entries, transform);
|
|
329
|
+
for (const partition of groups) {
|
|
330
|
+
validateSortFieldTypes(partition, transform);
|
|
331
|
+
partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
|
|
332
|
+
const positions = linearPositions(partition, transform);
|
|
333
|
+
for (const field of transform.fields) {
|
|
334
|
+
for (const run of missingRuns(partition, field)) {
|
|
335
|
+
fillRun(partition, field, run, transform, positions);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return cloneAndFreeze(entries.sort((a, b) => a.index - b.index).map(entry => entry.row));
|
|
340
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
import { normalizeMarkSelector } from "./markSelection.js";
|
|
4
|
+
|
|
5
|
+
function hasOwn(value, key) {
|
|
6
|
+
return Object.hasOwn(value, key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeNamedSelection(id) {
|
|
10
|
+
return cloneAndFreeze({
|
|
11
|
+
kind: "named",
|
|
12
|
+
id: validateUserId(id, "Label selection id")
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeInlineSelection(selector) {
|
|
17
|
+
return cloneAndFreeze({
|
|
18
|
+
kind: "inline",
|
|
19
|
+
selector: normalizeMarkSelector(selector)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeLabelSelectionCreate(options) {
|
|
24
|
+
if (!isPlainObject(options)) {
|
|
25
|
+
throw new TypeError("createMarkLabels options must be a plain object.");
|
|
26
|
+
}
|
|
27
|
+
const hasSelect = hasOwn(options, "select");
|
|
28
|
+
const hasSelection = hasOwn(options, "selection");
|
|
29
|
+
if (hasSelect && hasSelection) {
|
|
30
|
+
throw new Error("createMarkLabels accepts select or selection, not both.");
|
|
31
|
+
}
|
|
32
|
+
if (hasSelect) return normalizeInlineSelection(options.select);
|
|
33
|
+
if (hasSelection) return normalizeNamedSelection(options.selection);
|
|
34
|
+
return cloneAndFreeze({ kind: "all" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeLabelSelectionEdit(options) {
|
|
38
|
+
if (!isPlainObject(options)) {
|
|
39
|
+
throw new TypeError("editMarkLabelSelection options must be a plain object.");
|
|
40
|
+
}
|
|
41
|
+
const requested = ["select", "selection", "all"].filter(key =>
|
|
42
|
+
hasOwn(options, key)
|
|
43
|
+
);
|
|
44
|
+
if (requested.length !== 1) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"editMarkLabelSelection requires exactly one of select, selection, or all."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (requested[0] === "select") return normalizeInlineSelection(options.select);
|
|
50
|
+
if (requested[0] === "selection") {
|
|
51
|
+
return normalizeNamedSelection(options.selection);
|
|
52
|
+
}
|
|
53
|
+
if (options.all !== true) {
|
|
54
|
+
throw new TypeError("editMarkLabelSelection all must be true.");
|
|
55
|
+
}
|
|
56
|
+
return cloneAndFreeze({ kind: "all" });
|
|
57
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { aggregateRows } from "./aggregate.js";
|
|
2
2
|
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "./bars/policy.js";
|
|
3
3
|
import { stableFiniteSum } from "./numeric.js";
|
|
4
|
+
import {
|
|
5
|
+
readStatisticalWeights,
|
|
6
|
+
statisticalWeightTotal,
|
|
7
|
+
summarizeStatisticalWeights
|
|
8
|
+
} from "./weightedStatistics.js";
|
|
4
9
|
|
|
5
10
|
const CONTENTS = new Set(["category", "value", "share"]);
|
|
6
11
|
const NORMALIZATIONS = new Set(["source", "category"]);
|
|
@@ -44,7 +49,19 @@ export function normalizeMarkLabelContent(source, { content, normalizeBy }) {
|
|
|
44
49
|
|
|
45
50
|
function itemValue(source, item) {
|
|
46
51
|
if (source.mark.type === "bar") {
|
|
47
|
-
if (resolveBarGrain(source) === BAR_GRAINS.histogram)
|
|
52
|
+
if (resolveBarGrain(source) === BAR_GRAINS.histogram) {
|
|
53
|
+
const weight = source.encoding.x.weight;
|
|
54
|
+
if (weight === undefined || item.members.length === 0) return item.members.length;
|
|
55
|
+
const weighted = readStatisticalWeights(item.members, weight, "Histogram label");
|
|
56
|
+
return statisticalWeightTotal(
|
|
57
|
+
summarizeStatisticalWeights(
|
|
58
|
+
weighted.entries,
|
|
59
|
+
weighted.definition.kind,
|
|
60
|
+
"Histogram label"
|
|
61
|
+
),
|
|
62
|
+
"Histogram label value"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
48
65
|
const measure = source.encoding[resolveBarChannels(source).measure];
|
|
49
66
|
return measure.aggregate === "count" ? item.members.length
|
|
50
67
|
: aggregateRows(item.members, measure.field, measure.aggregate);
|