ggaction 0.0.12 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +20 -11
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -3
- package/src/actions/basic.js +4 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +13 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +18 -6
- package/src/actions/charts/area.js +51 -0
- package/src/actions/charts/bar.js +13 -6
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +48 -0
- package/src/actions/charts/ecdf.js +186 -0
- package/src/actions/charts/endpoints.js +473 -0
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +73 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +172 -0
- package/src/actions/charts/line.js +8 -6
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +52 -0
- package/src/actions/charts/polar.js +134 -0
- package/src/actions/charts/radar.js +305 -0
- package/src/actions/charts/radial.js +50 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +283 -0
- package/src/actions/charts/scatter.js +11 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +129 -3
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +111 -15
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +226 -30
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +38 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +19 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +30 -23
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +124 -34
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +6 -0
- package/src/actions/encodings/offset.js +30 -29
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +63 -11
- package/src/actions/encodings/position/index.js +52 -6
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +57 -13
- package/src/actions/encodings/position/resolve.js +88 -26
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +44 -34
- package/src/actions/encodings/ruleAppearance.js +24 -32
- package/src/actions/encodings/seriesLayout.js +105 -0
- package/src/actions/encodings/shared.js +107 -37
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +41 -8
- package/src/actions/errorBands/edit.js +360 -25
- package/src/actions/errorBars/create.js +56 -17
- package/src/actions/errorBars/edit.js +372 -21
- package/src/actions/facets/actions.js +619 -41
- package/src/actions/facets/derive.js +139 -25
- package/src/actions/gradientPlots/components.js +43 -13
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +8 -8
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +28 -19
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +184 -28
- package/src/actions/guides/axes/lines.js +13 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +50 -25
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +15 -8
- package/src/actions/guides/axes/ticks.js +11 -6
- package/src/actions/guides/axes/titles.js +32 -11
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +17 -4
- package/src/actions/guides/guides.js +54 -31
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +309 -279
- package/src/actions/guides/legends/categorical/components.js +21 -17
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +123 -354
- package/src/actions/guides/legends/categorical/options.js +32 -16
- package/src/actions/guides/legends/categorical/recipes.js +114 -9
- package/src/actions/guides/legends/categorical/resolve.js +30 -15
- package/src/actions/guides/legends/categorical/symbols.js +87 -33
- package/src/actions/guides/legends/continuous/common.js +99 -26
- package/src/actions/guides/legends/continuous/gradient.js +120 -89
- package/src/actions/guides/legends/continuous/index.js +8 -2
- package/src/actions/guides/legends/continuous/interval.js +79 -95
- package/src/actions/guides/legends/continuous/opacity.js +113 -89
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +40 -0
- package/src/actions/guides/legends/edit.js +321 -90
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +149 -102
- package/src/actions/guides/legends/lifecycle.js +143 -0
- package/src/actions/guides/legends/remove.js +43 -78
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +213 -116
- package/src/actions/guides/legends/strokeWidth.js +121 -73
- package/src/actions/guides/legends/target.js +123 -0
- package/src/actions/guides/legends/transition.js +192 -0
- package/src/actions/guides/polar/axes/facade.js +81 -66
- package/src/actions/guides/polar/axes/labels.js +28 -8
- package/src/actions/guides/polar/axes/lines.js +12 -5
- package/src/actions/guides/polar/axes/shared.js +22 -0
- package/src/actions/guides/polar/axes/ticks.js +6 -5
- package/src/actions/guides/polar/axes/titles.js +6 -5
- package/src/actions/guides/polar/grids.js +8 -8
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +15 -2
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +78 -20
- package/src/actions/marks/area/actions.js +51 -12
- package/src/actions/marks/area/materialize.js +22 -3
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +33 -9
- package/src/actions/marks/index.js +14 -0
- package/src/actions/marks/line/actions.js +71 -113
- package/src/actions/marks/line/materialize.js +57 -56
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +29 -21
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +154 -16
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +658 -0
- package/src/actions/marks/remove.js +87 -4
- package/src/actions/marks/rule/actions.js +77 -9
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +13 -13
- package/src/actions/marks/text/actions.js +296 -13
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +156 -8
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +3 -2
- package/src/actions/primitives/semanticAction.js +120 -10
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +23 -1
- package/src/actions/primitives/semanticValidation/scale.js +38 -8
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +11 -3
- package/src/actions/regression/edit.js +40 -13
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +21 -54
- package/src/actions/scales/consumers/families.js +28 -14
- package/src/actions/scales/consumers/index.js +42 -4
- package/src/actions/scales/consumers/seriesLayout.js +57 -12
- package/src/actions/scales/create.js +58 -26
- package/src/actions/scales/definitions.js +109 -39
- package/src/actions/scales/edit.js +101 -34
- package/src/actions/scales/editPolicy.js +95 -35
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +14 -49
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +140 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +98 -0
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +16 -0
- package/src/actions/theme/reconcile.js +1130 -0
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +90 -43
- package/src/actions/violinPlots/edit.js +192 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +32 -2
- package/src/core/compositionState.js +141 -8
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +37 -13
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +226 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +351 -0
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +123 -52
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/impute.js +340 -0
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +110 -0
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +109 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +8 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +63 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +8 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +57 -0
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +42 -5
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- package/src/grammar/schemas/semanticPath.js +23 -4
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +245 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +234 -13
- package/src/grammar/transformTopology.js +10 -0
- package/src/grammar/transforms.js +418 -16
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +232 -21
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +100 -12
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/labels.js +343 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +56 -147
- package/src/materialization/bars/histogram.js +104 -20
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/composition.js +3 -1
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +93 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +23 -66
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +28 -2
- package/src/materialization/legends.js +11 -2
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +68 -27
- package/src/materialization/marks/policies.js +4 -1
- package/src/materialization/rect.js +55 -42
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +6 -3
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +5 -1
- package/src/materialization/scales/resolve.js +65 -17
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +42 -71
- package/src/materialization/selection/items/common.js +27 -6
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +35 -8
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +324 -14
- package/src/mcp/adapter.js +2 -2
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/svg.js +19 -0
- package/src/selectors/datasets.js +44 -0
- package/src/selectors/layers.js +7 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +140 -0
- package/types/basic.d.ts +17 -2
- package/types/index.d.ts +211 -0
- package/types/program.d.ts +2182 -204
- package/src/materialization/bars/grouped.js +0 -144
package/src/grammar/window.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import {
|
|
3
|
+
maximumMagnitude,
|
|
4
|
+
requireFiniteResult,
|
|
3
5
|
stableFiniteMean,
|
|
4
6
|
stableFinitePrefixSums,
|
|
5
7
|
stableFiniteSum
|
|
6
8
|
} from "./numeric.js";
|
|
9
|
+
import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
|
|
7
10
|
|
|
8
11
|
const TRANSFORM_KEYS = [
|
|
9
|
-
"type", "partitionBy", "sortBy", "operations"
|
|
12
|
+
"type", "partitionBy", "sortBy", "operations", "temporalUnit"
|
|
10
13
|
];
|
|
11
14
|
const SORT_KEYS = ["field", "order"];
|
|
12
15
|
const ORDER_VALUES = ["ascending", "descending"];
|
|
@@ -17,7 +20,15 @@ const OPERATION_VALUES = [
|
|
|
17
20
|
const POSITION_OPERATIONS = new Set(OPERATION_VALUES.slice(0, 3));
|
|
18
21
|
const OFFSET_OPERATIONS = new Set(OPERATION_VALUES.slice(4, 6));
|
|
19
22
|
const MOVING_OPERATIONS = new Set(OPERATION_VALUES.slice(-2));
|
|
20
|
-
const FRAME_KEYS = ["preceding", "following"];
|
|
23
|
+
const FRAME_KEYS = ["preceding", "following", "duration"];
|
|
24
|
+
const DURATION_KEYS = ["preceding", "following", "unit"];
|
|
25
|
+
const DURATION_UNITS = Object.freeze({
|
|
26
|
+
millisecond: 1,
|
|
27
|
+
second: 1_000,
|
|
28
|
+
minute: 60_000,
|
|
29
|
+
hour: 3_600_000,
|
|
30
|
+
day: 86_400_000
|
|
31
|
+
});
|
|
21
32
|
|
|
22
33
|
function requireField(value, label) {
|
|
23
34
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -72,7 +83,7 @@ function operationKeys(operation) {
|
|
|
72
83
|
return ["op", "field", "as", "offset", "default"];
|
|
73
84
|
}
|
|
74
85
|
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
75
|
-
return ["op", "field", "as", "frame"];
|
|
86
|
+
return ["op", "field", "as", "frame", "minPeriods", "missing"];
|
|
76
87
|
}
|
|
77
88
|
return ["op"];
|
|
78
89
|
}
|
|
@@ -82,7 +93,29 @@ function validateFrame(frame, operation) {
|
|
|
82
93
|
throw new TypeError(`Window ${operation} frame must be a plain object.`);
|
|
83
94
|
}
|
|
84
95
|
rejectUnknownKeys(frame, FRAME_KEYS, `window ${operation} frame`);
|
|
85
|
-
|
|
96
|
+
const duration = Object.hasOwn(frame, "duration");
|
|
97
|
+
const rows = ["preceding", "following"].some(field => Object.hasOwn(frame, field));
|
|
98
|
+
if (duration === rows) {
|
|
99
|
+
throw new Error(`Window ${operation} frame requires exactly one row or duration mode.`);
|
|
100
|
+
}
|
|
101
|
+
if (duration) {
|
|
102
|
+
if (!isPlainObject(frame.duration)) {
|
|
103
|
+
throw new TypeError(`Window ${operation} duration must be a plain object.`);
|
|
104
|
+
}
|
|
105
|
+
rejectUnknownKeys(frame.duration, DURATION_KEYS, `window ${operation} duration`);
|
|
106
|
+
for (const field of ["preceding", "following"]) {
|
|
107
|
+
if (!Number.isFinite(frame.duration[field]) || frame.duration[field] < 0) {
|
|
108
|
+
throw new RangeError(
|
|
109
|
+
`Window ${operation} duration ${field} must be a non-negative finite number.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!Object.hasOwn(DURATION_UNITS, frame.duration.unit)) {
|
|
114
|
+
throw new Error(`Unsupported window duration unit "${frame.duration.unit}".`);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const field of ["preceding", "following"]) {
|
|
86
119
|
if (!Number.isInteger(frame[field]) || frame[field] < 0) {
|
|
87
120
|
throw new RangeError(
|
|
88
121
|
`Window ${operation} frame ${field} must be a non-negative integer.`
|
|
@@ -125,6 +158,12 @@ function validateOperations(operations, sortBy) {
|
|
|
125
158
|
}
|
|
126
159
|
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
127
160
|
validateFrame(operation.frame, operation.op);
|
|
161
|
+
if (!Number.isSafeInteger(operation.minPeriods) || operation.minPeriods <= 0) {
|
|
162
|
+
throw new RangeError(`Window ${operation.op} minPeriods must be a positive safe integer.`);
|
|
163
|
+
}
|
|
164
|
+
if (!["error", "skip"].includes(operation.missing)) {
|
|
165
|
+
throw new Error(`Unsupported window ${operation.op} missing policy "${operation.missing}".`);
|
|
166
|
+
}
|
|
128
167
|
}
|
|
129
168
|
});
|
|
130
169
|
}
|
|
@@ -140,6 +179,18 @@ export function validateWindowTransform(transform) {
|
|
|
140
179
|
validateFieldList(transform.partitionBy, "Window partitionBy");
|
|
141
180
|
validateSortBy(transform.sortBy);
|
|
142
181
|
validateOperations(transform.operations, transform.sortBy);
|
|
182
|
+
const durationOperations = transform.operations.filter(operation =>
|
|
183
|
+
MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
|
|
184
|
+
);
|
|
185
|
+
const hasTemporalUnit = Object.hasOwn(transform, "temporalUnit");
|
|
186
|
+
if (durationOperations.length > 0) {
|
|
187
|
+
validateTemporalUnit(transform.temporalUnit);
|
|
188
|
+
if (transform.sortBy.length !== 1 || transform.sortBy[0].order !== "ascending") {
|
|
189
|
+
throw new Error("Duration windows require exactly one ascending sortBy field.");
|
|
190
|
+
}
|
|
191
|
+
} else if (hasTemporalUnit) {
|
|
192
|
+
throw new Error("Window temporalUnit requires at least one duration operation.");
|
|
193
|
+
}
|
|
143
194
|
return transform;
|
|
144
195
|
}
|
|
145
196
|
|
|
@@ -152,7 +203,7 @@ function normalizeSortBy(value) {
|
|
|
152
203
|
if (value === undefined) return [];
|
|
153
204
|
if (!Array.isArray(value)) return value;
|
|
154
205
|
return value.map(sort => isPlainObject(sort)
|
|
155
|
-
? {
|
|
206
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
156
207
|
: sort
|
|
157
208
|
);
|
|
158
209
|
}
|
|
@@ -171,12 +222,20 @@ function normalizeOperations(value) {
|
|
|
171
222
|
};
|
|
172
223
|
}
|
|
173
224
|
if (MOVING_OPERATIONS.has(operation.op) && isPlainObject(operation.frame)) {
|
|
225
|
+
const duration = isPlainObject(operation.frame.duration)
|
|
226
|
+
? {
|
|
227
|
+
...operation.frame.duration,
|
|
228
|
+
following: operation.frame.duration.following ?? 0
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
174
231
|
return {
|
|
175
232
|
...operation,
|
|
176
|
-
frame: {
|
|
233
|
+
frame: duration === undefined ? {
|
|
177
234
|
...operation.frame,
|
|
178
235
|
following: operation.frame.following ?? 0
|
|
179
|
-
}
|
|
236
|
+
} : { ...operation.frame, duration },
|
|
237
|
+
minPeriods: operation.minPeriods ?? 1,
|
|
238
|
+
missing: operation.missing ?? "error"
|
|
180
239
|
};
|
|
181
240
|
}
|
|
182
241
|
return operation;
|
|
@@ -186,13 +245,15 @@ function normalizeOperations(value) {
|
|
|
186
245
|
export function normalizeWindowTransform({
|
|
187
246
|
partitionBy,
|
|
188
247
|
sortBy,
|
|
189
|
-
operations
|
|
248
|
+
operations,
|
|
249
|
+
temporalUnit
|
|
190
250
|
} = {}) {
|
|
191
251
|
const transform = {
|
|
192
252
|
type: "window",
|
|
193
253
|
partitionBy: normalizePartitionBy(partitionBy),
|
|
194
254
|
sortBy: normalizeSortBy(sortBy),
|
|
195
|
-
operations: normalizeOperations(operations)
|
|
255
|
+
operations: normalizeOperations(operations),
|
|
256
|
+
...(temporalUnit === undefined ? {} : { temporalUnit })
|
|
196
257
|
};
|
|
197
258
|
validateWindowTransform(transform);
|
|
198
259
|
return cloneAndFreeze(transform);
|
|
@@ -249,8 +310,14 @@ function validateSourceFields(rows, transform) {
|
|
|
249
310
|
if (!sourceFields.has(field)) {
|
|
250
311
|
throw new Error(`Window source does not contain field "${field}".`);
|
|
251
312
|
}
|
|
313
|
+
rows.forEach((row, index) => {
|
|
314
|
+
if (!Object.hasOwn(row, field)) {
|
|
315
|
+
throw new Error(`Window source does not contain field "${field}" at row ${index}.`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
252
318
|
}
|
|
253
319
|
const available = new Set(sourceFields);
|
|
320
|
+
const generated = new Set();
|
|
254
321
|
for (const operation of transform.operations) {
|
|
255
322
|
if (
|
|
256
323
|
!POSITION_OPERATIONS.has(operation.op) &&
|
|
@@ -258,10 +325,20 @@ function validateSourceFields(rows, transform) {
|
|
|
258
325
|
) {
|
|
259
326
|
throw new Error(`Window source does not contain field "${operation.field}".`);
|
|
260
327
|
}
|
|
328
|
+
if (!POSITION_OPERATIONS.has(operation.op) && !generated.has(operation.field)) {
|
|
329
|
+
rows.forEach((row, index) => {
|
|
330
|
+
if (!Object.hasOwn(row, operation.field)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Window source does not contain field "${operation.field}" at row ${index}.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
261
337
|
if (available.has(operation.as)) {
|
|
262
338
|
throw new Error(`Window output field "${operation.as}" already exists.`);
|
|
263
339
|
}
|
|
264
340
|
available.add(operation.as);
|
|
341
|
+
generated.add(operation.as);
|
|
265
342
|
}
|
|
266
343
|
}
|
|
267
344
|
|
|
@@ -316,6 +393,9 @@ function operationValues(
|
|
|
316
393
|
const values = [];
|
|
317
394
|
for (let index = first; index <= last; index += 1) {
|
|
318
395
|
const value = partition[index].row[operation.field];
|
|
396
|
+
if (value === null || value === undefined) {
|
|
397
|
+
if (operation.missing === "skip") continue;
|
|
398
|
+
}
|
|
319
399
|
if (!Number.isFinite(value)) {
|
|
320
400
|
throw new TypeError(
|
|
321
401
|
`Window ${operation.op} field "${operation.field}" must contain finite numbers.`
|
|
@@ -326,6 +406,18 @@ function operationValues(
|
|
|
326
406
|
return values;
|
|
327
407
|
}
|
|
328
408
|
|
|
409
|
+
function validateOperationValues(partition, operation) {
|
|
410
|
+
for (const entry of partition) {
|
|
411
|
+
const value = entry.row[operation.field];
|
|
412
|
+
if ((value === null || value === undefined) && operation.missing === "skip") continue;
|
|
413
|
+
if (!Number.isFinite(value)) {
|
|
414
|
+
throw new TypeError(
|
|
415
|
+
`Window ${operation.op} field "${operation.field}" must contain finite numbers.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
329
421
|
function stableWindowOutput(operation, calculate) {
|
|
330
422
|
const label = `Window ${operation.op} output "${operation.as}"`;
|
|
331
423
|
try {
|
|
@@ -336,10 +428,107 @@ function stableWindowOutput(operation, calculate) {
|
|
|
336
428
|
}
|
|
337
429
|
}
|
|
338
430
|
|
|
339
|
-
function
|
|
431
|
+
function setWindowOutput(entry, field, value) {
|
|
432
|
+
Object.defineProperty(entry.row, field, {
|
|
433
|
+
value, enumerable: true, writable: true, configurable: true
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function durationMilliseconds(duration, field) {
|
|
438
|
+
const value = duration[field] * DURATION_UNITS[duration.unit];
|
|
439
|
+
if (!Number.isFinite(value)) {
|
|
440
|
+
throw new RangeError(`Window duration ${field} is outside the finite numeric range.`);
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function durationPositions(partition, transform) {
|
|
446
|
+
const field = transform.sortBy[0].field;
|
|
447
|
+
return partition.map((entry, index) => entry.durationTimestamp ??
|
|
448
|
+
normalizeTemporalValue(entry.row[field], field, index, transform.temporalUnit));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validDateBoundary(value) {
|
|
452
|
+
return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function applyDurationOperation(partition, operation, transform) {
|
|
456
|
+
validateOperationValues(partition, operation);
|
|
457
|
+
const positions = durationPositions(partition, transform);
|
|
458
|
+
const preceding = durationMilliseconds(operation.frame.duration, "preceding");
|
|
459
|
+
const following = durationMilliseconds(operation.frame.duration, "following");
|
|
460
|
+
const rawValues = partition.map(entry => entry.row[operation.field]);
|
|
461
|
+
const finiteValues = rawValues.filter(Number.isFinite);
|
|
462
|
+
const scale = maximumMagnitude(finiteValues) || 1;
|
|
463
|
+
let left = 0;
|
|
464
|
+
let right = 0;
|
|
465
|
+
let total = 0;
|
|
466
|
+
let correction = 0;
|
|
467
|
+
let count = 0;
|
|
468
|
+
let ordinary = 0;
|
|
469
|
+
let ordinaryReliable = true;
|
|
470
|
+
const add = value => {
|
|
471
|
+
if (!Number.isFinite(value)) return;
|
|
472
|
+
if (ordinaryReliable) {
|
|
473
|
+
ordinary += value;
|
|
474
|
+
if (!Number.isFinite(ordinary)) ordinaryReliable = false;
|
|
475
|
+
}
|
|
476
|
+
const scaled = value / scale;
|
|
477
|
+
const next = total + scaled;
|
|
478
|
+
correction += Math.abs(total) >= Math.abs(scaled)
|
|
479
|
+
? total - next + scaled
|
|
480
|
+
: scaled - next + total;
|
|
481
|
+
total = next;
|
|
482
|
+
count += 1;
|
|
483
|
+
};
|
|
484
|
+
const remove = value => {
|
|
485
|
+
if (!Number.isFinite(value)) return;
|
|
486
|
+
if (ordinaryReliable) ordinary -= value;
|
|
487
|
+
const scaled = -value / scale;
|
|
488
|
+
const next = total + scaled;
|
|
489
|
+
correction += Math.abs(total) >= Math.abs(scaled)
|
|
490
|
+
? total - next + scaled
|
|
491
|
+
: scaled - next + total;
|
|
492
|
+
total = next;
|
|
493
|
+
count -= 1;
|
|
494
|
+
};
|
|
495
|
+
let index = 0;
|
|
496
|
+
while (index < partition.length) {
|
|
497
|
+
const timestamp = positions[index];
|
|
498
|
+
const lower = timestamp - preceding;
|
|
499
|
+
const upper = timestamp + following;
|
|
500
|
+
if (!validDateBoundary(lower) || !validDateBoundary(upper)) {
|
|
501
|
+
throw new RangeError("Window duration boundary is outside the supported Date range.");
|
|
502
|
+
}
|
|
503
|
+
while (right < partition.length && positions[right] <= upper) {
|
|
504
|
+
add(rawValues[right]);
|
|
505
|
+
right += 1;
|
|
506
|
+
}
|
|
507
|
+
while (left < right && positions[left] < lower) {
|
|
508
|
+
remove(rawValues[left]);
|
|
509
|
+
left += 1;
|
|
510
|
+
}
|
|
511
|
+
let end = index + 1;
|
|
512
|
+
while (end < partition.length && positions[end] === timestamp) end += 1;
|
|
513
|
+
let value = null;
|
|
514
|
+
if (count >= operation.minPeriods) {
|
|
515
|
+
const sum = requireFiniteResult(ordinaryReliable ? ordinary : (total + correction) * scale,
|
|
516
|
+
`Window ${operation.op} output "${operation.as}"`);
|
|
517
|
+
value = operation.op === "movingMean"
|
|
518
|
+
? requireFiniteResult(sum / count, `Window ${operation.op} output "${operation.as}"`)
|
|
519
|
+
: sum;
|
|
520
|
+
}
|
|
521
|
+
for (let peer = index; peer < end; peer += 1) {
|
|
522
|
+
setWindowOutput(partition[peer], operation.as, value);
|
|
523
|
+
}
|
|
524
|
+
index = end;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function applyOperation(partition, operation, sortBy, transform) {
|
|
340
529
|
if (operation.op === "rowNumber") {
|
|
341
530
|
partition.forEach((entry, index) => {
|
|
342
|
-
entry
|
|
531
|
+
setWindowOutput(entry, operation.as, index + 1);
|
|
343
532
|
});
|
|
344
533
|
return;
|
|
345
534
|
}
|
|
@@ -349,12 +538,14 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
349
538
|
partition.forEach((entry, index) => {
|
|
350
539
|
if (
|
|
351
540
|
index > 0 &&
|
|
352
|
-
|
|
541
|
+
(Object.hasOwn(entry, "durationTimestamp")
|
|
542
|
+
? entry.durationTimestamp !== partition[index - 1].durationTimestamp
|
|
543
|
+
: compareEntries(entry, partition[index - 1], sortBy, false) !== 0)
|
|
353
544
|
) {
|
|
354
545
|
rank = index + 1;
|
|
355
546
|
denseRank += 1;
|
|
356
547
|
}
|
|
357
|
-
entry
|
|
548
|
+
setWindowOutput(entry, operation.as, operation.op === "rank" ? rank : denseRank);
|
|
358
549
|
});
|
|
359
550
|
return;
|
|
360
551
|
}
|
|
@@ -364,11 +555,16 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
364
555
|
stableFinitePrefixSums(values, label)
|
|
365
556
|
);
|
|
366
557
|
partition.forEach((entry, index) => {
|
|
367
|
-
entry
|
|
558
|
+
setWindowOutput(entry, operation.as, totals[index]);
|
|
368
559
|
});
|
|
369
560
|
return;
|
|
370
561
|
}
|
|
371
562
|
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
563
|
+
if (Object.hasOwn(operation.frame, "duration")) {
|
|
564
|
+
applyDurationOperation(partition, operation, transform);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
validateOperationValues(partition, operation);
|
|
372
568
|
const calculate = operation.op === "movingMean"
|
|
373
569
|
? stableFiniteMean
|
|
374
570
|
: stableFiniteSum;
|
|
@@ -379,18 +575,18 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
379
575
|
index + operation.frame.following
|
|
380
576
|
);
|
|
381
577
|
const values = operationValues(partition, operation, first, last);
|
|
382
|
-
entry
|
|
383
|
-
|
|
384
|
-
|
|
578
|
+
setWindowOutput(entry, operation.as, values.length < operation.minPeriods
|
|
579
|
+
? null
|
|
580
|
+
: stableWindowOutput(operation, label => calculate(values, label)));
|
|
385
581
|
});
|
|
386
582
|
return;
|
|
387
583
|
}
|
|
388
584
|
const direction = operation.op === "lag" ? -1 : 1;
|
|
389
585
|
partition.forEach((entry, index) => {
|
|
390
586
|
const peer = partition[index + direction * operation.offset];
|
|
391
|
-
entry
|
|
587
|
+
setWindowOutput(entry, operation.as, peer === undefined
|
|
392
588
|
? operation.default
|
|
393
|
-
: peer.row[operation.field];
|
|
589
|
+
: peer.row[operation.field]);
|
|
394
590
|
});
|
|
395
591
|
}
|
|
396
592
|
|
|
@@ -405,13 +601,28 @@ export function deriveWindowRows(rows, transform) {
|
|
|
405
601
|
validateSourceFields(rows, transform);
|
|
406
602
|
const entries = rows.map((row, index) => ({ index, row: { ...row } }));
|
|
407
603
|
const partitions = partitionRows(entries, transform.partitionBy);
|
|
604
|
+
const hasDuration = transform.operations.some(operation =>
|
|
605
|
+
MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
|
|
606
|
+
);
|
|
408
607
|
for (const partition of partitions) {
|
|
409
608
|
validatePartitionSortValues(partition, transform);
|
|
410
|
-
|
|
609
|
+
if (hasDuration) {
|
|
610
|
+
const field = transform.sortBy[0].field;
|
|
611
|
+
partition.forEach(entry => {
|
|
612
|
+
entry.durationTimestamp = normalizeTemporalValue(
|
|
613
|
+
entry.row[field], field, entry.index, transform.temporalUnit
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
partition.sort((left, right) =>
|
|
617
|
+
left.durationTimestamp - right.durationTimestamp || left.index - right.index
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
|
|
621
|
+
}
|
|
411
622
|
}
|
|
412
623
|
for (const operation of transform.operations) {
|
|
413
624
|
for (const partition of partitions) {
|
|
414
|
-
applyOperation(partition, operation, transform.sortBy);
|
|
625
|
+
applyOperation(partition, operation, transform.sortBy, transform);
|
|
415
626
|
}
|
|
416
627
|
}
|
|
417
628
|
return cloneAndFreeze(entries.map(entry => entry.row));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const ALIGN_FRACTIONS = Object.freeze({ start: 0, center: 0.5, end: 1 });
|
|
4
|
+
const ASPECT_MODES = new Set(["frame", "data"]);
|
|
5
|
+
|
|
6
|
+
function validateAspect(value) {
|
|
7
|
+
if (!isPlainObject(value) || !ASPECT_MODES.has(value.mode)) {
|
|
8
|
+
throw new TypeError("Coordinate aspect resolver requires frame or data mode.");
|
|
9
|
+
}
|
|
10
|
+
if (!Number.isFinite(value.ratio) || value.ratio <= 0) {
|
|
11
|
+
throw new RangeError("Coordinate aspect ratio must be a positive finite number.");
|
|
12
|
+
}
|
|
13
|
+
const alignX = value.alignX ?? "center";
|
|
14
|
+
const alignY = value.alignY ?? "center";
|
|
15
|
+
if (!(alignX in ALIGN_FRACTIONS) || !(alignY in ALIGN_FRACTIONS)) {
|
|
16
|
+
throw new Error("Coordinate aspect alignment must be start, center, or end.");
|
|
17
|
+
}
|
|
18
|
+
return { mode: value.mode, ratio: value.ratio, alignX, alignY };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateBounds(bounds) {
|
|
22
|
+
if (
|
|
23
|
+
!isPlainObject(bounds) ||
|
|
24
|
+
![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
|
|
25
|
+
bounds.width < 0 ||
|
|
26
|
+
bounds.height < 0
|
|
27
|
+
) {
|
|
28
|
+
throw new TypeError(
|
|
29
|
+
"Coordinate aspect requires finite, non-negative allocated bounds."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return bounds;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function domainSpan(domain, channel) {
|
|
36
|
+
if (
|
|
37
|
+
!Array.isArray(domain) ||
|
|
38
|
+
domain.length !== 2 ||
|
|
39
|
+
!domain.every(Number.isFinite)
|
|
40
|
+
) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
`Coordinate data aspect requires a finite two-value ${channel} domain.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const span = Math.abs(domain[1] - domain[0]);
|
|
46
|
+
if (!Number.isFinite(span) || span <= 0) {
|
|
47
|
+
throw new RangeError(
|
|
48
|
+
`Coordinate data aspect requires a positive ${channel} domain span.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return span;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveEffectiveBounds(allocatedBounds, requestedAspect, domains) {
|
|
55
|
+
const allocated = validateBounds(allocatedBounds);
|
|
56
|
+
if (requestedAspect === undefined || requestedAspect === "auto") {
|
|
57
|
+
return cloneAndFreeze(allocated);
|
|
58
|
+
}
|
|
59
|
+
const aspect = validateAspect(requestedAspect);
|
|
60
|
+
const ratio = aspect.mode === "frame"
|
|
61
|
+
? aspect.ratio
|
|
62
|
+
: aspect.ratio * domainSpan(domains?.x, "x") /
|
|
63
|
+
domainSpan(domains?.y, "y");
|
|
64
|
+
if (!Number.isFinite(ratio) || ratio <= 0) {
|
|
65
|
+
throw new RangeError(
|
|
66
|
+
"Coordinate aspect must resolve to a positive finite frame ratio."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const width = Math.min(allocated.width, allocated.height * ratio);
|
|
70
|
+
const height = width / ratio;
|
|
71
|
+
if (![width, height].every(Number.isFinite)) {
|
|
72
|
+
throw new RangeError("Coordinate aspect bounds exceed the finite numeric range.");
|
|
73
|
+
}
|
|
74
|
+
return cloneAndFreeze({
|
|
75
|
+
x: allocated.x + (allocated.width - width) * ALIGN_FRACTIONS[aspect.alignX],
|
|
76
|
+
y: allocated.y + (allocated.height - height) * ALIGN_FRACTIONS[aspect.alignY],
|
|
77
|
+
width,
|
|
78
|
+
height
|
|
79
|
+
});
|
|
80
|
+
}
|
package/src/layout/facets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneAndFreeze } from "../core/immutable.js";
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_COMPOSITION_LAYOUT,
|
|
4
4
|
normalizeCompositionAlign,
|
|
@@ -23,12 +23,78 @@ function resolveColumns(columns, count) {
|
|
|
23
23
|
return Math.min(value, count);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function resolveGridCoordinates(children, columns) {
|
|
27
|
+
const requested = children.some(child => child.row !== undefined || child.column !== undefined);
|
|
28
|
+
if (!requested) return undefined;
|
|
29
|
+
if (children.some(child => !(
|
|
30
|
+
Number.isInteger(child.row) && child.row >= 0 &&
|
|
31
|
+
Number.isInteger(child.column) && child.column >= 0
|
|
32
|
+
))) {
|
|
33
|
+
throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");
|
|
34
|
+
}
|
|
35
|
+
const keys = children.map(child => `${child.row}:${child.column}`);
|
|
36
|
+
if (new Set(keys).size !== keys.length) {
|
|
37
|
+
throw new Error("Facet grid children must occupy unique row and column coordinates.");
|
|
38
|
+
}
|
|
39
|
+
const requiredColumns = Math.max(...children.map(child => child.column)) + 1;
|
|
40
|
+
if (columns !== undefined && columns !== requiredColumns) {
|
|
41
|
+
throw new RangeError("Facet grid columns must match the declared column domain.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
columns: requiredColumns,
|
|
45
|
+
rows: Math.max(...children.map(child => child.row)) + 1
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
function alignedOffset(remaining, align) {
|
|
27
50
|
if (align === "start") return 0;
|
|
28
51
|
if (align === "end") return remaining;
|
|
29
52
|
return remaining / 2;
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
56
|
+
|
|
57
|
+
function normalizeHeaderLayout(value, rows) {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
return {
|
|
60
|
+
outer: ZERO_SIDES,
|
|
61
|
+
cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(value) || Object.keys(value).some(
|
|
65
|
+
key => !["outer", "cellRows"].includes(key)
|
|
66
|
+
)) {
|
|
67
|
+
throw new TypeError("Facet headerLayout must contain outer and cellRows.");
|
|
68
|
+
}
|
|
69
|
+
const outer = { ...ZERO_SIDES, ...(value.outer ?? {}) };
|
|
70
|
+
if (!isPlainObject(value.outer ?? {}) || Object.keys(outer).some(
|
|
71
|
+
key => !Object.hasOwn(ZERO_SIDES, key)
|
|
72
|
+
)) {
|
|
73
|
+
throw new TypeError("Facet headerLayout.outer must contain side reservations.");
|
|
74
|
+
}
|
|
75
|
+
for (const [side, amount] of Object.entries(outer)) {
|
|
76
|
+
validateCompositionSpacing(amount, `Facet headerLayout.outer.${side}`);
|
|
77
|
+
}
|
|
78
|
+
const sourceRows = value.cellRows ?? {};
|
|
79
|
+
if (!isPlainObject(sourceRows) || Object.keys(sourceRows).some(
|
|
80
|
+
key => !["top", "bottom"].includes(key)
|
|
81
|
+
)) {
|
|
82
|
+
throw new TypeError("Facet headerLayout.cellRows must contain top and bottom arrays.");
|
|
83
|
+
}
|
|
84
|
+
const cellRows = Object.fromEntries(["top", "bottom"].map(side => {
|
|
85
|
+
const amounts = sourceRows[side] ?? Array(rows).fill(0);
|
|
86
|
+
if (!Array.isArray(amounts) || amounts.length !== rows) {
|
|
87
|
+
throw new RangeError(`Facet headerLayout.cellRows.${side} must match row count.`);
|
|
88
|
+
}
|
|
89
|
+
amounts.forEach((amount, index) => validateCompositionSpacing(
|
|
90
|
+
amount,
|
|
91
|
+
`Facet headerLayout.cellRows.${side}[${index}]`
|
|
92
|
+
));
|
|
93
|
+
return [side, [...amounts]];
|
|
94
|
+
}));
|
|
95
|
+
return { outer, cellRows };
|
|
96
|
+
}
|
|
97
|
+
|
|
32
98
|
export function resolveFacetLayout({
|
|
33
99
|
children,
|
|
34
100
|
columns,
|
|
@@ -40,13 +106,16 @@ export function resolveFacetLayout({
|
|
|
40
106
|
sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
|
|
41
107
|
sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
|
|
42
108
|
sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
|
|
43
|
-
sharedLegendPosition = "right"
|
|
109
|
+
sharedLegendPosition = "right",
|
|
110
|
+
headerLayout
|
|
44
111
|
} = {}) {
|
|
45
112
|
const values = children?.map(child => child?.value);
|
|
113
|
+
const coordinates = resolveGridCoordinates(children ?? [], columns);
|
|
46
114
|
const resolvedChildren = normalizeCompositionChildren(children?.map(
|
|
47
|
-
({ value: _value, ...child }) => child
|
|
115
|
+
({ value: _value, row: _row, column: _column, ...child }) => child
|
|
48
116
|
));
|
|
49
|
-
const resolvedColumns =
|
|
117
|
+
const resolvedColumns = coordinates?.columns ??
|
|
118
|
+
resolveColumns(columns, resolvedChildren.length);
|
|
50
119
|
const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
|
|
51
120
|
const resolvedAlign = normalizeCompositionAlign(align);
|
|
52
121
|
const resolvedPadding = normalizeCompositionPadding(padding);
|
|
@@ -79,26 +148,38 @@ export function resolveFacetLayout({
|
|
|
79
148
|
const legendLane = sharedLegend
|
|
80
149
|
? legendGap + (sideLegend ? legendWidth : legendHeight)
|
|
81
150
|
: 0;
|
|
82
|
-
const rowCount =
|
|
151
|
+
const rowCount = coordinates?.rows ??
|
|
152
|
+
Math.ceil(resolvedChildren.length / resolvedColumns);
|
|
153
|
+
const headers = normalizeHeaderLayout(headerLayout, rowCount);
|
|
83
154
|
const columnWidths = Array(resolvedColumns).fill(-Infinity);
|
|
84
155
|
const rowHeights = Array(rowCount).fill(-Infinity);
|
|
85
156
|
resolvedChildren.forEach((child, index) => {
|
|
86
|
-
const column =
|
|
87
|
-
|
|
157
|
+
const column = coordinates === undefined
|
|
158
|
+
? index % resolvedColumns
|
|
159
|
+
: children[index].column;
|
|
160
|
+
const row = coordinates === undefined
|
|
161
|
+
? Math.floor(index / resolvedColumns)
|
|
162
|
+
: children[index].row;
|
|
88
163
|
columnWidths[column] = Math.max(columnWidths[column], child.width);
|
|
89
164
|
rowHeights[row] = Math.max(rowHeights[row], child.height);
|
|
90
165
|
});
|
|
91
166
|
const columnStarts = columnWidths.map((_, column) =>
|
|
92
|
-
resolvedPadding.left + columnWidths.slice(0, column)
|
|
167
|
+
resolvedPadding.left + headers.outer.left + columnWidths.slice(0, column)
|
|
93
168
|
.reduce((sum, width) => sum + width, 0) + resolvedGap * column
|
|
94
169
|
);
|
|
95
170
|
const rowStarts = rowHeights.map((_, row) =>
|
|
96
|
-
resolvedTitleHeight + resolvedPadding.top +
|
|
97
|
-
|
|
171
|
+
resolvedTitleHeight + resolvedPadding.top + headers.outer.top +
|
|
172
|
+
rowHeights.slice(0, row).reduce((sum, height, index) =>
|
|
173
|
+
sum + headers.cellRows.top[index] + height + headers.cellRows.bottom[index], 0) +
|
|
174
|
+
resolvedGap * row + headers.cellRows.top[row]
|
|
98
175
|
);
|
|
99
176
|
const placements = resolvedChildren.map((child, index) => {
|
|
100
|
-
const column =
|
|
101
|
-
|
|
177
|
+
const column = coordinates === undefined
|
|
178
|
+
? index % resolvedColumns
|
|
179
|
+
: children[index].column;
|
|
180
|
+
const row = coordinates === undefined
|
|
181
|
+
? Math.floor(index / resolvedColumns)
|
|
182
|
+
: children[index].row;
|
|
102
183
|
return {
|
|
103
184
|
id: child.id,
|
|
104
185
|
value: values[index],
|
|
@@ -119,12 +200,18 @@ export function resolveFacetLayout({
|
|
|
119
200
|
};
|
|
120
201
|
});
|
|
121
202
|
const gridWidth = resolvedPadding.left +
|
|
203
|
+
headers.outer.left +
|
|
122
204
|
columnWidths.reduce((sum, width) => sum + width, 0) +
|
|
123
205
|
resolvedGap * Math.max(0, resolvedColumns - 1) +
|
|
206
|
+
headers.outer.right +
|
|
124
207
|
resolvedPadding.right;
|
|
125
208
|
const gridHeight = resolvedTitleHeight + resolvedPadding.top +
|
|
209
|
+
headers.outer.top +
|
|
126
210
|
rowHeights.reduce((sum, height) => sum + height, 0) +
|
|
211
|
+
headers.cellRows.top.reduce((sum, amount) => sum + amount, 0) +
|
|
212
|
+
headers.cellRows.bottom.reduce((sum, amount) => sum + amount, 0) +
|
|
127
213
|
resolvedGap * Math.max(0, rowCount - 1) +
|
|
214
|
+
headers.outer.bottom +
|
|
128
215
|
resolvedPadding.bottom;
|
|
129
216
|
return cloneAndFreeze({
|
|
130
217
|
columns: resolvedColumns,
|
|
@@ -133,6 +220,7 @@ export function resolveFacetLayout({
|
|
|
133
220
|
align: resolvedAlign,
|
|
134
221
|
padding: resolvedPadding,
|
|
135
222
|
titleHeight: resolvedTitleHeight,
|
|
223
|
+
headerLayout: headers,
|
|
136
224
|
gridWidth,
|
|
137
225
|
width: gridWidth + (sideLegend ? legendLane : 0),
|
|
138
226
|
height: gridHeight + (sideLegend ? 0 : legendLane),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function assertGuideCollisionBlocks(blocks) {
|
|
2
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
3
|
+
const a = blocks[index];
|
|
4
|
+
for (const b of blocks.slice(index + 1)) {
|
|
5
|
+
if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
|
|
6
|
+
if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
|
|
7
|
+
a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
|
|
8
|
+
throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|