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
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
readQuantitativeField,
|
|
5
|
-
readTemporalField
|
|
6
|
-
} from "./scales/index.js";
|
|
7
|
-
import {
|
|
8
|
-
layoutSeriesPartition,
|
|
9
|
-
validateColorLayout
|
|
10
|
-
} from "./seriesLayout.js";
|
|
2
|
+
import { readNominalField, readQuantitativeField, readTemporalField } from "./scales/index.js";
|
|
3
|
+
import { layoutSeriesPartition, validateColorLayout } from "./seriesLayout.js";
|
|
11
4
|
import { stableOrderPathValues } from "./pathOrder.js";
|
|
12
5
|
import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
|
|
6
|
+
import { validatePathSeriesAppearance } from "./pathSeries.js";
|
|
7
|
+
import { readAreaEndpoint, validateAreaEndpointPair } from "./areaEndpoints.js";
|
|
8
|
+
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
13
9
|
|
|
14
10
|
function indexSegments(segments) {
|
|
15
11
|
const indexed = [];
|
|
@@ -21,7 +17,7 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
21
17
|
if (layer?.mark?.type !== "area") {
|
|
22
18
|
throw new Error("Area series derivation requires a semantic area mark.");
|
|
23
19
|
}
|
|
24
|
-
const { x, y, x2, y2
|
|
20
|
+
const { x, y, x2, y2 } = layer.encoding ?? {};
|
|
25
21
|
const vertical =
|
|
26
22
|
["quantitative", "temporal"].includes(x?.fieldType) &&
|
|
27
23
|
y?.fieldType === "quantitative" &&
|
|
@@ -37,26 +33,19 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
37
33
|
`Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
|
|
38
34
|
);
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
throw new Error(`Area group encoding on mark "${layer.id}" must be nominal.`);
|
|
42
|
-
}
|
|
36
|
+
const grouping = validatePathSeriesAppearance(rows, layer);
|
|
43
37
|
const orientation = vertical ? "vertical" : "horizontal";
|
|
44
38
|
const independent = vertical ? x : y;
|
|
45
39
|
const independentValues = independent.fieldType === "temporal"
|
|
46
|
-
? readTemporalField(rows, independent.field)
|
|
40
|
+
? readTemporalField(rows, independent.field, independent.temporalUnit)
|
|
47
41
|
: readQuantitativeField(rows, independent.field);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
? rows.map(() => undefined)
|
|
56
|
-
: readNominalField(rows, group.field);
|
|
57
|
-
const colorValues = color === undefined
|
|
58
|
-
? rows.map(() => undefined)
|
|
59
|
-
: readNominalField(rows, color.field);
|
|
42
|
+
if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
|
|
43
|
+
const primary = vertical ? y : x;
|
|
44
|
+
const secondary = vertical ? y2 : x2;
|
|
45
|
+
validateAreaEndpointPair(primary, secondary);
|
|
46
|
+
const missing = layer.mark.missing ?? "error";
|
|
47
|
+
const lower = readAreaEndpoint(rows, primary, missing);
|
|
48
|
+
const upper = readAreaEndpoint(rows, secondary, missing);
|
|
60
49
|
const pathOrder = layer.encoding?.pathOrder;
|
|
61
50
|
const orderValues = pathOrder === undefined
|
|
62
51
|
? undefined
|
|
@@ -64,32 +53,24 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
64
53
|
const groups = new Map();
|
|
65
54
|
|
|
66
55
|
for (let index = 0; index < rows.length; index += 1) {
|
|
67
|
-
const
|
|
56
|
+
const tuple = grouping.map(field => rows[index][field]);
|
|
57
|
+
const key = JSON.stringify(tuple);
|
|
68
58
|
const series = groups.get(key) ?? {
|
|
69
|
-
key:
|
|
70
|
-
...(group === undefined ? {} : { [group.field]: key }),
|
|
71
|
-
...(color === undefined ? {} : { [color.field]: colorValues[index] })
|
|
72
|
-
},
|
|
59
|
+
key: Object.fromEntries(grouping.map((field, i) => [field, tuple[i]])),
|
|
73
60
|
values: []
|
|
74
61
|
};
|
|
75
|
-
if (
|
|
76
|
-
color !== undefined &&
|
|
77
|
-
!Object.is(series.key[color.field], colorValues[index])
|
|
78
|
-
) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Area series "${String(key)}" must have one color value.`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
62
|
series.values.push(vertical
|
|
84
63
|
? {
|
|
85
64
|
x: independentValues[index],
|
|
86
65
|
y: lower[index],
|
|
87
66
|
y2: upper[index],
|
|
67
|
+
...(missing === "break" ? { sourceIndex: index } : {}),
|
|
88
68
|
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
89
69
|
}
|
|
90
70
|
: {
|
|
91
71
|
x: lower[index],
|
|
92
72
|
x2: upper[index],
|
|
73
|
+
...(missing === "break" ? { sourceIndex: index } : {}),
|
|
93
74
|
y: independentValues[index],
|
|
94
75
|
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
95
76
|
});
|
|
@@ -98,7 +79,8 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
98
79
|
if (groups.size === 0) {
|
|
99
80
|
throw new Error(`Area mark "${layer.id}" has no values.`);
|
|
100
81
|
}
|
|
101
|
-
|
|
82
|
+
layoutRawAreaGroups(groups, { layer, vertical, primary, secondary });
|
|
83
|
+
const series = [...groups.values()].flatMap(item => {
|
|
102
84
|
const key = vertical ? "x" : "y";
|
|
103
85
|
const values = pathOrder === undefined
|
|
104
86
|
? item.values.sort((left, right) => left[key] - right[key])
|
|
@@ -107,6 +89,22 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
107
89
|
item.values.map(value => value.pathOrder),
|
|
108
90
|
pathOrder.order
|
|
109
91
|
);
|
|
92
|
+
if (missing === "break") {
|
|
93
|
+
const segments = [];
|
|
94
|
+
let segment = [];
|
|
95
|
+
const emit = () => {
|
|
96
|
+
if (segment.length >= 2) segments.push({ key: item.key,
|
|
97
|
+
sourceIndices: segment.map(value => value.sourceIndex),
|
|
98
|
+
values: segment.map(({ sourceIndex, ...value }) => value) });
|
|
99
|
+
segment = [];
|
|
100
|
+
};
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
if ((vertical ? value.y == null || value.y2 == null : value.x == null || value.x2 == null)) emit();
|
|
103
|
+
else segment.push(value);
|
|
104
|
+
}
|
|
105
|
+
emit();
|
|
106
|
+
return segments;
|
|
107
|
+
}
|
|
110
108
|
if (values.length < 2) {
|
|
111
109
|
throw new Error(
|
|
112
110
|
`Area series on mark "${layer.id}" requires at least two points.`
|
|
@@ -114,6 +112,7 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
114
112
|
}
|
|
115
113
|
return { key: item.key, values };
|
|
116
114
|
});
|
|
115
|
+
if (series.length === 0) throw new Error(`Area mark "${layer.id}" has no valid segment with at least two points.`);
|
|
117
116
|
return cloneAndFreeze({
|
|
118
117
|
orientation,
|
|
119
118
|
xValues: series.flatMap(item => item.values.flatMap(value =>
|
|
@@ -151,7 +150,7 @@ export function deriveCenteredAreaSeries(rows, layer) {
|
|
|
151
150
|
);
|
|
152
151
|
}
|
|
153
152
|
const xValues = x.fieldType === "temporal"
|
|
154
|
-
? readTemporalField(rows, x.field)
|
|
153
|
+
? readTemporalField(rows, x.field, x.temporalUnit)
|
|
155
154
|
: readQuantitativeField(rows, x.field);
|
|
156
155
|
const yValues = readQuantitativeField(rows, y.field);
|
|
157
156
|
const groups = readNominalField(rows, group.field);
|
|
@@ -359,3 +358,50 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
|
|
|
359
358
|
}))
|
|
360
359
|
});
|
|
361
360
|
}
|
|
361
|
+
|
|
362
|
+
export function layoutRawAreaGroups(groups, { layer, vertical, primary, secondary }) {
|
|
363
|
+
const mode = layer.layout?.mode ?? "overlay";
|
|
364
|
+
validateColorLayout(mode);
|
|
365
|
+
if (mode === "overlay") return;
|
|
366
|
+
if (mode === "group" || (mode === "center" && !vertical)) {
|
|
367
|
+
throw new Error(`Area layout "${mode}" is incompatible with this orientation.`);
|
|
368
|
+
}
|
|
369
|
+
const primaryDatum = Object.hasOwn(primary, "datum");
|
|
370
|
+
const secondaryDatum = Object.hasOwn(secondary, "datum");
|
|
371
|
+
if (primaryDatum === secondaryDatum || (primaryDatum ? primary.datum : secondary.datum) !== 0) {
|
|
372
|
+
throw new Error("Stacked area requires one value field and one zero datum endpoint.");
|
|
373
|
+
}
|
|
374
|
+
const position = vertical ? "x" : "y";
|
|
375
|
+
const lower = vertical ? "y" : "x", upper = `${lower}2`;
|
|
376
|
+
const measure = primaryDatum ? upper : lower;
|
|
377
|
+
const series = [...groups.values()];
|
|
378
|
+
const indices = series.map(item => {
|
|
379
|
+
const index = new Map();
|
|
380
|
+
for (const value of item.values) {
|
|
381
|
+
if (index.has(value[position])) throw new Error("Area layout requires unique group/position rows.");
|
|
382
|
+
index.set(value[position], value);
|
|
383
|
+
}
|
|
384
|
+
return index;
|
|
385
|
+
});
|
|
386
|
+
const positions = [...new Set(series.flatMap(item => item.values.map(value => value[position])))].sort((a, b) => a - b);
|
|
387
|
+
validateGeneratedItemLimit(positions.length * series.length, "Area layout cell count");
|
|
388
|
+
if (indices.some(index => index.size !== positions.length || positions.some(value => !index.has(value)))) {
|
|
389
|
+
throw new Error("Area layout requires an aligned row for every group and position.");
|
|
390
|
+
}
|
|
391
|
+
for (const position of positions) {
|
|
392
|
+
const points = indices.map(index => index.get(position));
|
|
393
|
+
if (points.some(point => point[measure] == null)) {
|
|
394
|
+
for (const point of points) { point[lower] = null; point[upper] = null; }
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const segments = layoutSeriesPartition(points.map(point => point[measure]), mode);
|
|
398
|
+
const byIndex = new Map(segments.map(segment => [segment.index, segment]));
|
|
399
|
+
let endpoint = mode === "center" ? segments[0]?.start ?? 0 : 0;
|
|
400
|
+
points.forEach((point, index) => {
|
|
401
|
+
const segment = byIndex.get(index);
|
|
402
|
+
point[lower] = segment?.start ?? (mode === "diverging" ? 0 : endpoint);
|
|
403
|
+
point[upper] = segment?.end ?? (mode === "diverging" ? 0 : endpoint);
|
|
404
|
+
if (segment !== undefined && mode !== "diverging") endpoint = segment.end;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { readSeriesIdentity } from "../pathSeries.js";
|
|
1
2
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
3
|
import { readNominalField, readTemporalField } from "../scales/index.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
validateAggregateFieldValues,
|
|
7
|
-
} from "../aggregate.js";
|
|
8
|
-
import {
|
|
9
|
-
BAR_GRAINS,
|
|
10
|
-
resolveBarChannels,
|
|
11
|
-
resolveBarGrain
|
|
12
|
-
} from "./policy.js";
|
|
4
|
+
import { aggregateRows, validateAggregateFieldType, validateAggregateFieldValues } from "../aggregate.js";
|
|
5
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain, resolveBarColorLayout } from "./policy.js";
|
|
6
|
+
import { layoutSeriesPartition } from "../seriesLayout.js";
|
|
13
7
|
|
|
14
8
|
function requireAggregateBarEncoding(layer) {
|
|
15
9
|
if (layer?.mark?.type !== "bar") {
|
|
@@ -33,7 +27,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
33
27
|
const { channels, category, measure } = requireAggregateBarEncoding(layer);
|
|
34
28
|
validateAggregateFieldValues(rows, measure.field, measure.fieldType);
|
|
35
29
|
const categoryValues = category.fieldType === "temporal"
|
|
36
|
-
? readTemporalField(rows, category.field)
|
|
30
|
+
? readTemporalField(rows, category.field, category.temporalUnit)
|
|
37
31
|
: readNominalField(rows, category.field);
|
|
38
32
|
const color = layer.encoding?.color;
|
|
39
33
|
let colorValues;
|
|
@@ -52,18 +46,26 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
52
46
|
);
|
|
53
47
|
}
|
|
54
48
|
}
|
|
49
|
+
const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
|
|
55
50
|
const groups = new Map();
|
|
56
51
|
|
|
57
52
|
for (let index = 0; index < rows.length; index += 1) {
|
|
58
53
|
const categoryValue = categoryValues[index];
|
|
59
54
|
const colorValue = colorValues?.[index];
|
|
60
|
-
const
|
|
55
|
+
const seriesValue = identity?.values[index];
|
|
56
|
+
const key = JSON.stringify([categoryValue, identity === undefined ? colorValue : seriesValue]);
|
|
61
57
|
const group = groups.get(key) ?? {
|
|
62
58
|
category: categoryValue,
|
|
59
|
+
...(identity === undefined ? {} : { series: seriesValue }),
|
|
63
60
|
...(colorValues === undefined ? {} : { color: colorValue }),
|
|
64
|
-
rows: []
|
|
61
|
+
rows: [],
|
|
62
|
+
sourceIndices: []
|
|
65
63
|
};
|
|
64
|
+
if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
|
|
65
|
+
throw new Error("Bar color requires one categorical value within each aggregate cell.");
|
|
66
|
+
}
|
|
66
67
|
group.rows.push(rows[index]);
|
|
68
|
+
group.sourceIndices.push(index);
|
|
67
69
|
groups.set(key, group);
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -82,6 +84,8 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
82
84
|
x: channels.category === "x" ? group.category : value,
|
|
83
85
|
y: channels.category === "y" ? group.category : value,
|
|
84
86
|
...(color === undefined ? {} : { color: aggregateColor }),
|
|
87
|
+
...(identity === undefined ? {} : { series: group.series }),
|
|
88
|
+
sourceIndices: group.sourceIndices,
|
|
85
89
|
count: group.rows.length
|
|
86
90
|
}];
|
|
87
91
|
});
|
|
@@ -96,3 +100,23 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
96
100
|
values
|
|
97
101
|
});
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
export function deriveSeriesBarCells(rows, layer, categoryDomain, seriesDomain) {
|
|
105
|
+
const channels = resolveBarChannels(layer);
|
|
106
|
+
const values = deriveBarAggregates(rows, layer).values;
|
|
107
|
+
const identity = readSeriesIdentity(rows, layer);
|
|
108
|
+
const categories = categoryDomain ?? [...new Set(values.map(cell => cell[channels.category]))].sort((a, b) => a - b);
|
|
109
|
+
const seriesKey = cell => layer.encoding.group !== undefined ? cell.series : layer.encoding.color?.fieldType === "quantitative" ? undefined : cell.color;
|
|
110
|
+
const series = seriesDomain ?? (layer.encoding.group !== undefined ? identity.domain : [...new Set(values.map(seriesKey))]);
|
|
111
|
+
const cells = new Map(values.map(cell => [JSON.stringify([cell[channels.category], seriesKey(cell)]), cell]));
|
|
112
|
+
const layout = resolveBarColorLayout(layer);
|
|
113
|
+
return categories.flatMap(category => {
|
|
114
|
+
const partition = series.map(key => cells.get(JSON.stringify([category, key])));
|
|
115
|
+
const segments = layout === "group"
|
|
116
|
+
? partition.flatMap((cell, index) => cell === undefined ? [] : [{ index, start: 0, end: cell[channels.measure] }])
|
|
117
|
+
: layoutSeriesPartition(partition.map(cell => cell?.[channels.measure] ?? 0), layout);
|
|
118
|
+
return segments.flatMap(segment => partition[segment.index] === undefined ? [] : [{
|
|
119
|
+
cell: partition[segment.index], start: segment.start, end: segment.end, seriesIndex: segment.index
|
|
120
|
+
}]);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -45,6 +45,50 @@ export function normalizeOffsetPadding(options = {}, existing, channel = "xOffse
|
|
|
45
45
|
return cloneAndFreeze({ paddingInner, paddingOuter });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function normalizeOffsetScalePolicy(
|
|
49
|
+
options = {},
|
|
50
|
+
existing,
|
|
51
|
+
channel = "xOffset"
|
|
52
|
+
) {
|
|
53
|
+
const hasPadding = Object.hasOwn(options, "padding");
|
|
54
|
+
const hasInner = Object.hasOwn(options, "paddingInner");
|
|
55
|
+
const hasOuter = Object.hasOwn(options, "paddingOuter");
|
|
56
|
+
if (hasPadding && (hasInner || hasOuter)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${channel} padding cannot be combined with paddingInner or paddingOuter.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const padding = hasPadding ? options.padding : undefined;
|
|
62
|
+
if (hasPadding && (!Number.isFinite(padding) || padding < 0 || padding >= 1)) {
|
|
63
|
+
throw new RangeError(
|
|
64
|
+
`${channel} padding must be from 0 (inclusive) to 1 (exclusive).`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const paddingInner = hasPadding
|
|
68
|
+
? padding
|
|
69
|
+
: hasInner ? options.paddingInner : existing?.paddingInner ?? 0;
|
|
70
|
+
const paddingOuter = hasPadding
|
|
71
|
+
? padding
|
|
72
|
+
: hasOuter ? options.paddingOuter : existing?.paddingOuter ?? 0;
|
|
73
|
+
const align = Object.hasOwn(options, "align")
|
|
74
|
+
? options.align
|
|
75
|
+
: existing?.align ?? 0.5;
|
|
76
|
+
if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
|
|
77
|
+
throw new RangeError(
|
|
78
|
+
`${channel} paddingInner must be from 0 (inclusive) to 1 (exclusive).`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
|
|
82
|
+
throw new RangeError(
|
|
83
|
+
`${channel} paddingOuter must be a non-negative finite number.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isFinite(align) || align < 0 || align > 1) {
|
|
87
|
+
throw new RangeError(`${channel} align must be between 0 and 1.`);
|
|
88
|
+
}
|
|
89
|
+
return cloneAndFreeze({ paddingInner, paddingOuter, align });
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
export function resolveBarWidth(config, slotBandwidth) {
|
|
49
93
|
if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
|
|
50
94
|
throw new Error("Bar width requires a positive resolved slot bandwidth.");
|
|
@@ -11,7 +11,7 @@ export const BAR_ORIENTATIONS = Object.freeze({
|
|
|
11
11
|
horizontal: "horizontal"
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
export function isBarCategoryEncoding(encoding) {
|
|
15
15
|
return ["nominal", "ordinal", "temporal"].includes(encoding?.fieldType);
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -27,14 +27,14 @@ export function resolveBarOrientation(layer) {
|
|
|
27
27
|
if (x?.bin !== undefined && y?.aggregate === "count") {
|
|
28
28
|
return BAR_ORIENTATIONS.vertical;
|
|
29
29
|
}
|
|
30
|
-
if (
|
|
30
|
+
if (isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
|
|
31
31
|
return BAR_ORIENTATIONS.vertical;
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
33
|
+
if (isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative") {
|
|
34
34
|
return BAR_ORIENTATIONS.horizontal;
|
|
35
35
|
}
|
|
36
|
-
if (
|
|
37
|
-
if (isMeasure(x) &&
|
|
36
|
+
if (isBarCategoryEncoding(x) && isMeasure(y)) return BAR_ORIENTATIONS.vertical;
|
|
37
|
+
if (isMeasure(x) && isBarCategoryEncoding(y)) return BAR_ORIENTATIONS.horizontal;
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -63,8 +63,8 @@ export function resolveBarGrain(layer) {
|
|
|
63
63
|
return BAR_GRAINS.histogram;
|
|
64
64
|
}
|
|
65
65
|
if (
|
|
66
|
-
(
|
|
67
|
-
(
|
|
66
|
+
(isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") ||
|
|
67
|
+
(isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative")
|
|
68
68
|
) return BAR_GRAINS.ranged;
|
|
69
69
|
if (resolveBarOrientation(layer) !== undefined) {
|
|
70
70
|
return BAR_GRAINS.aggregate;
|
|
@@ -73,6 +73,7 @@ export function resolveBarGrain(layer) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function inferBarColorLayout(layer) {
|
|
76
|
+
if (layer?.layout?.mode !== undefined) return layer.layout.mode;
|
|
76
77
|
if (layer?.encoding?.color?.layout !== undefined) {
|
|
77
78
|
return layer.encoding.color.layout;
|
|
78
79
|
}
|
|
@@ -84,6 +85,7 @@ export function inferBarColorLayout(layer) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export function resolveBarColorLayout(layer) {
|
|
88
|
+
if (layer?.layout?.mode !== undefined) return layer.layout.mode;
|
|
87
89
|
if (layer?.encoding?.color?.layout !== undefined) {
|
|
88
90
|
return layer.encoding.color.layout;
|
|
89
91
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
findHistogramBinIndex,
|
|
4
|
+
normalizeHistogramBin,
|
|
5
|
+
resolveHistogramBins
|
|
6
|
+
} from "./histogram.js";
|
|
7
|
+
import {
|
|
8
|
+
normalizeStatisticalWeight,
|
|
9
|
+
readStatisticalWeights,
|
|
10
|
+
sumEntryWeights,
|
|
11
|
+
summarizeStatisticalWeights,
|
|
12
|
+
validateWeightedNumericFields,
|
|
13
|
+
weightedRows
|
|
14
|
+
} from "./weightedStatistics.js";
|
|
15
|
+
|
|
16
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
17
|
+
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
18
|
+
"members", "as", "weight", "resolved"
|
|
19
|
+
]);
|
|
20
|
+
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
21
|
+
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
22
|
+
|
|
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
|
+
|
|
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
|
+
|
|
37
|
+
function normalizeAs(field, value, members) {
|
|
38
|
+
const requested = value ?? {};
|
|
39
|
+
if (!isPlainObject(requested)) return requested;
|
|
40
|
+
const result = {
|
|
41
|
+
...requested,
|
|
42
|
+
lower: requested.lower ?? `${field}_start`,
|
|
43
|
+
upper: requested.upper ?? `${field}_end`,
|
|
44
|
+
count: requested.count ?? "count"
|
|
45
|
+
};
|
|
46
|
+
if (members) result.members = requested.members ?? "members";
|
|
47
|
+
else if (Object.hasOwn(requested, "members")) result.members = requested.members;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeBinTransform({
|
|
52
|
+
field,
|
|
53
|
+
maxBins,
|
|
54
|
+
step,
|
|
55
|
+
boundaries,
|
|
56
|
+
extent,
|
|
57
|
+
nice,
|
|
58
|
+
zero,
|
|
59
|
+
includeEmpty,
|
|
60
|
+
members,
|
|
61
|
+
as,
|
|
62
|
+
weight
|
|
63
|
+
} = {}) {
|
|
64
|
+
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
65
|
+
if (selected.length > 1) {
|
|
66
|
+
throw new Error("Bin data accepts only one of maxBins, step, or boundaries.");
|
|
67
|
+
}
|
|
68
|
+
const bin = normalizeHistogramBin(
|
|
69
|
+
boundaries !== undefined ? { boundaries }
|
|
70
|
+
: step !== undefined ? { step }
|
|
71
|
+
: { maxBins: maxBins ?? 10 }
|
|
72
|
+
);
|
|
73
|
+
const resolvedMembers = members ?? false;
|
|
74
|
+
const transform = {
|
|
75
|
+
type: "bin",
|
|
76
|
+
field,
|
|
77
|
+
bin,
|
|
78
|
+
extent: extent ?? "auto",
|
|
79
|
+
nice: nice ?? true,
|
|
80
|
+
zero: zero ?? false,
|
|
81
|
+
includeEmpty: includeEmpty ?? true,
|
|
82
|
+
members: resolvedMembers,
|
|
83
|
+
as: normalizeAs(field, as, resolvedMembers),
|
|
84
|
+
...(weight === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { weight: normalizeStatisticalWeight(weight, "Bin weight") })
|
|
87
|
+
};
|
|
88
|
+
validateBinTransform(transform);
|
|
89
|
+
return cloneAndFreeze(transform);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function validateBinTransform(transform) {
|
|
93
|
+
if (!isPlainObject(transform)) {
|
|
94
|
+
throw new TypeError("Bin transform must be a plain object.");
|
|
95
|
+
}
|
|
96
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "bin transform");
|
|
97
|
+
if (transform.type !== "bin") {
|
|
98
|
+
throw new Error(`Unsupported bin transform "${transform.type}".`);
|
|
99
|
+
}
|
|
100
|
+
requireField(transform.field, "Bin field");
|
|
101
|
+
if (transform.weight !== undefined) {
|
|
102
|
+
normalizeStatisticalWeight(transform.weight, "Bin weight");
|
|
103
|
+
}
|
|
104
|
+
normalizeHistogramBin(transform.bin);
|
|
105
|
+
if (
|
|
106
|
+
transform.extent !== "auto" &&
|
|
107
|
+
(!Array.isArray(transform.extent) || transform.extent.length !== 2 ||
|
|
108
|
+
!transform.extent.every(Number.isFinite) || transform.extent[0] >= transform.extent[1])
|
|
109
|
+
) {
|
|
110
|
+
throw new TypeError("Bin extent must be auto or two ascending finite numbers.");
|
|
111
|
+
}
|
|
112
|
+
for (const property of ["nice", "zero", "includeEmpty", "members"]) {
|
|
113
|
+
if (typeof transform[property] !== "boolean") {
|
|
114
|
+
throw new TypeError(`Bin ${property} must be a boolean.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!isPlainObject(transform.as)) {
|
|
118
|
+
throw new TypeError("Bin as must be a plain object.");
|
|
119
|
+
}
|
|
120
|
+
rejectUnknownKeys(transform.as, AS_KEYS, "bin as");
|
|
121
|
+
const required = ["lower", "upper", "count", ...(transform.members ? ["members"] : [])];
|
|
122
|
+
if (!transform.members && Object.hasOwn(transform.as, "members")) {
|
|
123
|
+
throw new Error("Bin as.members requires members: true.");
|
|
124
|
+
}
|
|
125
|
+
const outputs = required.map(key => requireField(transform.as[key], `Bin as.${key}`));
|
|
126
|
+
if (new Set(outputs).size !== outputs.length) {
|
|
127
|
+
throw new Error("Bin output fields must be unique.");
|
|
128
|
+
}
|
|
129
|
+
if (transform.resolved !== undefined) {
|
|
130
|
+
if (!isPlainObject(transform.resolved)) {
|
|
131
|
+
throw new TypeError("Bin resolved must be a plain object.");
|
|
132
|
+
}
|
|
133
|
+
rejectUnknownKeys(transform.resolved, RESOLVED_KEYS, "bin resolved");
|
|
134
|
+
resolveHistogramBins({
|
|
135
|
+
values: transform.resolved.boundaries ?? [],
|
|
136
|
+
bin: { boundaries: transform.resolved.boundaries },
|
|
137
|
+
domain: transform.resolved.domain,
|
|
138
|
+
nice: transform.nice,
|
|
139
|
+
zero: transform.zero
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return transform;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readValues(rows, field) {
|
|
146
|
+
return rows.map((row, index) => {
|
|
147
|
+
const value = row[field];
|
|
148
|
+
if (!Number.isFinite(value)) {
|
|
149
|
+
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function deriveBinRows(rows, transform) {
|
|
156
|
+
validateBinTransform(transform);
|
|
157
|
+
const values = readValues(rows, transform.field);
|
|
158
|
+
const weights = transform.weight === undefined
|
|
159
|
+
? undefined
|
|
160
|
+
: readStatisticalWeights(rows, transform.weight, "Bin");
|
|
161
|
+
if (weights !== undefined) {
|
|
162
|
+
validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
|
|
163
|
+
}
|
|
164
|
+
const weightSummary = weights === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: summarizeStatisticalWeights(
|
|
167
|
+
weights.entries,
|
|
168
|
+
weights.definition.kind,
|
|
169
|
+
"Bin data"
|
|
170
|
+
);
|
|
171
|
+
const contributingValues = weightSummary === undefined
|
|
172
|
+
? values
|
|
173
|
+
: weightSummary.positive.map(entry => entry.row[transform.field]);
|
|
174
|
+
if (
|
|
175
|
+
transform.extent !== "auto" &&
|
|
176
|
+
contributingValues.some(value =>
|
|
177
|
+
value < transform.extent[0] || value > transform.extent[1]
|
|
178
|
+
)
|
|
179
|
+
) {
|
|
180
|
+
throw new RangeError("Bin extent must contain the histogram data extent.");
|
|
181
|
+
}
|
|
182
|
+
const resolved = resolveHistogramBins({
|
|
183
|
+
values: contributingValues,
|
|
184
|
+
bin: transform.bin,
|
|
185
|
+
domain: transform.extent,
|
|
186
|
+
nice: transform.nice,
|
|
187
|
+
zero: transform.zero
|
|
188
|
+
});
|
|
189
|
+
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
190
|
+
const entries = weightSummary === undefined
|
|
191
|
+
? rows.map((row, index) => ({ row, index }))
|
|
192
|
+
: weightSummary.positive;
|
|
193
|
+
entries.forEach(entry => {
|
|
194
|
+
const value = entry.row[transform.field];
|
|
195
|
+
const bin = findHistogramBinIndex(value, resolved.boundaries);
|
|
196
|
+
if (bin !== -1) members[bin].push(entry);
|
|
197
|
+
});
|
|
198
|
+
const output = members.flatMap((group, index) =>
|
|
199
|
+
!transform.includeEmpty && group.length === 0 ? [] : [{
|
|
200
|
+
[transform.as.lower]: resolved.boundaries[index],
|
|
201
|
+
[transform.as.upper]: resolved.boundaries[index + 1],
|
|
202
|
+
[transform.as.count]: weightSummary === undefined
|
|
203
|
+
? group.length
|
|
204
|
+
: sumEntryWeights(group, weightSummary, "Bin mass"),
|
|
205
|
+
...(transform.members
|
|
206
|
+
? {
|
|
207
|
+
[transform.as.members]: weightSummary === undefined
|
|
208
|
+
? group.map(entry => entry.row)
|
|
209
|
+
: weightedRows({ positive: group })
|
|
210
|
+
}
|
|
211
|
+
: {})
|
|
212
|
+
}]
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
values: output,
|
|
216
|
+
resolved: {
|
|
217
|
+
domain: resolved.domain,
|
|
218
|
+
...(resolved.step === undefined ? {} : { step: resolved.step }),
|
|
219
|
+
boundaries: resolved.boundaries
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function resolveBinTransform(result, transform) {
|
|
225
|
+
return cloneAndFreeze([{ ...transform, resolved: result.resolved }]);
|
|
226
|
+
}
|
|
@@ -2,6 +2,8 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
|
2
2
|
import { isNominalValue, readNominalField } from "./scales/fields.js";
|
|
3
3
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
4
4
|
|
|
5
|
+
export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
|
|
6
|
+
|
|
5
7
|
const DIRECTIONS = ["ascending", "descending"];
|
|
6
8
|
const AGGREGATES = ["sum", "mean", "min", "max"];
|
|
7
9
|
|
|
@@ -130,3 +132,35 @@ export function resolveCategoryOrder(rows, categoryField, order) {
|
|
|
130
132
|
return direction * comparison || first.get(left) - first.get(right);
|
|
131
133
|
}));
|
|
132
134
|
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
export function normalizeLegendOrder(order) {
|
|
138
|
+
if (order === "scale") return order;
|
|
139
|
+
if (!isPlainObject(order)) {
|
|
140
|
+
throw new TypeError('Legend order must be "scale", { values }, or { channel }.');
|
|
141
|
+
}
|
|
142
|
+
const keys = Object.keys(order);
|
|
143
|
+
if (keys.length !== 1 || !["values", "channel"].includes(keys[0])) {
|
|
144
|
+
throw new Error("Legend order requires exactly one of values or channel.");
|
|
145
|
+
}
|
|
146
|
+
if (keys[0] === "values") return normalizeCategoryOrder({ values: order.values });
|
|
147
|
+
if (!CATEGORY_ORDER_CHANNELS.includes(order.channel)) {
|
|
148
|
+
throw new Error("Legend order channel must be x, y, or theta.");
|
|
149
|
+
}
|
|
150
|
+
return cloneAndFreeze({ channel: order.channel });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveLegendOrderDomain(domain, order, linked, observed = domain) {
|
|
154
|
+
if (order === undefined || order === "scale") return domain;
|
|
155
|
+
const normalized = normalizeLegendOrder(order);
|
|
156
|
+
if (normalized.values !== undefined) {
|
|
157
|
+
const seen = [...new Set(observed)].filter(value => domain.some(item => Object.is(item, value)));
|
|
158
|
+
const available = [...seen, ...domain.filter(value => !seen.some(item => Object.is(item, value)))];
|
|
159
|
+
return resolveCategoryOrder(available.map(value => ({ value })), "value", normalized);
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(linked) || linked.length !== domain.length ||
|
|
162
|
+
!domain.every(value => linked.some(other => Object.is(value, other)))) {
|
|
163
|
+
throw new Error("Linked legend order requires the same categorical domain.");
|
|
164
|
+
}
|
|
165
|
+
return cloneAndFreeze(linked);
|
|
166
|
+
}
|