ggaction 0.0.13 → 0.0.16
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 +146 -0
- package/README.md +41 -12
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +39 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +20 -12
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +34 -9
- package/src/actions/boxPlots/materialize.js +12 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +21 -12
- package/src/actions/charts/area.js +6 -2
- package/src/actions/charts/bar.js +3 -2
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +7 -3
- package/src/actions/charts/ecdf.js +6 -3
- package/src/actions/charts/endpoints.js +29 -7
- package/src/actions/charts/heatmap.js +3 -2
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +4 -2
- package/src/actions/charts/interval-regression.js +7 -4
- package/src/actions/charts/line.js +4 -2
- package/src/actions/charts/parallel.js +9 -3
- package/src/actions/charts/pie.js +11 -3
- package/src/actions/charts/polar.js +17 -6
- package/src/actions/charts/radar.js +9 -3
- package/src/actions/charts/radial.js +12 -4
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +8 -4
- package/src/actions/charts/scatter.js +4 -2
- package/src/actions/composition/actions.js +29 -24
- package/src/actions/coordinates/actions.js +2 -2
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +63 -193
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +4 -4
- package/src/actions/data/create.js +7 -14
- package/src/actions/data/density.js +15 -23
- package/src/actions/data/derived.js +57 -31
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +551 -0
- package/src/actions/data/filter.js +28 -22
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +62 -47
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +21 -27
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +125 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +6 -3
- package/src/actions/data/window.js +3 -3
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +3 -2
- package/src/actions/encodings/density.js +7 -4
- package/src/actions/encodings/histogram.js +4 -3
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +22 -15
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +14 -6
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +13 -9
- package/src/actions/encodings/ruleAppearance.js +2 -24
- package/src/actions/encodings/seriesLayout.js +2 -1
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +34 -20
- package/src/actions/errorBands/edit.js +34 -137
- package/src/actions/errorBars/create.js +43 -20
- package/src/actions/errorBars/edit.js +29 -125
- package/src/actions/facets/actions.js +267 -73
- package/src/actions/facets/derive.js +99 -28
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +15 -8
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +18 -9
- package/src/actions/gradientPlots/materialize.js +5 -3
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +67 -21
- package/src/actions/guides/axes/lines.js +15 -9
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +6 -5
- package/src/actions/guides/axes/ticks.js +19 -6
- package/src/actions/guides/axes/titles.js +21 -13
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +20 -4
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +138 -52
- package/src/actions/guides/legends/categorical/components.js +55 -45
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +98 -16
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +104 -52
- package/src/actions/guides/legends/continuous/common.js +76 -20
- package/src/actions/guides/legends/continuous/gradient.js +95 -158
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +21 -16
- package/src/actions/guides/legends/continuous/opacity.js +37 -83
- package/src/actions/guides/legends/continuous/stroke.js +225 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +159 -60
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +59 -24
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +17 -8
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +100 -108
- package/src/actions/guides/legends/strokeWidth.js +38 -92
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +8 -8
- package/src/actions/guides/polar/axes/labels.js +29 -10
- package/src/actions/guides/polar/axes/lines.js +19 -19
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +10 -16
- package/src/actions/guides/polar/axes/titles.js +6 -6
- package/src/actions/guides/polar/grids.js +12 -14
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +16 -3
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +80 -22
- package/src/actions/marks/area/actions.js +38 -13
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +12 -3
- package/src/actions/marks/bar/edit.js +21 -4
- package/src/actions/marks/bar/materialize.js +34 -40
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +40 -17
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +9 -3
- package/src/actions/marks/point/edit.js +19 -7
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +48 -22
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +36 -15
- package/src/actions/marks/references.js +602 -33
- package/src/actions/marks/remove.js +67 -41
- package/src/actions/marks/rule/actions.js +51 -14
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +79 -14
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +157 -17
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +72 -0
- package/src/actions/marks/tick/actions.js +25 -8
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +31 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +11 -3
- package/src/actions/primitives/semanticAction.js +90 -14
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +36 -25
- package/src/actions/regression/create.js +13 -6
- package/src/actions/regression/edit.js +24 -11
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +173 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +55 -23
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +67 -12
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +5 -4
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +179 -66
- package/src/actions/selection/index.js +3 -0
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +70 -15
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +313 -121
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +6 -3
- package/src/actions/violinPlots/edit.js +2 -1
- package/src/core/action.js +117 -35
- package/src/core/compositionState.js +6 -1
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +21 -1
- package/src/core/resourceReferences.js +341 -0
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/core/vocabulary.js +16 -3
- package/src/diagnostics.js +1 -0
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +341 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +190 -41
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +351 -7
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +9 -3
- package/src/materialization/facetGuides/preparation.js +13 -8
- package/src/materialization/facetHeaders.js +347 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +20 -5
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/index.js +5 -4
- 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/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +29 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +38 -5
- package/src/selectors/layers.js +10 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +103 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +10 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +90 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +944 -118
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 {
|
|
@@ -342,7 +434,98 @@ function setWindowOutput(entry, field, value) {
|
|
|
342
434
|
});
|
|
343
435
|
}
|
|
344
436
|
|
|
345
|
-
function
|
|
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) {
|
|
346
529
|
if (operation.op === "rowNumber") {
|
|
347
530
|
partition.forEach((entry, index) => {
|
|
348
531
|
setWindowOutput(entry, operation.as, index + 1);
|
|
@@ -355,7 +538,9 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
355
538
|
partition.forEach((entry, index) => {
|
|
356
539
|
if (
|
|
357
540
|
index > 0 &&
|
|
358
|
-
|
|
541
|
+
(Object.hasOwn(entry, "durationTimestamp")
|
|
542
|
+
? entry.durationTimestamp !== partition[index - 1].durationTimestamp
|
|
543
|
+
: compareEntries(entry, partition[index - 1], sortBy, false) !== 0)
|
|
359
544
|
) {
|
|
360
545
|
rank = index + 1;
|
|
361
546
|
denseRank += 1;
|
|
@@ -375,6 +560,11 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
375
560
|
return;
|
|
376
561
|
}
|
|
377
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);
|
|
378
568
|
const calculate = operation.op === "movingMean"
|
|
379
569
|
? stableFiniteMean
|
|
380
570
|
: stableFiniteSum;
|
|
@@ -385,9 +575,9 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
385
575
|
index + operation.frame.following
|
|
386
576
|
);
|
|
387
577
|
const values = operationValues(partition, operation, first, last);
|
|
388
|
-
setWindowOutput(entry, operation.as,
|
|
389
|
-
|
|
390
|
-
|
|
578
|
+
setWindowOutput(entry, operation.as, values.length < operation.minPeriods
|
|
579
|
+
? null
|
|
580
|
+
: stableWindowOutput(operation, label => calculate(values, label)));
|
|
391
581
|
});
|
|
392
582
|
return;
|
|
393
583
|
}
|
|
@@ -411,13 +601,28 @@ export function deriveWindowRows(rows, transform) {
|
|
|
411
601
|
validateSourceFields(rows, transform);
|
|
412
602
|
const entries = rows.map((row, index) => ({ index, row: { ...row } }));
|
|
413
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
|
+
);
|
|
414
607
|
for (const partition of partitions) {
|
|
415
608
|
validatePartitionSortValues(partition, transform);
|
|
416
|
-
|
|
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
|
+
}
|
|
417
622
|
}
|
|
418
623
|
for (const operation of transform.operations) {
|
|
419
624
|
for (const partition of partitions) {
|
|
420
|
-
applyOperation(partition, operation, transform.sortBy);
|
|
625
|
+
applyOperation(partition, operation, transform.sortBy, transform);
|
|
421
626
|
}
|
|
422
627
|
}
|
|
423
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,
|
|
@@ -52,6 +52,49 @@ function alignedOffset(remaining, align) {
|
|
|
52
52
|
return remaining / 2;
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
98
|
export function resolveFacetLayout({
|
|
56
99
|
children,
|
|
57
100
|
columns,
|
|
@@ -63,7 +106,8 @@ export function resolveFacetLayout({
|
|
|
63
106
|
sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
|
|
64
107
|
sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
|
|
65
108
|
sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
|
|
66
|
-
sharedLegendPosition = "right"
|
|
109
|
+
sharedLegendPosition = "right",
|
|
110
|
+
headerLayout
|
|
67
111
|
} = {}) {
|
|
68
112
|
const values = children?.map(child => child?.value);
|
|
69
113
|
const coordinates = resolveGridCoordinates(children ?? [], columns);
|
|
@@ -106,6 +150,7 @@ export function resolveFacetLayout({
|
|
|
106
150
|
: 0;
|
|
107
151
|
const rowCount = coordinates?.rows ??
|
|
108
152
|
Math.ceil(resolvedChildren.length / resolvedColumns);
|
|
153
|
+
const headers = normalizeHeaderLayout(headerLayout, rowCount);
|
|
109
154
|
const columnWidths = Array(resolvedColumns).fill(-Infinity);
|
|
110
155
|
const rowHeights = Array(rowCount).fill(-Infinity);
|
|
111
156
|
resolvedChildren.forEach((child, index) => {
|
|
@@ -119,12 +164,14 @@ export function resolveFacetLayout({
|
|
|
119
164
|
rowHeights[row] = Math.max(rowHeights[row], child.height);
|
|
120
165
|
});
|
|
121
166
|
const columnStarts = columnWidths.map((_, column) =>
|
|
122
|
-
resolvedPadding.left + columnWidths.slice(0, column)
|
|
167
|
+
resolvedPadding.left + headers.outer.left + columnWidths.slice(0, column)
|
|
123
168
|
.reduce((sum, width) => sum + width, 0) + resolvedGap * column
|
|
124
169
|
);
|
|
125
170
|
const rowStarts = rowHeights.map((_, row) =>
|
|
126
|
-
resolvedTitleHeight + resolvedPadding.top +
|
|
127
|
-
|
|
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]
|
|
128
175
|
);
|
|
129
176
|
const placements = resolvedChildren.map((child, index) => {
|
|
130
177
|
const column = coordinates === undefined
|
|
@@ -153,12 +200,18 @@ export function resolveFacetLayout({
|
|
|
153
200
|
};
|
|
154
201
|
});
|
|
155
202
|
const gridWidth = resolvedPadding.left +
|
|
203
|
+
headers.outer.left +
|
|
156
204
|
columnWidths.reduce((sum, width) => sum + width, 0) +
|
|
157
205
|
resolvedGap * Math.max(0, resolvedColumns - 1) +
|
|
206
|
+
headers.outer.right +
|
|
158
207
|
resolvedPadding.right;
|
|
159
208
|
const gridHeight = resolvedTitleHeight + resolvedPadding.top +
|
|
209
|
+
headers.outer.top +
|
|
160
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) +
|
|
161
213
|
resolvedGap * Math.max(0, rowCount - 1) +
|
|
214
|
+
headers.outer.bottom +
|
|
162
215
|
resolvedPadding.bottom;
|
|
163
216
|
return cloneAndFreeze({
|
|
164
217
|
columns: resolvedColumns,
|
|
@@ -167,6 +220,7 @@ export function resolveFacetLayout({
|
|
|
167
220
|
align: resolvedAlign,
|
|
168
221
|
padding: resolvedPadding,
|
|
169
222
|
titleHeight: resolvedTitleHeight,
|
|
223
|
+
headerLayout: headers,
|
|
170
224
|
gridWidth,
|
|
171
225
|
width: gridWidth + (sideLegend ? legendLane : 0),
|
|
172
226
|
height: gridHeight + (sideLegend ? 0 : legendLane),
|