ggaction 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
|
@@ -13,30 +13,31 @@ function indexSegments(segments) {
|
|
|
13
13
|
return indexed;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function deriveAreaSeries(rows, layer) {
|
|
16
|
+
export function deriveAreaSeries(rows, layer, options = {}) {
|
|
17
17
|
if (layer?.mark?.type !== "area") {
|
|
18
18
|
throw new Error("Area series derivation requires a semantic area mark.");
|
|
19
19
|
}
|
|
20
20
|
const { x, y, x2, y2 } = layer.encoding ?? {};
|
|
21
21
|
const vertical =
|
|
22
|
-
["quantitative", "temporal"].includes(x?.fieldType) &&
|
|
22
|
+
["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) &&
|
|
23
23
|
y?.fieldType === "quantitative" &&
|
|
24
24
|
y2?.fieldType === "quantitative" &&
|
|
25
25
|
x2 === undefined;
|
|
26
26
|
const horizontal =
|
|
27
|
-
["quantitative", "temporal"].includes(y?.fieldType) &&
|
|
27
|
+
["quantitative", "temporal", "nominal", "ordinal"].includes(y?.fieldType) &&
|
|
28
28
|
x?.fieldType === "quantitative" &&
|
|
29
29
|
x2?.fieldType === "quantitative" &&
|
|
30
30
|
y2 === undefined;
|
|
31
31
|
if (vertical === horizontal) {
|
|
32
32
|
throw new Error(
|
|
33
|
-
`Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or
|
|
33
|
+
`Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
const grouping = validatePathSeriesAppearance(rows, layer);
|
|
37
37
|
const orientation = vertical ? "vertical" : "horizontal";
|
|
38
38
|
const independent = vertical ? x : y;
|
|
39
|
-
const
|
|
39
|
+
const categorical = ["nominal", "ordinal"].includes(independent.fieldType);
|
|
40
|
+
const independentValues = categorical ? readNominalField(rows, independent.field) : independent.fieldType === "temporal"
|
|
40
41
|
? readTemporalField(rows, independent.field, independent.temporalUnit)
|
|
41
42
|
: readQuantitativeField(rows, independent.field);
|
|
42
43
|
if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
|
|
@@ -46,6 +47,9 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
46
47
|
const missing = layer.mark.missing ?? "error";
|
|
47
48
|
const lower = readAreaEndpoint(rows, primary, missing);
|
|
48
49
|
const upper = readAreaEndpoint(rows, secondary, missing);
|
|
50
|
+
const categoryOrder = categorical ? options[vertical ? "xDomain" : "yDomain"] ?? [...new Set(independentValues)] : undefined;
|
|
51
|
+
const ranks = new Map(categoryOrder?.map((value, index) => [value, index]));
|
|
52
|
+
if (categorical && independentValues.some(value => !ranks.has(value))) throw new Error("Area categorical domain must contain every observed position.");
|
|
49
53
|
const pathOrder = layer.encoding?.pathOrder;
|
|
50
54
|
const orderValues = pathOrder === undefined
|
|
51
55
|
? undefined
|
|
@@ -83,7 +87,7 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
83
87
|
const series = [...groups.values()].flatMap(item => {
|
|
84
88
|
const key = vertical ? "x" : "y";
|
|
85
89
|
const values = pathOrder === undefined
|
|
86
|
-
? item.values.sort((left, right) => left[key] - right[key])
|
|
90
|
+
? item.values.sort((left, right) => categorical ? ranks.get(left[key]) - ranks.get(right[key]) : left[key] - right[key])
|
|
87
91
|
: stableOrderPathValues(
|
|
88
92
|
item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
|
|
89
93
|
item.values.map(value => value.pathOrder),
|
|
@@ -125,13 +129,13 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
125
129
|
});
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
export function deriveCenteredAreaSeries(rows, layer) {
|
|
132
|
+
export function deriveCenteredAreaSeries(rows, layer, options = {}) {
|
|
129
133
|
if (layer?.mark?.type !== "area") {
|
|
130
134
|
throw new Error("Centered area series derivation requires an area mark.");
|
|
131
135
|
}
|
|
132
136
|
const { x, y, x2, y2, group, color } = layer.encoding ?? {};
|
|
133
137
|
if (
|
|
134
|
-
!["quantitative", "temporal"].includes(x?.fieldType) ||
|
|
138
|
+
!["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) ||
|
|
135
139
|
y?.fieldType !== "quantitative" ||
|
|
136
140
|
y?.stack !== "center" ||
|
|
137
141
|
x2 !== undefined ||
|
|
@@ -149,7 +153,8 @@ export function deriveCenteredAreaSeries(rows, layer) {
|
|
|
149
153
|
`Centered area color on mark "${layer.id}" must match its group field.`
|
|
150
154
|
);
|
|
151
155
|
}
|
|
152
|
-
const
|
|
156
|
+
const categorical = ["nominal", "ordinal"].includes(x.fieldType);
|
|
157
|
+
const xValues = categorical ? readNominalField(rows, x.field) : x.fieldType === "temporal"
|
|
153
158
|
? readTemporalField(rows, x.field, x.temporalUnit)
|
|
154
159
|
: readQuantitativeField(rows, x.field);
|
|
155
160
|
const yValues = readQuantitativeField(rows, y.field);
|
|
@@ -172,7 +177,10 @@ export function deriveCenteredAreaSeries(rows, layer) {
|
|
|
172
177
|
values.set(xValues[index], yValues[index]);
|
|
173
178
|
positions.add(xValues[index]);
|
|
174
179
|
}
|
|
175
|
-
const orderedPositions =
|
|
180
|
+
const orderedPositions = categorical
|
|
181
|
+
? (options.xDomain ?? [...positions]).filter(value => positions.has(value))
|
|
182
|
+
: [...positions].sort((left, right) => left - right);
|
|
183
|
+
if (orderedPositions.length !== positions.size) throw new Error("Area categorical domain must contain every observed position.");
|
|
176
184
|
if (groupOrder.length === 0 || orderedPositions.length < 2) {
|
|
177
185
|
throw new Error(
|
|
178
186
|
`Centered area mark "${layer.id}" requires at least two aligned positions.`
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
2
|
+
import { floorUtcTimeUnit, offsetUtcTimeUnit, TIME_UNITS, validTimestamp } from "../timeUnit.js";
|
|
3
|
+
import { mapContinuousScaleValues } from "../scales/mapping.js";
|
|
1
4
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
5
|
|
|
3
6
|
export const DEFAULT_BAR_WIDTH = Object.freeze({ band: 0.72 });
|
|
@@ -9,8 +12,16 @@ export const DEFAULT_OFFSET_PADDING = Object.freeze({
|
|
|
9
12
|
export function normalizeBarWidth(options = {}, existing) {
|
|
10
13
|
const hasBand = options.band !== undefined;
|
|
11
14
|
const hasPixels = options.pixels !== undefined;
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
const hasInterval = options.interval !== undefined;
|
|
16
|
+
if (Number(hasBand) + Number(hasPixels) + Number(hasInterval) > 1) {
|
|
17
|
+
throw new Error("encodeBarWidth band, pixels, and interval are mutually exclusive.");
|
|
18
|
+
}
|
|
19
|
+
if (hasInterval) {
|
|
20
|
+
validateOptionObject(options.interval, ["unit", "insetStart", "insetEnd"], "Bar width interval");
|
|
21
|
+
const { unit, insetStart = 0, insetEnd = 0 } = options.interval;
|
|
22
|
+
if (!TIME_UNITS.includes(unit) || ["week", "weekday"].includes(unit)) throw new Error("Bar width interval requires year, quarter, month, day, hour, minute, or second.");
|
|
23
|
+
if (![insetStart, insetEnd].every(v => Number.isFinite(v) && v >= 0)) throw new RangeError("Bar interval insets must be non-negative finite pixels.");
|
|
24
|
+
return cloneAndFreeze({ interval: { unit, insetStart, insetEnd } });
|
|
14
25
|
}
|
|
15
26
|
if (!hasBand && !hasPixels) {
|
|
16
27
|
return cloneAndFreeze(existing ?? DEFAULT_BAR_WIDTH);
|
|
@@ -90,9 +101,25 @@ export function normalizeOffsetScalePolicy(
|
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
export function resolveBarWidth(config, slotBandwidth) {
|
|
104
|
+
const normalized = normalizeBarWidth(config);
|
|
105
|
+
if (normalized.pixels !== undefined) return normalized.pixels;
|
|
93
106
|
if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
|
|
94
107
|
throw new Error("Bar width requires a positive resolved slot bandwidth.");
|
|
95
108
|
}
|
|
96
|
-
const normalized = normalizeBarWidth(config);
|
|
97
109
|
return normalized.pixels ?? slotBandwidth * normalized.band;
|
|
98
110
|
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
// Width intent never buckets or aggregates data: category values are interval starts.
|
|
114
|
+
export function resolveBarInterval(config, value, scale) {
|
|
115
|
+
if (scale.type !== "time") throw new Error("Bar interval width requires a temporal category scale.");
|
|
116
|
+
const { unit, insetStart, insetEnd } = normalizeBarWidth(config).interval;
|
|
117
|
+
const start = floorUtcTimeUnit(value, unit);
|
|
118
|
+
if (start !== value) throw new Error("Bar interval category values must be UTC interval starts.");
|
|
119
|
+
const end = offsetUtcTimeUnit(start, unit);
|
|
120
|
+
if (!validTimestamp(end)) throw new RangeError("Bar interval end exceeds the supported Date range.");
|
|
121
|
+
if (start < Math.min(...scale.domain) || end > Math.max(...scale.domain)) throw new RangeError("Bar interval endpoints must be inside the temporal scale domain; set an explicit covering domain.");
|
|
122
|
+
const [a, b] = mapContinuousScaleValues([start, end], scale), width = Math.abs(b - a) - insetStart - insetEnd;
|
|
123
|
+
if (width <= 0) throw new RangeError("Bar interval insets must leave a positive width.");
|
|
124
|
+
return { position: Math.min(a, b) + (b >= a ? insetStart : insetEnd), width };
|
|
125
|
+
}
|
|
@@ -3,6 +3,7 @@ import { isAggregate } from "../aggregate.js";
|
|
|
3
3
|
export const BAR_GRAINS = Object.freeze({
|
|
4
4
|
histogram: "histogram",
|
|
5
5
|
aggregate: "aggregate",
|
|
6
|
+
centered: "centered",
|
|
6
7
|
ranged: "ranged"
|
|
7
8
|
});
|
|
8
9
|
|
|
@@ -20,10 +21,17 @@ function isMeasure(encoding) {
|
|
|
20
21
|
isAggregate(encoding.aggregate);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function hasNumericCenters(layer) {
|
|
25
|
+
return layer.mark.orientation !== undefined && [layer.encoding?.x, layer.encoding?.y].every(
|
|
26
|
+
encoding => encoding?.fieldType === "quantitative" && encoding.bin === undefined && encoding.aggregate === undefined
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export function resolveBarOrientation(layer) {
|
|
24
31
|
if (layer?.mark?.type !== "bar") return undefined;
|
|
25
32
|
const x = layer.encoding?.x;
|
|
26
33
|
const y = layer.encoding?.y;
|
|
34
|
+
if (hasNumericCenters(layer)) return layer.mark.orientation;
|
|
27
35
|
if (x?.bin !== undefined && y?.aggregate === "count") {
|
|
28
36
|
return BAR_ORIENTATIONS.vertical;
|
|
29
37
|
}
|
|
@@ -56,6 +64,8 @@ export function resolveBarGrain(layer) {
|
|
|
56
64
|
const x = layer.encoding?.x;
|
|
57
65
|
const y = layer.encoding?.y;
|
|
58
66
|
|
|
67
|
+
if (hasNumericCenters(layer)) return BAR_GRAINS.centered;
|
|
68
|
+
|
|
59
69
|
if (
|
|
60
70
|
x?.bin !== undefined &&
|
|
61
71
|
y?.aggregate === "count"
|
|
@@ -80,7 +90,7 @@ export function inferBarColorLayout(layer) {
|
|
|
80
90
|
const grain = resolveBarGrain(layer);
|
|
81
91
|
if (grain === BAR_GRAINS.histogram) return "stack";
|
|
82
92
|
if (grain === BAR_GRAINS.aggregate) return "group";
|
|
83
|
-
if (grain === BAR_GRAINS.ranged) return "overlay";
|
|
93
|
+
if (grain === BAR_GRAINS.ranged || grain === BAR_GRAINS.centered) return "overlay";
|
|
84
94
|
return undefined;
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -89,6 +99,7 @@ export function resolveBarColorLayout(layer) {
|
|
|
89
99
|
if (layer?.encoding?.color?.layout !== undefined) {
|
|
90
100
|
return layer.encoding.color.layout;
|
|
91
101
|
}
|
|
102
|
+
if (resolveBarGrain(layer) === BAR_GRAINS.centered) return "overlay";
|
|
92
103
|
const channels = resolveBarChannels(layer);
|
|
93
104
|
if (layer?.encoding?.[channels?.measure]?.stack === "normalize") return "fill";
|
|
94
105
|
const offsetChannel = resolveBarOffsetChannel(layer);
|
package/src/grammar/bin.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
2
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import {
|
|
3
5
|
findHistogramBinIndex,
|
|
@@ -15,24 +17,12 @@ import {
|
|
|
15
17
|
|
|
16
18
|
const TRANSFORM_KEYS = Object.freeze([
|
|
17
19
|
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
18
|
-
"members", "as", "weight", "resolved"
|
|
20
|
+
"members", "as", "weight", "missing", "resolved"
|
|
19
21
|
]);
|
|
20
22
|
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
21
23
|
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
22
24
|
|
|
23
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
24
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
25
|
-
if (unknown !== undefined) {
|
|
26
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
25
|
|
|
30
|
-
function requireField(value, label) {
|
|
31
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
32
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
26
|
|
|
37
27
|
function normalizeAs(field, value, members) {
|
|
38
28
|
const requested = value ?? {};
|
|
@@ -59,7 +49,8 @@ export function normalizeBinTransform({
|
|
|
59
49
|
includeEmpty,
|
|
60
50
|
members,
|
|
61
51
|
as,
|
|
62
|
-
weight
|
|
52
|
+
weight,
|
|
53
|
+
missing
|
|
63
54
|
} = {}) {
|
|
64
55
|
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
65
56
|
if (selected.length > 1) {
|
|
@@ -83,7 +74,8 @@ export function normalizeBinTransform({
|
|
|
83
74
|
as: normalizeAs(field, as, resolvedMembers),
|
|
84
75
|
...(weight === undefined
|
|
85
76
|
? {}
|
|
86
|
-
: { weight: normalizeStatisticalWeight(weight, "Bin weight") })
|
|
77
|
+
: { weight: normalizeStatisticalWeight(weight, "Bin weight") }),
|
|
78
|
+
...(missing === undefined ? {} : { missing })
|
|
87
79
|
};
|
|
88
80
|
validateBinTransform(transform);
|
|
89
81
|
return cloneAndFreeze(transform);
|
|
@@ -101,6 +93,9 @@ export function validateBinTransform(transform) {
|
|
|
101
93
|
if (transform.weight !== undefined) {
|
|
102
94
|
normalizeStatisticalWeight(transform.weight, "Bin weight");
|
|
103
95
|
}
|
|
96
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
97
|
+
throw new Error('Bin missing must be "error" or "drop".');
|
|
98
|
+
}
|
|
104
99
|
normalizeHistogramBin(transform.bin);
|
|
105
100
|
if (
|
|
106
101
|
transform.extent !== "auto" &&
|
|
@@ -142,22 +137,31 @@ export function validateBinTransform(transform) {
|
|
|
142
137
|
return transform;
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
function readValues(rows, field) {
|
|
146
|
-
|
|
140
|
+
function readValues(rows, field, missing) {
|
|
141
|
+
const eligible = [];
|
|
142
|
+
let excluded = 0;
|
|
143
|
+
rows.forEach((row, index) => {
|
|
147
144
|
const value = row[field];
|
|
145
|
+
if (value === null || value === undefined) {
|
|
146
|
+
if (missing === "drop") { excluded += 1; return; }
|
|
147
|
+
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
148
|
+
}
|
|
148
149
|
if (!Number.isFinite(value)) {
|
|
149
150
|
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
+
eligible.push({ row, index });
|
|
152
153
|
});
|
|
154
|
+
return { eligible, excluded };
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
export function deriveBinRows(rows, transform) {
|
|
156
158
|
validateBinTransform(transform);
|
|
157
|
-
const
|
|
159
|
+
const selected = readValues(rows, transform.field, transform.missing);
|
|
160
|
+
const eligibleRows = selected.eligible.map(entry => entry.row);
|
|
161
|
+
const values = eligibleRows.map(row => row[transform.field]);
|
|
158
162
|
const weights = transform.weight === undefined
|
|
159
163
|
? undefined
|
|
160
|
-
: readStatisticalWeights(
|
|
164
|
+
: readStatisticalWeights(eligibleRows, transform.weight, "Bin");
|
|
161
165
|
if (weights !== undefined) {
|
|
162
166
|
validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
|
|
163
167
|
}
|
|
@@ -188,7 +192,7 @@ export function deriveBinRows(rows, transform) {
|
|
|
188
192
|
});
|
|
189
193
|
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
190
194
|
const entries = weightSummary === undefined
|
|
191
|
-
?
|
|
195
|
+
? selected.eligible
|
|
192
196
|
: weightSummary.positive;
|
|
193
197
|
entries.forEach(entry => {
|
|
194
198
|
const value = entry.row[transform.field];
|
|
@@ -217,7 +221,22 @@ export function deriveBinRows(rows, transform) {
|
|
|
217
221
|
domain: resolved.domain,
|
|
218
222
|
...(resolved.step === undefined ? {} : { step: resolved.step }),
|
|
219
223
|
boundaries: resolved.boundaries
|
|
220
|
-
}
|
|
224
|
+
},
|
|
225
|
+
...(transform.missing === undefined ? {} : {
|
|
226
|
+
report: {
|
|
227
|
+
version: 1,
|
|
228
|
+
owner: { kind: "data", id: "pending" },
|
|
229
|
+
inputs: [],
|
|
230
|
+
units: [{
|
|
231
|
+
role: transform.as.count,
|
|
232
|
+
group: {},
|
|
233
|
+
inputRows: rows.length,
|
|
234
|
+
usedRows: eligibleRows.length,
|
|
235
|
+
excludedRows: selected.excluded,
|
|
236
|
+
excludedByReason: selected.excluded === 0 ? {} : { "missing-value": selected.excluded }
|
|
237
|
+
}]
|
|
238
|
+
}
|
|
239
|
+
})
|
|
221
240
|
};
|
|
222
241
|
}
|
|
223
242
|
|
package/src/grammar/bin2d.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
1
2
|
import {
|
|
2
3
|
cloneAndFreeze,
|
|
3
4
|
isPlainObject
|
|
@@ -18,12 +19,6 @@ const RESOLVED_KEYS = Object.freeze([
|
|
|
18
19
|
"extent", "edges", "eligibleCount", "occupiedCount"
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
22
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
23
|
-
if (unknown !== undefined) {
|
|
24
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
22
|
|
|
28
23
|
function requirePositiveInteger(value, label) {
|
|
29
24
|
if (!Number.isInteger(value) || value <= 0) {
|
package/src/grammar/boxPlot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
-
import {
|
|
2
|
+
import { quantileSorted, stableDecimal } from "./numeric.js";
|
|
3
3
|
|
|
4
4
|
export const BOX_FIELDS = Object.freeze({
|
|
5
5
|
q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
|
|
@@ -9,7 +9,7 @@ export const BOX_FIELDS = Object.freeze({
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
const TRANSFORM_KEYS = [
|
|
12
|
-
"type", "category", "field", "method", "whisker", "factor", "as"
|
|
12
|
+
"type", "category", "field", "method", "whisker", "factor", "as", "summary"
|
|
13
13
|
];
|
|
14
14
|
const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
|
|
15
15
|
|
|
@@ -18,6 +18,36 @@ function field(value, label) {
|
|
|
18
18
|
return value;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const SUMMARY_KEYS = Object.freeze(["min", "q1", "median", "q3", "max"]);
|
|
22
|
+
|
|
23
|
+
export function normalizeBoxSummary(value) {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
if (!isPlainObject(value) || Object.keys(value).length !== SUMMARY_KEYS.length ||
|
|
26
|
+
!SUMMARY_KEYS.every(key => Object.hasOwn(value, key))) {
|
|
27
|
+
throw new TypeError("Box summary requires min, q1, median, q3 and max field names.");
|
|
28
|
+
}
|
|
29
|
+
return cloneAndFreeze(Object.fromEntries(SUMMARY_KEYS.map(key => [key, field(value[key], `Box summary.${key}`)])));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function derivePrecomputedBoxData(rows, transform) {
|
|
33
|
+
const { category, summary, as } = transform;
|
|
34
|
+
const summaries = [];
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const key = row[category];
|
|
37
|
+
if (key === undefined || key === null || key === "") continue;
|
|
38
|
+
const values = SUMMARY_KEYS.map(role => row[summary[role]]);
|
|
39
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Precomputed box summaries require finite values for all five fields.");
|
|
40
|
+
if (values.some((value, index) => index > 0 && value < values[index - 1])) {
|
|
41
|
+
throw new RangeError("Box summary must satisfy min <= q1 <= median <= q3 <= max.");
|
|
42
|
+
}
|
|
43
|
+
const [min, q1, median, q3, max] = values;
|
|
44
|
+
summaries.push({ [category]: key, [as.q1]: q1, [as.median]: median, [as.q3]: q3,
|
|
45
|
+
[as.lowerWhisker]: min, [as.upperWhisker]: max });
|
|
46
|
+
}
|
|
47
|
+
if (summaries.length === 0) throw new Error("Box transform requires at least one valid row.");
|
|
48
|
+
return cloneAndFreeze({ summaries, outliers: [] });
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
function validateOutputs(as, occupied) {
|
|
22
52
|
if (!isPlainObject(as)) throw new TypeError("Box as must be a plain object.");
|
|
23
53
|
const keys = Object.keys(as);
|
|
@@ -40,6 +70,18 @@ export function validateBoxTransform(value) {
|
|
|
40
70
|
}
|
|
41
71
|
field(value.category, "Box category field");
|
|
42
72
|
field(value.field, "Box measure field");
|
|
73
|
+
if (value.method === "precomputed") {
|
|
74
|
+
const summary = normalizeBoxSummary(value.summary);
|
|
75
|
+
if (summary === undefined || value.type !== "boxSummary" || value.field !== summary.median) {
|
|
76
|
+
throw new Error("Precomputed box summaries require a summary mapping and its median measure field.");
|
|
77
|
+
}
|
|
78
|
+
if (value.whisker !== undefined || value.factor !== undefined) {
|
|
79
|
+
throw new Error("Precomputed box summaries do not compute whiskers or factors.");
|
|
80
|
+
}
|
|
81
|
+
validateOutputs(value.as, new Set([value.category, ...Object.values(summary)]));
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (value.summary !== undefined) throw new Error("Box summary fields require the precomputed method.");
|
|
43
85
|
if (value.method !== "linear") throw new Error(`Unsupported box quantile method "${value.method}".`);
|
|
44
86
|
const whisker = value.whisker ?? "tukey";
|
|
45
87
|
if (!["tukey", "minmax"].includes(whisker)) {
|
|
@@ -56,6 +98,16 @@ export function validateBoxTransform(value) {
|
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
export function normalizeBoxTransform(options = {}) {
|
|
101
|
+
if (options.summary !== undefined) {
|
|
102
|
+
const summary = normalizeBoxSummary(options.summary);
|
|
103
|
+
const normalized = { type: options.type ?? "boxSummary", category: options.category,
|
|
104
|
+
field: options.field ?? summary.median, method: "precomputed", summary, as: options.as ?? BOX_FIELDS };
|
|
105
|
+
if (options.factor !== undefined || (options.whisker !== undefined && options.whisker !== "minmax")) {
|
|
106
|
+
throw new Error("Precomputed box summaries require minmax whiskers without a factor.");
|
|
107
|
+
}
|
|
108
|
+
validateBoxTransform(normalized);
|
|
109
|
+
return cloneAndFreeze(normalized);
|
|
110
|
+
}
|
|
59
111
|
const {
|
|
60
112
|
type = "boxSummary",
|
|
61
113
|
category,
|
|
@@ -79,16 +131,11 @@ export function normalizeBoxTransform(options = {}) {
|
|
|
79
131
|
return cloneAndFreeze(normalized);
|
|
80
132
|
}
|
|
81
133
|
|
|
82
|
-
function quantile(sorted, p) {
|
|
83
|
-
const position = (sorted.length - 1) * p;
|
|
84
|
-
const lower = Math.floor(position);
|
|
85
|
-
const upper = Math.ceil(position);
|
|
86
|
-
return interpolateNumber(sorted[lower], sorted[upper], position - lower);
|
|
87
|
-
}
|
|
88
134
|
|
|
89
135
|
export function deriveBoxData(rows, transform) {
|
|
90
136
|
if (!Array.isArray(rows)) throw new TypeError("Box rows must be an array.");
|
|
91
137
|
validateBoxTransform(transform);
|
|
138
|
+
if (transform.method === "precomputed") return derivePrecomputedBoxData(rows, transform);
|
|
92
139
|
const { category, field: measure, factor, as } = transform;
|
|
93
140
|
const whisker = transform.whisker ?? "tukey";
|
|
94
141
|
const groups = [];
|
|
@@ -111,7 +158,7 @@ export function deriveBoxData(rows, transform) {
|
|
|
111
158
|
for (const group of groups) {
|
|
112
159
|
const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
|
|
113
160
|
const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
|
|
114
|
-
stableDecimal(
|
|
161
|
+
stableDecimal(quantileSorted(sorted, probability))
|
|
115
162
|
);
|
|
116
163
|
const spread = q3 - q1;
|
|
117
164
|
const lowerFence = whisker === "minmax"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyField } from "../core/validation.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import { isNominalValue, readNominalField } from "./scales/fields.js";
|
|
3
4
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
@@ -7,12 +8,6 @@ export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
|
|
|
7
8
|
const DIRECTIONS = ["ascending", "descending"];
|
|
8
9
|
const AGGREGATES = ["sum", "mean", "min", "max"];
|
|
9
10
|
|
|
10
|
-
function nonEmptyField(value, label) {
|
|
11
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
12
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
11
|
|
|
17
12
|
const sameValue = Object.is;
|
|
18
13
|
|
package/src/grammar/complete.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
2
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
|
|
3
5
|
const TRANSFORM_KEYS = Object.freeze([
|
|
@@ -6,17 +8,7 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
8
|
const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
|
|
7
9
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
8
10
|
|
|
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
11
|
|
|
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
12
|
|
|
21
13
|
function scalarType(value, label) {
|
|
22
14
|
if (value === null) return "null";
|
|
@@ -258,20 +250,24 @@ export function deriveCompleteRows(rows, transform) {
|
|
|
258
250
|
if (!allowed.has(identity)) {
|
|
259
251
|
throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
|
|
260
252
|
}
|
|
261
|
-
if (lookup.has(identity))
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
if (!lookup.has(identity)) lookup.set(identity, []);
|
|
254
|
+
lookup.get(identity).push(entry);
|
|
255
|
+
}
|
|
256
|
+
const groupSize = group.values.length + domain.length - lookup.size;
|
|
257
|
+
if (output.length + groupSize > MAX_OUTPUT_ROWS) {
|
|
258
|
+
throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
265
259
|
}
|
|
266
260
|
for (const keyValue of domain) {
|
|
267
261
|
const identity = scalarKey(keyValue, "Complete domain value");
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
262
|
+
const entries = lookup.get(identity);
|
|
263
|
+
if (entries === undefined) {
|
|
270
264
|
output.push(syntheticRow(fields, group, keyValue, transform));
|
|
271
265
|
} else {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const row = { ...entry.row };
|
|
268
|
+
if (transform.members !== undefined) define(row, transform.members, [entry.index]);
|
|
269
|
+
output.push(row);
|
|
270
|
+
}
|
|
275
271
|
}
|
|
276
272
|
}
|
|
277
273
|
}
|