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
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveTextBounds
|
|
4
4
|
} from "../../core/textMetrics.js";
|
|
5
5
|
import { interpolateNumber } from "../numeric.js";
|
|
6
|
+
import { resolveStrokeDetails } from "../strokeStyle.js";
|
|
6
7
|
import {
|
|
7
8
|
findGraphic,
|
|
8
9
|
findGraphicParent,
|
|
@@ -174,15 +175,101 @@ function endpointDirection(start, command, end, reverse = false) {
|
|
|
174
175
|
return undefined;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
function
|
|
178
|
+
function includeCircle(bounds, point, radius) {
|
|
179
|
+
return includeExtent(
|
|
180
|
+
bounds,
|
|
181
|
+
point.x - radius,
|
|
182
|
+
point.x + radius,
|
|
183
|
+
point.y - radius,
|
|
184
|
+
point.y + radius
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function includeEndpointCap(
|
|
189
|
+
bounds,
|
|
190
|
+
point,
|
|
191
|
+
direction,
|
|
192
|
+
strokeExtent,
|
|
193
|
+
lineCap,
|
|
194
|
+
atStart
|
|
195
|
+
) {
|
|
196
|
+
if (lineCap === "butt" || direction === undefined) return bounds;
|
|
197
|
+
if (lineCap === "round") {
|
|
198
|
+
return includeCircle(bounds, point, strokeExtent);
|
|
199
|
+
}
|
|
200
|
+
const sign = atStart ? -1 : 1;
|
|
201
|
+
const center = {
|
|
202
|
+
x: point.x + direction.x * strokeExtent * sign,
|
|
203
|
+
y: point.y + direction.y * strokeExtent * sign
|
|
204
|
+
};
|
|
205
|
+
const normal = { x: -direction.y, y: direction.x };
|
|
206
|
+
return includeExtent(
|
|
207
|
+
bounds,
|
|
208
|
+
center.x - Math.abs(normal.x) * strokeExtent,
|
|
209
|
+
center.x + Math.abs(normal.x) * strokeExtent,
|
|
210
|
+
center.y - Math.abs(normal.y) * strokeExtent,
|
|
211
|
+
center.y + Math.abs(normal.y) * strokeExtent
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function segmentStrokeBounds(start, end, strokeExtent, lineCap = "butt") {
|
|
216
|
+
const command = { op: "L", x: end.x, y: end.y };
|
|
217
|
+
const direction = endpointDirection(start, command, end);
|
|
218
|
+
let bounds = includeExtent(
|
|
219
|
+
undefined,
|
|
220
|
+
start.x,
|
|
221
|
+
start.x,
|
|
222
|
+
start.y,
|
|
223
|
+
start.y
|
|
224
|
+
);
|
|
225
|
+
bounds = includeExtent(bounds, end.x, end.x, end.y, end.y);
|
|
226
|
+
if (direction === undefined) {
|
|
227
|
+
return lineCap === "butt"
|
|
228
|
+
? bounds
|
|
229
|
+
: includeCircle(bounds, start, strokeExtent);
|
|
230
|
+
}
|
|
231
|
+
const normal = { x: -direction.y, y: direction.x };
|
|
232
|
+
for (const point of [start, end]) {
|
|
233
|
+
bounds = includeExtent(
|
|
234
|
+
bounds,
|
|
235
|
+
point.x - Math.abs(normal.x) * strokeExtent,
|
|
236
|
+
point.x + Math.abs(normal.x) * strokeExtent,
|
|
237
|
+
point.y - Math.abs(normal.y) * strokeExtent,
|
|
238
|
+
point.y + Math.abs(normal.y) * strokeExtent
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
bounds = includeEndpointCap(
|
|
242
|
+
bounds, start, direction, strokeExtent, lineCap, true
|
|
243
|
+
);
|
|
244
|
+
bounds = includeEndpointCap(
|
|
245
|
+
bounds, end, direction, strokeExtent, lineCap, false
|
|
246
|
+
);
|
|
247
|
+
return bounds;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function includeJoin(
|
|
251
|
+
bounds,
|
|
252
|
+
point,
|
|
253
|
+
incoming,
|
|
254
|
+
outgoing,
|
|
255
|
+
strokeExtent,
|
|
256
|
+
lineJoin,
|
|
257
|
+
miterLimit
|
|
258
|
+
) {
|
|
178
259
|
if (incoming === undefined || outgoing === undefined) return;
|
|
179
260
|
const cross = incoming.x * outgoing.y - incoming.y * outgoing.x;
|
|
180
261
|
if (Math.abs(cross) <= EPSILON) return;
|
|
262
|
+
if (lineJoin === "round") {
|
|
263
|
+
includeCircle(bounds, point, strokeExtent);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (lineJoin === "bevel") return;
|
|
181
267
|
const normalX = -incoming.y - outgoing.y;
|
|
182
268
|
const normalY = incoming.x + outgoing.x;
|
|
183
269
|
const normalLength = Math.hypot(normalX, normalY);
|
|
270
|
+
if (!(normalLength > EPSILON)) return;
|
|
184
271
|
const ratio = 2 / normalLength;
|
|
185
|
-
if (ratio >
|
|
272
|
+
if (ratio > miterLimit * (1 + EPSILON)) return;
|
|
186
273
|
const distance = strokeExtent * ratio * (cross > 0 ? -1 : 1) / normalLength;
|
|
187
274
|
includeExtent(
|
|
188
275
|
bounds,
|
|
@@ -193,16 +280,11 @@ function includeMiter(bounds, point, incoming, outgoing, strokeExtent) {
|
|
|
193
280
|
);
|
|
194
281
|
}
|
|
195
282
|
|
|
196
|
-
function pathStrokeBounds(commands, bounds, strokeExtent) {
|
|
197
|
-
|
|
198
|
-
bounds.left, bounds.right, bounds.top, bounds.bottom, strokeExtent
|
|
199
|
-
);
|
|
200
|
-
if (!(strokeExtent > 0)) return expanded;
|
|
283
|
+
function pathStrokeBounds(commands, bounds, strokeExtent, details) {
|
|
284
|
+
if (!(strokeExtent > 0)) return bounds;
|
|
201
285
|
const start = { x: commands[0].x, y: commands[0].y };
|
|
202
286
|
let current = start;
|
|
203
|
-
|
|
204
|
-
let previousDirection;
|
|
205
|
-
let segmentCount = 0;
|
|
287
|
+
const segments = [];
|
|
206
288
|
for (const command of commands.slice(1)) {
|
|
207
289
|
const end = command.op === "Z"
|
|
208
290
|
? start
|
|
@@ -210,31 +292,94 @@ function pathStrokeBounds(commands, bounds, strokeExtent) {
|
|
|
210
292
|
const outgoing = endpointDirection(current, command, end);
|
|
211
293
|
const incoming = endpointDirection(current, command, end, true);
|
|
212
294
|
if (outgoing !== undefined || incoming !== undefined) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
segments.push({
|
|
296
|
+
start: current,
|
|
297
|
+
end,
|
|
298
|
+
command,
|
|
299
|
+
outgoing,
|
|
300
|
+
incoming
|
|
301
|
+
});
|
|
220
302
|
}
|
|
221
303
|
current = end;
|
|
222
304
|
if (command.op === "Z") break;
|
|
223
305
|
}
|
|
224
|
-
|
|
225
|
-
|
|
306
|
+
let expanded = bounds;
|
|
307
|
+
for (const segment of segments) {
|
|
308
|
+
let segmentBounds;
|
|
309
|
+
if (segment.command.op === "C") {
|
|
310
|
+
const centerline = resolvePathCommandBounds([
|
|
311
|
+
{ op: "M", ...segment.start },
|
|
312
|
+
segment.command
|
|
313
|
+
]);
|
|
314
|
+
segmentBounds = box(
|
|
315
|
+
centerline.left,
|
|
316
|
+
centerline.right,
|
|
317
|
+
centerline.top,
|
|
318
|
+
centerline.bottom,
|
|
319
|
+
strokeExtent
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
segmentBounds = segmentStrokeBounds(
|
|
323
|
+
segment.start,
|
|
324
|
+
segment.end,
|
|
325
|
+
strokeExtent,
|
|
326
|
+
"butt"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
expanded = unionBounds([expanded, segmentBounds]);
|
|
330
|
+
}
|
|
331
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
332
|
+
includeJoin(
|
|
333
|
+
expanded,
|
|
334
|
+
segments[index].start,
|
|
335
|
+
segments[index - 1].incoming,
|
|
336
|
+
segments[index].outgoing,
|
|
337
|
+
strokeExtent,
|
|
338
|
+
details.lineJoin,
|
|
339
|
+
details.miterLimit
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const closed = commands.at(-1)?.op === "Z";
|
|
343
|
+
if (closed && segments.length > 1) {
|
|
344
|
+
includeJoin(
|
|
226
345
|
expanded,
|
|
227
346
|
start,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
strokeExtent
|
|
347
|
+
segments.at(-1).incoming,
|
|
348
|
+
segments[0].outgoing,
|
|
349
|
+
strokeExtent,
|
|
350
|
+
details.lineJoin,
|
|
351
|
+
details.miterLimit
|
|
352
|
+
);
|
|
353
|
+
} else if (!closed && segments.length > 0) {
|
|
354
|
+
expanded = includeEndpointCap(
|
|
355
|
+
expanded,
|
|
356
|
+
segments[0].start,
|
|
357
|
+
segments[0].outgoing,
|
|
358
|
+
strokeExtent,
|
|
359
|
+
details.lineCap,
|
|
360
|
+
true
|
|
361
|
+
);
|
|
362
|
+
expanded = includeEndpointCap(
|
|
363
|
+
expanded,
|
|
364
|
+
segments.at(-1).end,
|
|
365
|
+
segments.at(-1).incoming,
|
|
366
|
+
strokeExtent,
|
|
367
|
+
details.lineCap,
|
|
368
|
+
false
|
|
231
369
|
);
|
|
232
370
|
}
|
|
233
371
|
return expanded;
|
|
234
372
|
}
|
|
235
373
|
|
|
374
|
+
function hasActiveStroke(properties) {
|
|
375
|
+
return typeof properties.stroke === "string" &&
|
|
376
|
+
(properties.strokeWidth ?? 0) > 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
236
379
|
function primitiveBounds(type, properties = {}) {
|
|
237
|
-
const strokeExtent = (properties
|
|
380
|
+
const strokeExtent = hasActiveStroke(properties)
|
|
381
|
+
? properties.strokeWidth / 2
|
|
382
|
+
: 0;
|
|
238
383
|
if (type === "circle") {
|
|
239
384
|
if (![properties.x, properties.y, properties.radius].every(Number.isFinite)) {
|
|
240
385
|
return undefined;
|
|
@@ -261,12 +406,11 @@ function primitiveBounds(type, properties = {}) {
|
|
|
261
406
|
if (type === "line") {
|
|
262
407
|
if (![properties.x1, properties.y1, properties.x2, properties.y2]
|
|
263
408
|
.every(Number.isFinite)) return undefined;
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
strokeExtent
|
|
409
|
+
return segmentStrokeBounds(
|
|
410
|
+
{ x: properties.x1, y: properties.y1 },
|
|
411
|
+
{ x: properties.x2, y: properties.y2 },
|
|
412
|
+
strokeExtent,
|
|
413
|
+
resolveStrokeDetails(properties).lineCap
|
|
270
414
|
);
|
|
271
415
|
}
|
|
272
416
|
if (type === "text") {
|
|
@@ -283,7 +427,12 @@ function primitiveBounds(type, properties = {}) {
|
|
|
283
427
|
if (!Array.isArray(commands)) return undefined;
|
|
284
428
|
const bounds = resolvePathCommandBounds(commands);
|
|
285
429
|
if (bounds === undefined) return undefined;
|
|
286
|
-
return pathStrokeBounds(
|
|
430
|
+
return pathStrokeBounds(
|
|
431
|
+
commands,
|
|
432
|
+
bounds,
|
|
433
|
+
strokeExtent,
|
|
434
|
+
resolveStrokeDetails(properties)
|
|
435
|
+
);
|
|
287
436
|
}
|
|
288
437
|
return undefined;
|
|
289
438
|
}
|
|
@@ -18,14 +18,24 @@ const ENCODING_PATHS = Object.freeze([
|
|
|
18
18
|
"encoding.x.bin.maxBins",
|
|
19
19
|
"encoding.x.bin.step",
|
|
20
20
|
"encoding.x.bin.boundaries",
|
|
21
|
+
"encoding.x.weight",
|
|
21
22
|
"encoding.color.layout",
|
|
22
23
|
"encoding.color.aggregate",
|
|
24
|
+
"encoding.group.fields",
|
|
25
|
+
"encoding.group.inferredFrom",
|
|
26
|
+
...["x", "y", "x2", "y2", "theta", "color"].map(channel =>
|
|
27
|
+
`encoding.${channel}.temporalUnit`
|
|
28
|
+
),
|
|
23
29
|
"encoding.pathOrder.field",
|
|
24
30
|
"encoding.pathOrder.fieldType",
|
|
25
31
|
"encoding.pathOrder.order",
|
|
26
32
|
"encoding.theta.aggregate",
|
|
27
33
|
"encoding.theta.weight",
|
|
34
|
+
"encoding.radius.aggregate",
|
|
35
|
+
"encoding.theta.categoryOrder",
|
|
28
36
|
"encoding.text.format",
|
|
37
|
+
"encoding.text.content",
|
|
38
|
+
"encoding.text.normalizeBy",
|
|
29
39
|
"encoding.parallel.dimensions",
|
|
30
40
|
"encoding.parallel.key",
|
|
31
41
|
"encoding.parallel.missing",
|
|
@@ -45,7 +55,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
45
55
|
collection: "layers",
|
|
46
56
|
removableContainers: new Set(
|
|
47
57
|
[...ENCODING_CHANNELS.map(channel => `encoding.${channel}`),
|
|
48
|
-
"encoding.parallel"]
|
|
58
|
+
"encoding.parallel", "layout"]
|
|
49
59
|
),
|
|
50
60
|
properties: new Set([
|
|
51
61
|
"data",
|
|
@@ -53,6 +63,8 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
53
63
|
"coordinate",
|
|
54
64
|
"transform",
|
|
55
65
|
"mark.type",
|
|
66
|
+
"mark.missing",
|
|
67
|
+
"layout.mode",
|
|
56
68
|
...ENCODING_PATHS
|
|
57
69
|
])
|
|
58
70
|
},
|
|
@@ -60,13 +72,13 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
60
72
|
collection: "scales",
|
|
61
73
|
properties: new Set([
|
|
62
74
|
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
63
|
-
"unknown", "base", "exponent", "constant", "interpolate",
|
|
75
|
+
"unknown", "base", "exponent", "constant", "interpolate", "radialMapping", "midpoint",
|
|
64
76
|
"paddingInner", "paddingOuter", "padding", "align"
|
|
65
77
|
])
|
|
66
78
|
},
|
|
67
79
|
coordinate: {
|
|
68
80
|
collection: "coordinates",
|
|
69
|
-
properties: new Set(["type"])
|
|
81
|
+
properties: new Set(["type", "aspect", "polarFrame"])
|
|
70
82
|
}
|
|
71
83
|
});
|
|
72
84
|
|
|
@@ -86,8 +98,13 @@ const GUIDE_PATHS = new Set([
|
|
|
86
98
|
"axis.parallel.target",
|
|
87
99
|
"axis.parallel.coordinate",
|
|
88
100
|
"axis.parallel.scales",
|
|
101
|
+
"axis.parallel.titles",
|
|
89
102
|
"legend.color.scale",
|
|
90
103
|
"legend.color.title",
|
|
104
|
+
"legend.color.order",
|
|
105
|
+
"legend.stroke.scale",
|
|
106
|
+
"legend.stroke.title",
|
|
107
|
+
"legend.stroke.order",
|
|
91
108
|
"legend.size.scale",
|
|
92
109
|
"legend.size.title",
|
|
93
110
|
"legend.strokeWidth.scale",
|
|
@@ -97,6 +114,7 @@ const GUIDE_PATHS = new Set([
|
|
|
97
114
|
"legend.series.channels",
|
|
98
115
|
"legend.series.scales",
|
|
99
116
|
"legend.series.title",
|
|
117
|
+
"legend.series.order",
|
|
100
118
|
"grid.horizontal.scale",
|
|
101
119
|
"grid.horizontal.coordinate",
|
|
102
120
|
"grid.vertical.scale",
|
|
@@ -118,6 +136,7 @@ const GUIDE_REMOVABLE_CONTAINERS = new Set([
|
|
|
118
136
|
"grid.theta",
|
|
119
137
|
"grid.radial",
|
|
120
138
|
"legend.color",
|
|
139
|
+
"legend.stroke",
|
|
121
140
|
"legend.size",
|
|
122
141
|
"legend.strokeWidth",
|
|
123
142
|
"legend.opacity",
|
|
@@ -137,7 +156,7 @@ export function parseSemanticPath(property, { allowContainer = false } = {}) {
|
|
|
137
156
|
|
|
138
157
|
if (allowContainer) {
|
|
139
158
|
const entityMatch = property.match(
|
|
140
|
-
new RegExp(`^(dataset|layer)\\[(${USER_ID_SOURCE})\\]$`)
|
|
159
|
+
new RegExp(`^(dataset|layer|scale|coordinate)\\[(${USER_ID_SOURCE})\\]$`)
|
|
141
160
|
);
|
|
142
161
|
if (entityMatch) {
|
|
143
162
|
const [, kind, id] = entityMatch;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { layoutSeriesPartition } from "./seriesLayout.js";
|
|
3
|
+
|
|
4
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
5
|
+
"type", "category", "group", "value", "mode", "as"
|
|
6
|
+
]);
|
|
7
|
+
const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
|
|
8
|
+
const MODES = new Set(["stack", "fill", "center", "diverging"]);
|
|
9
|
+
const MAX_OUTPUT_ROWS = 10_000;
|
|
10
|
+
|
|
11
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
12
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
13
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireField(value, label) {
|
|
17
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeAs(value, field) {
|
|
24
|
+
const requested = value ?? {};
|
|
25
|
+
if (!isPlainObject(requested)) return requested;
|
|
26
|
+
return {
|
|
27
|
+
...requested,
|
|
28
|
+
start: requested.start ?? `${field}_start`,
|
|
29
|
+
end: requested.end ?? `${field}_end`,
|
|
30
|
+
value: requested.value ?? `${field}_value`,
|
|
31
|
+
share: requested.share ?? `${field}_share`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeStackTransform({ category, group, value, mode, as } = {}) {
|
|
36
|
+
const transform = {
|
|
37
|
+
type: "stack",
|
|
38
|
+
category,
|
|
39
|
+
group,
|
|
40
|
+
value,
|
|
41
|
+
mode: mode ?? "stack",
|
|
42
|
+
as: normalizeAs(as, value)
|
|
43
|
+
};
|
|
44
|
+
validateStackTransform(transform);
|
|
45
|
+
return cloneAndFreeze(transform);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateStackTransform(transform) {
|
|
49
|
+
if (!isPlainObject(transform)) {
|
|
50
|
+
throw new TypeError("Stack transform must be a plain object.");
|
|
51
|
+
}
|
|
52
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "stack transform");
|
|
53
|
+
if (transform.type !== "stack") {
|
|
54
|
+
throw new Error(`Unsupported stack transform "${transform.type}".`);
|
|
55
|
+
}
|
|
56
|
+
const roles = [
|
|
57
|
+
requireField(transform.category, "Stack category"),
|
|
58
|
+
requireField(transform.group, "Stack group"),
|
|
59
|
+
requireField(transform.value, "Stack value")
|
|
60
|
+
];
|
|
61
|
+
if (new Set(roles).size !== roles.length) {
|
|
62
|
+
throw new Error("Stack category, group, and value fields must be unique.");
|
|
63
|
+
}
|
|
64
|
+
if (!MODES.has(transform.mode)) {
|
|
65
|
+
throw new Error(`Unsupported stack mode "${transform.mode}".`);
|
|
66
|
+
}
|
|
67
|
+
if (!isPlainObject(transform.as)) {
|
|
68
|
+
throw new TypeError("Stack as must be a plain object.");
|
|
69
|
+
}
|
|
70
|
+
rejectUnknownKeys(transform.as, AS_KEYS, "stack as");
|
|
71
|
+
const outputs = AS_KEYS.map(key => requireField(transform.as[key], `Stack as.${key}`));
|
|
72
|
+
if (new Set(outputs).size !== outputs.length) {
|
|
73
|
+
throw new Error("Stack output fields must be unique.");
|
|
74
|
+
}
|
|
75
|
+
return transform;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scalarKey(value, label) {
|
|
79
|
+
if (value === null) return "null";
|
|
80
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
81
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
82
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
83
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
84
|
+
}
|
|
85
|
+
throw new TypeError(`${label} must contain null, strings, booleans, or finite numbers.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolvedPartition(entries, mode) {
|
|
89
|
+
const values = entries.map(entry => entry.row.value);
|
|
90
|
+
const segments = layoutSeriesPartition(values, mode);
|
|
91
|
+
const byIndex = new Map(segments.map(segment => [segment.index, segment]));
|
|
92
|
+
const shares = layoutSeriesPartition(values.map(Math.abs), "fill");
|
|
93
|
+
const shareByIndex = new Map(shares.map(segment => [
|
|
94
|
+
segment.index,
|
|
95
|
+
segment.end - segment.start
|
|
96
|
+
]));
|
|
97
|
+
let boundary = mode === "center" ? segments[0]?.start ?? 0 : 0;
|
|
98
|
+
return entries.map((entry, index) => {
|
|
99
|
+
const segment = byIndex.get(index);
|
|
100
|
+
const start = segment?.start ?? (mode === "diverging" ? 0 : boundary);
|
|
101
|
+
const end = segment?.end ?? start;
|
|
102
|
+
if (segment !== undefined && mode !== "diverging") boundary = segment.end;
|
|
103
|
+
return {
|
|
104
|
+
index: entry.index,
|
|
105
|
+
value: entry.row.value,
|
|
106
|
+
start,
|
|
107
|
+
end,
|
|
108
|
+
share: shareByIndex.get(index) ?? 0
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function deriveStackRows(rows, transform) {
|
|
114
|
+
validateStackTransform(transform);
|
|
115
|
+
if (rows.length > MAX_OUTPUT_ROWS) {
|
|
116
|
+
throw new RangeError(`Stack output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
117
|
+
}
|
|
118
|
+
const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
119
|
+
for (const output of Object.values(transform.as)) {
|
|
120
|
+
if (sourceFields.has(output)) {
|
|
121
|
+
throw new Error(`Stack output field "${output}" already exists.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const partitions = new Map();
|
|
125
|
+
const groups = new Map();
|
|
126
|
+
rows.forEach((source, index) => {
|
|
127
|
+
for (const field of [transform.category, transform.group, transform.value]) {
|
|
128
|
+
if (!Object.hasOwn(source, field)) {
|
|
129
|
+
throw new Error(`Stack source does not contain field "${field}" at row ${index}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const categoryKey = scalarKey(source[transform.category], "Stack category field");
|
|
133
|
+
const groupKey = scalarKey(source[transform.group], "Stack group field");
|
|
134
|
+
if (!Number.isFinite(source[transform.value])) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`Stack value field "${transform.value}" must contain a finite number at row ${index}.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (!groups.has(groupKey)) groups.set(groupKey, groups.size);
|
|
140
|
+
let partition = partitions.get(categoryKey);
|
|
141
|
+
if (partition === undefined) {
|
|
142
|
+
partition = new Map();
|
|
143
|
+
partitions.set(categoryKey, partition);
|
|
144
|
+
}
|
|
145
|
+
if (partition.has(groupKey)) {
|
|
146
|
+
throw new Error("Stack source requires one row per category/group cell.");
|
|
147
|
+
}
|
|
148
|
+
partition.set(groupKey, {
|
|
149
|
+
index,
|
|
150
|
+
row: { groupKey, value: source[transform.value] }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const resolved = new Array(rows.length);
|
|
155
|
+
for (const partition of partitions.values()) {
|
|
156
|
+
const entries = [...partition.values()].sort((left, right) =>
|
|
157
|
+
groups.get(left.row.groupKey) - groups.get(right.row.groupKey)
|
|
158
|
+
);
|
|
159
|
+
for (const item of resolvedPartition(entries, transform.mode)) {
|
|
160
|
+
resolved[item.index] = item;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return rows.map((row, index) => ({
|
|
164
|
+
...row,
|
|
165
|
+
[transform.as.start]: resolved[index].start,
|
|
166
|
+
[transform.as.end]: resolved[index].end,
|
|
167
|
+
[transform.as.value]: resolved[index].value,
|
|
168
|
+
[transform.as.share]: resolved[index].share
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { aggregateRows } from "./aggregate.js";
|
|
2
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
+
|
|
4
|
+
const STATISTIC_KEYS = Object.freeze(["op", "p"]);
|
|
5
|
+
const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
|
|
6
|
+
const POPULATIONS = new Set(["boundData", "visibleItems"]);
|
|
7
|
+
const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
|
|
8
|
+
|
|
9
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
10
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
11
|
+
if (unknown !== undefined) {
|
|
12
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeReferenceStatistic(value, label = "Reference statistic") {
|
|
17
|
+
if (!isPlainObject(value)) {
|
|
18
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
19
|
+
}
|
|
20
|
+
rejectUnknownKeys(value, STATISTIC_KEYS, label.toLowerCase());
|
|
21
|
+
if (SIMPLE_STATISTICS.has(value.op)) {
|
|
22
|
+
if (Object.hasOwn(value, "p")) {
|
|
23
|
+
throw new Error(`${label} ${value.op} does not accept p.`);
|
|
24
|
+
}
|
|
25
|
+
return cloneAndFreeze({ op: value.op });
|
|
26
|
+
}
|
|
27
|
+
if (value.op !== "quantile") {
|
|
28
|
+
throw new Error(`Unsupported ${label.toLowerCase()} "${value.op ?? "unknown"}".`);
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isFinite(value.p) || value.p < 0 || value.p > 1) {
|
|
31
|
+
throw new RangeError(`${label} quantile p must be between 0 and 1.`);
|
|
32
|
+
}
|
|
33
|
+
return cloneAndFreeze({ op: "quantile", p: value.p });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeReferenceStatistics(value, count, label) {
|
|
37
|
+
if (!Array.isArray(value) || value.length !== count) {
|
|
38
|
+
throw new TypeError(`${label} requires exactly ${count} statistic${count === 1 ? "" : "s"}.`);
|
|
39
|
+
}
|
|
40
|
+
return Object.freeze(value.map((statistic, index) =>
|
|
41
|
+
normalizeReferenceStatistic(statistic, `${label} statistic ${index + 1}`)
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeReferencePopulation(value, label = "Reference population") {
|
|
46
|
+
const population = value ?? "boundData";
|
|
47
|
+
if (!POPULATIONS.has(population)) {
|
|
48
|
+
throw new Error(`${label} must be boundData or visibleItems.`);
|
|
49
|
+
}
|
|
50
|
+
return population;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeReferenceField(value, label = "Reference field") {
|
|
54
|
+
if (value === undefined) return cloneAndFreeze({ kind: "axis" });
|
|
55
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
56
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
57
|
+
}
|
|
58
|
+
return cloneAndFreeze({ kind: "explicit", field: value });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeStatisticalReferenceTransform({ target } = {}) {
|
|
62
|
+
const transform = { type: "statisticalReference", target };
|
|
63
|
+
validateStatisticalReferenceTransform(transform);
|
|
64
|
+
return cloneAndFreeze(transform);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function validateStatisticalReferenceTransform(transform) {
|
|
68
|
+
if (!isPlainObject(transform)) {
|
|
69
|
+
throw new TypeError("Statistical reference transform must be a plain object.");
|
|
70
|
+
}
|
|
71
|
+
rejectUnknownKeys(
|
|
72
|
+
transform,
|
|
73
|
+
TRANSFORM_KEYS,
|
|
74
|
+
"statistical reference transform"
|
|
75
|
+
);
|
|
76
|
+
if (transform.type !== "statisticalReference") {
|
|
77
|
+
throw new Error(`Unsupported statistical reference transform "${transform.type}".`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof transform.target !== "string" || transform.target.length === 0) {
|
|
80
|
+
throw new TypeError("Statistical reference transform target must be a non-empty string.");
|
|
81
|
+
}
|
|
82
|
+
return transform;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function deriveStatisticalReferenceValues(values, statistics, label) {
|
|
86
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
87
|
+
throw new Error(`${label} population must contain at least one value.`);
|
|
88
|
+
}
|
|
89
|
+
values.forEach((value, index) => {
|
|
90
|
+
if (!Number.isFinite(value)) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
`${label} population value ${index} must be a finite number.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return statistics.map(statistic => aggregateRows(
|
|
97
|
+
values.map(value => ({ value })),
|
|
98
|
+
"value",
|
|
99
|
+
statistic.op === "quantile"
|
|
100
|
+
? { op: "quantile", probability: statistic.p }
|
|
101
|
+
: statistic.op
|
|
102
|
+
));
|
|
103
|
+
}
|