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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
import { normalizeMarkSelector } from "./markSelection.js";
|
|
4
|
+
|
|
5
|
+
function hasOwn(value, key) {
|
|
6
|
+
return Object.hasOwn(value, key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeNamedSelection(id) {
|
|
10
|
+
return cloneAndFreeze({
|
|
11
|
+
kind: "named",
|
|
12
|
+
id: validateUserId(id, "Label selection id")
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeInlineSelection(selector) {
|
|
17
|
+
return cloneAndFreeze({
|
|
18
|
+
kind: "inline",
|
|
19
|
+
selector: normalizeMarkSelector(selector)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeLabelSelectionCreate(options) {
|
|
24
|
+
if (!isPlainObject(options)) {
|
|
25
|
+
throw new TypeError("createMarkLabels options must be a plain object.");
|
|
26
|
+
}
|
|
27
|
+
const hasSelect = hasOwn(options, "select");
|
|
28
|
+
const hasSelection = hasOwn(options, "selection");
|
|
29
|
+
if (hasSelect && hasSelection) {
|
|
30
|
+
throw new Error("createMarkLabels accepts select or selection, not both.");
|
|
31
|
+
}
|
|
32
|
+
if (hasSelect) return normalizeInlineSelection(options.select);
|
|
33
|
+
if (hasSelection) return normalizeNamedSelection(options.selection);
|
|
34
|
+
return cloneAndFreeze({ kind: "all" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeLabelSelectionEdit(options) {
|
|
38
|
+
if (!isPlainObject(options)) {
|
|
39
|
+
throw new TypeError("editMarkLabelSelection options must be a plain object.");
|
|
40
|
+
}
|
|
41
|
+
const requested = ["select", "selection", "all"].filter(key =>
|
|
42
|
+
hasOwn(options, key)
|
|
43
|
+
);
|
|
44
|
+
if (requested.length !== 1) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"editMarkLabelSelection requires exactly one of select, selection, or all."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (requested[0] === "select") return normalizeInlineSelection(options.select);
|
|
50
|
+
if (requested[0] === "selection") {
|
|
51
|
+
return normalizeNamedSelection(options.selection);
|
|
52
|
+
}
|
|
53
|
+
if (options.all !== true) {
|
|
54
|
+
throw new TypeError("editMarkLabelSelection all must be true.");
|
|
55
|
+
}
|
|
56
|
+
return cloneAndFreeze({ kind: "all" });
|
|
57
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { aggregateRows } from "./aggregate.js";
|
|
2
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "./bars/policy.js";
|
|
3
|
+
import { stableFiniteSum } from "./numeric.js";
|
|
4
|
+
import {
|
|
5
|
+
readStatisticalWeights,
|
|
6
|
+
statisticalWeightTotal,
|
|
7
|
+
summarizeStatisticalWeights
|
|
8
|
+
} from "./weightedStatistics.js";
|
|
9
|
+
|
|
10
|
+
const CONTENTS = new Set(["category", "value", "share"]);
|
|
11
|
+
const NORMALIZATIONS = new Set(["source", "category"]);
|
|
12
|
+
|
|
13
|
+
export function validateMarkLabelContent(content) {
|
|
14
|
+
if (!CONTENTS.has(content)) throw new Error(`Unsupported text content "${content}".`);
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateMarkLabelNormalization(normalizeBy) {
|
|
19
|
+
if (!NORMALIZATIONS.has(normalizeBy)) throw new Error(`Unsupported text normalizeBy "${normalizeBy}".`);
|
|
20
|
+
return normalizeBy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeMarkLabelContent(source, { content, normalizeBy }) {
|
|
24
|
+
validateMarkLabelContent(content);
|
|
25
|
+
if (!["bar", "arc"].includes(source?.mark?.type)) {
|
|
26
|
+
throw new Error("Semantic text content requires a Bar or Arc source; use field or value for other marks.");
|
|
27
|
+
}
|
|
28
|
+
if (content !== "share" && normalizeBy !== undefined) {
|
|
29
|
+
throw new Error("Text normalizeBy is only supported with share content.");
|
|
30
|
+
}
|
|
31
|
+
const normalization = content === "share" ? normalizeBy ?? "source" : undefined;
|
|
32
|
+
if (normalization !== undefined) validateMarkLabelNormalization(normalization);
|
|
33
|
+
if (normalization === "category" && source.mark.type !== "bar") {
|
|
34
|
+
throw new Error("Category share normalization requires a Bar source.");
|
|
35
|
+
}
|
|
36
|
+
if (source.mark.type === "bar") {
|
|
37
|
+
const grain = resolveBarGrain(source);
|
|
38
|
+
if (grain === BAR_GRAINS.ranged) {
|
|
39
|
+
throw new Error("Ranged Bar text requires an explicit field or value.");
|
|
40
|
+
}
|
|
41
|
+
if (content === "category" && grain === BAR_GRAINS.histogram) {
|
|
42
|
+
throw new Error("Histogram categories are intervals; use an explicit text field or value.");
|
|
43
|
+
}
|
|
44
|
+
} else if (content === "category" && source.encoding?.theta?.fieldType === "quantitative") {
|
|
45
|
+
throw new Error("Category text requires categorical Arc theta.");
|
|
46
|
+
}
|
|
47
|
+
return { content, ...(normalization === undefined ? {} : { normalizeBy: normalization }) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function itemValue(source, item) {
|
|
51
|
+
if (source.mark.type === "bar") {
|
|
52
|
+
if (resolveBarGrain(source) === BAR_GRAINS.histogram) {
|
|
53
|
+
const weight = source.encoding.x.weight;
|
|
54
|
+
if (weight === undefined || item.members.length === 0) return item.members.length;
|
|
55
|
+
const weighted = readStatisticalWeights(item.members, weight, "Histogram label");
|
|
56
|
+
return statisticalWeightTotal(
|
|
57
|
+
summarizeStatisticalWeights(
|
|
58
|
+
weighted.entries,
|
|
59
|
+
weighted.definition.kind,
|
|
60
|
+
"Histogram label"
|
|
61
|
+
),
|
|
62
|
+
"Histogram label value"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const measure = source.encoding[resolveBarChannels(source).measure];
|
|
66
|
+
return measure.aggregate === "count" ? item.members.length
|
|
67
|
+
: aggregateRows(item.members, measure.field, measure.aggregate);
|
|
68
|
+
}
|
|
69
|
+
if (source.encoding?.radius !== undefined) return item.channels.radius;
|
|
70
|
+
const theta = source.encoding.theta;
|
|
71
|
+
if (theta.fieldType === "quantitative") return item.channels.theta;
|
|
72
|
+
return theta.aggregate === "count" ? item.members.length
|
|
73
|
+
: aggregateRows(item.members, theta.weight, "sum");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function categoryValue(source, item) {
|
|
77
|
+
return item.channels[source.mark.type === "arc" ? "theta" : resolveBarChannels(source).category];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function categoryKey(source, item) {
|
|
81
|
+
return JSON.stringify(resolveBarGrain(source) === BAR_GRAINS.histogram
|
|
82
|
+
? [item.channels.x, item.channels.x2]
|
|
83
|
+
: [categoryValue(source, item)]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveMarkLabelValues(source, items, options) {
|
|
87
|
+
const config = normalizeMarkLabelContent(source, options);
|
|
88
|
+
if (config.content === "category") return items.map(item => categoryValue(source, item));
|
|
89
|
+
const values = items.map(item => itemValue(source, item));
|
|
90
|
+
if (config.content === "value" || items.length === 0) return values;
|
|
91
|
+
if (values.some(value => !Number.isFinite(value) || value < 0)) {
|
|
92
|
+
throw new Error("Share labels require finite non-negative source values.");
|
|
93
|
+
}
|
|
94
|
+
const groups = new Map();
|
|
95
|
+
items.forEach((item, index) => {
|
|
96
|
+
const key = config.normalizeBy === "source" ? "source" : categoryKey(source, item);
|
|
97
|
+
const group = groups.get(key) ?? [];
|
|
98
|
+
group.push(index);
|
|
99
|
+
groups.set(key, group);
|
|
100
|
+
});
|
|
101
|
+
const shares = [];
|
|
102
|
+
for (const indices of groups.values()) {
|
|
103
|
+
const maximum = indices.reduce((max, index) => Math.max(max, values[index]), 0);
|
|
104
|
+
if (maximum === 0) throw new Error("Share label denominator must be positive.");
|
|
105
|
+
// Ratios remain defined even when the unscaled sum would overflow.
|
|
106
|
+
const total = stableFiniteSum(indices.map(index => values[index] / maximum), "Share label denominator");
|
|
107
|
+
for (const index of indices) shares[index] = (values[index] / maximum) / total;
|
|
108
|
+
}
|
|
109
|
+
return shares;
|
|
110
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
inverseLerp,
|
|
4
|
+
numericExtent,
|
|
5
|
+
requireFiniteResult,
|
|
6
|
+
stableFiniteDeviation,
|
|
7
|
+
stableFiniteSum
|
|
8
|
+
} from "./numeric.js";
|
|
9
|
+
|
|
10
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
11
|
+
"type", "field", "as", "groupBy", "method", "variance",
|
|
12
|
+
"zeroDenominator", "baseline", "sortBy"
|
|
13
|
+
]);
|
|
14
|
+
const METHODS = Object.freeze([
|
|
15
|
+
"share", "zscore", "minmax", "index", "change", "percentChange"
|
|
16
|
+
]);
|
|
17
|
+
const ZERO_POLICIES = Object.freeze(["error", "null", "zero"]);
|
|
18
|
+
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
19
|
+
const ORDERS = Object.freeze(["ascending", "descending"]);
|
|
20
|
+
const BASELINE_KEYS = Object.freeze(["position", "value"]);
|
|
21
|
+
const BASELINE_METHODS = new Set(["index", "change", "percentChange"]);
|
|
22
|
+
|
|
23
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
24
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
25
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireField(value, label) {
|
|
29
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
30
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeGroupBy(value) {
|
|
36
|
+
if (value === undefined) return [];
|
|
37
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateGroupBy(groupBy) {
|
|
41
|
+
if (!Array.isArray(groupBy) || groupBy.some(field =>
|
|
42
|
+
typeof field !== "string" || field.length === 0
|
|
43
|
+
)) {
|
|
44
|
+
throw new TypeError("Normalize groupBy must contain field names.");
|
|
45
|
+
}
|
|
46
|
+
if (new Set(groupBy).size !== groupBy.length) {
|
|
47
|
+
throw new Error("Normalize groupBy fields must be unique.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeSortBy(value) {
|
|
52
|
+
if (value === undefined) return [];
|
|
53
|
+
if (!Array.isArray(value)) return value;
|
|
54
|
+
return value.map(sort => isPlainObject(sort)
|
|
55
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
56
|
+
: sort
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateSortBy(sortBy) {
|
|
61
|
+
if (!Array.isArray(sortBy)) throw new TypeError("Normalize sortBy must be an array.");
|
|
62
|
+
const fields = [];
|
|
63
|
+
sortBy.forEach((sort, index) => {
|
|
64
|
+
if (!isPlainObject(sort)) {
|
|
65
|
+
throw new TypeError(`Normalize sortBy[${index}] must be a plain object.`);
|
|
66
|
+
}
|
|
67
|
+
rejectUnknownKeys(sort, SORT_KEYS, `normalize sortBy[${index}]`);
|
|
68
|
+
fields.push(requireField(sort.field, `Normalize sortBy[${index}].field`));
|
|
69
|
+
if (!ORDERS.includes(sort.order)) {
|
|
70
|
+
throw new Error(`Unsupported normalize sort order "${sort.order}".`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (new Set(fields).size !== fields.length) {
|
|
74
|
+
throw new Error("Normalize sortBy fields must be unique.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeBaseline(value) {
|
|
79
|
+
if (!isPlainObject(value)) return value;
|
|
80
|
+
return { ...value };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateBaseline(baseline, sortBy) {
|
|
84
|
+
if (!isPlainObject(baseline)) {
|
|
85
|
+
throw new TypeError("Normalize baseline must be a plain object.");
|
|
86
|
+
}
|
|
87
|
+
rejectUnknownKeys(baseline, BASELINE_KEYS, "normalize baseline");
|
|
88
|
+
const modes = BASELINE_KEYS.filter(key => Object.hasOwn(baseline, key));
|
|
89
|
+
if (modes.length !== 1) {
|
|
90
|
+
throw new Error("Normalize baseline requires exactly one of position or value.");
|
|
91
|
+
}
|
|
92
|
+
if (modes[0] === "value") {
|
|
93
|
+
if (!Number.isFinite(baseline.value)) {
|
|
94
|
+
throw new TypeError("Normalize baseline value must be finite.");
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (!["first", "last"].includes(baseline.position)) {
|
|
98
|
+
throw new Error("Normalize baseline position must be first or last.");
|
|
99
|
+
}
|
|
100
|
+
if (sortBy.length === 0) {
|
|
101
|
+
throw new Error("Normalize baseline position requires non-empty sortBy.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function validateNormalizeTransform(transform) {
|
|
107
|
+
if (!isPlainObject(transform)) {
|
|
108
|
+
throw new TypeError("Normalize transform must be a plain object.");
|
|
109
|
+
}
|
|
110
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "normalize transform");
|
|
111
|
+
if (transform.type !== "normalize") {
|
|
112
|
+
throw new Error(`Unsupported normalize transform "${transform.type}".`);
|
|
113
|
+
}
|
|
114
|
+
requireField(transform.field, "Normalize field");
|
|
115
|
+
requireField(transform.as, "Normalize output field");
|
|
116
|
+
validateGroupBy(transform.groupBy);
|
|
117
|
+
if (!METHODS.includes(transform.method)) {
|
|
118
|
+
throw new Error(`Unsupported normalize method "${transform.method}".`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hasVariance = Object.hasOwn(transform, "variance");
|
|
122
|
+
const hasZero = Object.hasOwn(transform, "zeroDenominator");
|
|
123
|
+
const hasBaseline = Object.hasOwn(transform, "baseline");
|
|
124
|
+
const hasSort = Object.hasOwn(transform, "sortBy");
|
|
125
|
+
if (transform.method === "zscore") {
|
|
126
|
+
if (!["population", "sample"].includes(transform.variance)) {
|
|
127
|
+
throw new Error("Normalize zscore variance must be population or sample.");
|
|
128
|
+
}
|
|
129
|
+
} else if (hasVariance) {
|
|
130
|
+
throw new Error(`Normalize variance is not available for ${transform.method}.`);
|
|
131
|
+
}
|
|
132
|
+
if (hasZero && !ZERO_POLICIES.includes(transform.zeroDenominator)) {
|
|
133
|
+
throw new Error(`Unsupported normalize zeroDenominator "${transform.zeroDenominator}".`);
|
|
134
|
+
}
|
|
135
|
+
if (transform.method === "change" && hasZero) {
|
|
136
|
+
throw new Error("Normalize zeroDenominator is not available for change.");
|
|
137
|
+
}
|
|
138
|
+
if (BASELINE_METHODS.has(transform.method)) {
|
|
139
|
+
validateSortBy(transform.sortBy);
|
|
140
|
+
validateBaseline(transform.baseline, transform.sortBy);
|
|
141
|
+
} else {
|
|
142
|
+
if (hasBaseline) {
|
|
143
|
+
throw new Error(`Normalize baseline is not available for ${transform.method}.`);
|
|
144
|
+
}
|
|
145
|
+
if (hasSort) {
|
|
146
|
+
throw new Error(`Normalize sortBy is not available for ${transform.method}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return transform;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function normalizeNormalizeTransform(args = {}) {
|
|
153
|
+
const {
|
|
154
|
+
field,
|
|
155
|
+
as,
|
|
156
|
+
groupBy,
|
|
157
|
+
method,
|
|
158
|
+
variance,
|
|
159
|
+
zeroDenominator,
|
|
160
|
+
baseline,
|
|
161
|
+
sortBy
|
|
162
|
+
} = args;
|
|
163
|
+
const transform = {
|
|
164
|
+
type: "normalize",
|
|
165
|
+
field,
|
|
166
|
+
as,
|
|
167
|
+
groupBy: normalizeGroupBy(groupBy),
|
|
168
|
+
method
|
|
169
|
+
};
|
|
170
|
+
if (method === "zscore") transform.variance = variance ?? "population";
|
|
171
|
+
else if (Object.hasOwn(args, "variance")) transform.variance = variance;
|
|
172
|
+
if (method !== "change") transform.zeroDenominator = zeroDenominator ?? "error";
|
|
173
|
+
else if (Object.hasOwn(args, "zeroDenominator")) {
|
|
174
|
+
transform.zeroDenominator = zeroDenominator;
|
|
175
|
+
}
|
|
176
|
+
if (BASELINE_METHODS.has(method)) {
|
|
177
|
+
transform.baseline = normalizeBaseline(baseline ?? { position: "first" });
|
|
178
|
+
transform.sortBy = normalizeSortBy(sortBy);
|
|
179
|
+
} else {
|
|
180
|
+
if (Object.hasOwn(args, "baseline")) transform.baseline = normalizeBaseline(baseline);
|
|
181
|
+
if (Object.hasOwn(args, "sortBy")) transform.sortBy = normalizeSortBy(sortBy);
|
|
182
|
+
}
|
|
183
|
+
validateNormalizeTransform(transform);
|
|
184
|
+
return cloneAndFreeze(transform);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function scalarKey(value, label) {
|
|
188
|
+
if (value === null) return "null";
|
|
189
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
190
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
191
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
192
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
193
|
+
}
|
|
194
|
+
throw new TypeError(
|
|
195
|
+
`${label} must contain null, strings, booleans, or finite numbers.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function compareScalar(left, right, label) {
|
|
200
|
+
const leftMissing = left === null || left === undefined;
|
|
201
|
+
const rightMissing = right === null || right === undefined;
|
|
202
|
+
if (leftMissing || rightMissing) {
|
|
203
|
+
if (leftMissing && rightMissing) return 0;
|
|
204
|
+
return leftMissing ? 1 : -1;
|
|
205
|
+
}
|
|
206
|
+
if (typeof left !== typeof right ||
|
|
207
|
+
!["number", "string", "boolean"].includes(typeof left) ||
|
|
208
|
+
typeof left === "number" && (!Number.isFinite(left) || !Number.isFinite(right))) {
|
|
209
|
+
throw new TypeError(`${label} must contain one comparable primitive type.`);
|
|
210
|
+
}
|
|
211
|
+
if (left === right) return 0;
|
|
212
|
+
return left < right ? -1 : 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function compareEntries(left, right, sortBy) {
|
|
216
|
+
for (const sort of sortBy) {
|
|
217
|
+
const comparison = compareScalar(
|
|
218
|
+
left.row[sort.field],
|
|
219
|
+
right.row[sort.field],
|
|
220
|
+
`Normalize sort field "${sort.field}"`
|
|
221
|
+
);
|
|
222
|
+
if (comparison !== 0) return sort.order === "ascending" ? comparison : -comparison;
|
|
223
|
+
}
|
|
224
|
+
return left.index - right.index;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateRows(rows, transform) {
|
|
228
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
229
|
+
throw new TypeError("Normalize source rows must be plain objects.");
|
|
230
|
+
}
|
|
231
|
+
if (rows.length === 0) return;
|
|
232
|
+
const fields = [
|
|
233
|
+
transform.field,
|
|
234
|
+
...transform.groupBy,
|
|
235
|
+
...(transform.sortBy ?? []).map(sort => sort.field)
|
|
236
|
+
];
|
|
237
|
+
rows.forEach((row, index) => {
|
|
238
|
+
if (Object.hasOwn(row, transform.as)) {
|
|
239
|
+
throw new Error(`Normalize output field "${transform.as}" already exists.`);
|
|
240
|
+
}
|
|
241
|
+
for (const field of fields) {
|
|
242
|
+
if (!Object.hasOwn(row, field)) {
|
|
243
|
+
throw new Error(`Normalize source does not contain field "${field}" at row ${index}.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!Number.isFinite(row[transform.field])) {
|
|
247
|
+
throw new TypeError(
|
|
248
|
+
`Normalize field "${transform.field}" must contain finite numbers at row ${index}.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
for (const field of transform.groupBy) {
|
|
252
|
+
scalarKey(row[field], `Normalize group field "${field}"`);
|
|
253
|
+
}
|
|
254
|
+
for (const sort of transform.sortBy ?? []) {
|
|
255
|
+
compareScalar(
|
|
256
|
+
row[sort.field],
|
|
257
|
+
row[sort.field],
|
|
258
|
+
`Normalize sort field "${sort.field}"`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function groupRows(rows, transform) {
|
|
265
|
+
const groups = new Map();
|
|
266
|
+
rows.forEach((row, index) => {
|
|
267
|
+
const key = transform.groupBy.length === 0
|
|
268
|
+
? "all"
|
|
269
|
+
: transform.groupBy.map(field =>
|
|
270
|
+
scalarKey(row[field], `Normalize group field "${field}"`)
|
|
271
|
+
).join("\0");
|
|
272
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
273
|
+
groups.get(key).push({ row, index });
|
|
274
|
+
});
|
|
275
|
+
return [...groups.values()];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function zeroResult(policy, method) {
|
|
279
|
+
if (policy === "null") return null;
|
|
280
|
+
if (policy === "zero") return 0;
|
|
281
|
+
throw new RangeError(`Normalize ${method} denominator is zero.`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function finiteOutput(value, method) {
|
|
285
|
+
return requireFiniteResult(value, `Normalize ${method} output`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function zscore(value, mean, deviation, method) {
|
|
289
|
+
const difference = value - mean;
|
|
290
|
+
return finiteOutput(
|
|
291
|
+
Number.isFinite(difference) ? difference / deviation : value / deviation - mean / deviation,
|
|
292
|
+
method
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function applyGroup(group, transform, outputs) {
|
|
297
|
+
const values = group.map(entry => entry.row[transform.field]);
|
|
298
|
+
if (transform.method === "share" && values.some(value => value < 0)) {
|
|
299
|
+
throw new RangeError("Normalize share does not accept negative values.");
|
|
300
|
+
}
|
|
301
|
+
if (transform.method === "share") {
|
|
302
|
+
const total = stableFiniteSum(values, "Normalize share sum");
|
|
303
|
+
group.forEach((entry, index) => {
|
|
304
|
+
outputs[entry.index] = total === 0
|
|
305
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
306
|
+
: finiteOutput(values[index] / total, transform.method);
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (transform.method === "minmax") {
|
|
311
|
+
const [minimum, maximum] = numericExtent(values);
|
|
312
|
+
group.forEach((entry, index) => {
|
|
313
|
+
outputs[entry.index] = minimum === maximum
|
|
314
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
315
|
+
: finiteOutput(inverseLerp(values[index], minimum, maximum), transform.method);
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (transform.method === "zscore") {
|
|
320
|
+
if (transform.variance === "sample" && values.length < 2) {
|
|
321
|
+
throw new RangeError("Normalize sample zscore requires at least two values.");
|
|
322
|
+
}
|
|
323
|
+
const { mean, deviation } = stableFiniteDeviation(values, {
|
|
324
|
+
sample: transform.variance === "sample",
|
|
325
|
+
label: "Normalize zscore deviation"
|
|
326
|
+
});
|
|
327
|
+
group.forEach((entry, index) => {
|
|
328
|
+
outputs[entry.index] = deviation === 0
|
|
329
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
330
|
+
: zscore(values[index], mean, deviation, transform.method);
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const sorted = [...group].sort((left, right) =>
|
|
336
|
+
compareEntries(left, right, transform.sortBy)
|
|
337
|
+
);
|
|
338
|
+
const baseline = Object.hasOwn(transform.baseline, "value")
|
|
339
|
+
? transform.baseline.value
|
|
340
|
+
: sorted[transform.baseline.position === "first" ? 0 : sorted.length - 1]
|
|
341
|
+
.row[transform.field];
|
|
342
|
+
group.forEach(entry => {
|
|
343
|
+
const value = entry.row[transform.field];
|
|
344
|
+
if (transform.method === "change") {
|
|
345
|
+
outputs[entry.index] = finiteOutput(value - baseline, transform.method);
|
|
346
|
+
} else if (baseline === 0) {
|
|
347
|
+
outputs[entry.index] = zeroResult(transform.zeroDenominator, transform.method);
|
|
348
|
+
} else if (transform.method === "index") {
|
|
349
|
+
outputs[entry.index] = finiteOutput(value / baseline * 100, transform.method);
|
|
350
|
+
} else {
|
|
351
|
+
outputs[entry.index] = finiteOutput(value / baseline - 1, transform.method);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function deriveNormalizedRows(rows, transform) {
|
|
357
|
+
validateNormalizeTransform(transform);
|
|
358
|
+
validateRows(rows, transform);
|
|
359
|
+
if (rows.length === 0) return cloneAndFreeze([]);
|
|
360
|
+
const outputs = new Array(rows.length);
|
|
361
|
+
for (const group of groupRows(rows, transform)) applyGroup(group, transform, outputs);
|
|
362
|
+
return cloneAndFreeze(rows.map((row, index) => ({
|
|
363
|
+
...row,
|
|
364
|
+
[transform.as]: outputs[index]
|
|
365
|
+
})));
|
|
366
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { validateNonEmptyString, validateOptionObject } from "../core/validation.js";
|
|
2
|
+
|
|
3
|
+
export function validateParallelAxisTitles(value) {
|
|
4
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
5
|
+
throw new TypeError("Parallel axis titles must be a non-empty field/text array.");
|
|
6
|
+
}
|
|
7
|
+
const fields = new Set();
|
|
8
|
+
for (const title of value) {
|
|
9
|
+
validateOptionObject(title, ["field", "text"], "Parallel axis title");
|
|
10
|
+
validateNonEmptyString(title.field, "Parallel axis title field");
|
|
11
|
+
validateNonEmptyString(title.text, "Parallel axis title text");
|
|
12
|
+
if (fields.has(title.field)) throw new Error("Parallel axis titles require unique fields.");
|
|
13
|
+
fields.add(title.field);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { validateNonEmptyString } from "../core/validation.js";
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
readQuantitativeField,
|
|
5
|
+
readScaleField
|
|
6
|
+
} from "./scales/fields.js";
|
|
7
|
+
|
|
8
|
+
export function normalizeGroupFields(value) {
|
|
9
|
+
const fields = Array.isArray(value) ? [...value] : [value];
|
|
10
|
+
for (const field of fields) validateNonEmptyString(field, "Path group field");
|
|
11
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length) {
|
|
12
|
+
throw new Error("Path group fields must be non-empty and unique.");
|
|
13
|
+
}
|
|
14
|
+
return fields;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pathGroupFields(layer) {
|
|
18
|
+
const group = layer.encoding?.group;
|
|
19
|
+
if (group !== undefined) {
|
|
20
|
+
if (group.fieldType !== "nominal") {
|
|
21
|
+
throw new Error(`Path group on mark "${layer.id}" must be nominal.`);
|
|
22
|
+
}
|
|
23
|
+
if (Object.hasOwn(group, "field") && Object.hasOwn(group, "fields")) {
|
|
24
|
+
throw new Error("Path group requires exactly one of field or fields.");
|
|
25
|
+
}
|
|
26
|
+
return normalizeGroupFields(group.fields ?? group.field);
|
|
27
|
+
}
|
|
28
|
+
if (layer.mark?.type !== "line") return [];
|
|
29
|
+
const color = layer.encoding?.color?.field;
|
|
30
|
+
const dash = layer.encoding?.strokeDash?.field;
|
|
31
|
+
if (color !== undefined && dash !== undefined && color !== dash) {
|
|
32
|
+
throw new Error("Line color field must match strokeDash field unless encodeGroup defines the series.");
|
|
33
|
+
}
|
|
34
|
+
return color === undefined && dash === undefined ? [] : [color ?? dash];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readPathSeriesFields(rows, layer) {
|
|
38
|
+
const fields = pathGroupFields(layer);
|
|
39
|
+
for (const field of fields) readNominalField(rows, field);
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function seriesValueIndex(rows, fields, field, channel, encoding = {}) {
|
|
44
|
+
const quantitative = channel === "strokeWidth" || channel === "opacity";
|
|
45
|
+
const values = channel === "stroke"
|
|
46
|
+
? readScaleField(rows, field, encoding.fieldType ?? "nominal", {
|
|
47
|
+
temporalUnit: encoding.temporalUnit
|
|
48
|
+
})
|
|
49
|
+
: quantitative
|
|
50
|
+
? readQuantitativeField(rows, field)
|
|
51
|
+
: readNominalField(rows, field);
|
|
52
|
+
const result = new Map();
|
|
53
|
+
rows.forEach((row, index) => {
|
|
54
|
+
const key = JSON.stringify(fields.map(field => row[field]));
|
|
55
|
+
const value = values[index];
|
|
56
|
+
if (channel === "strokeWidth" && value < 0) {
|
|
57
|
+
throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
|
|
58
|
+
}
|
|
59
|
+
if (result.has(key) && result.get(key) !== value) {
|
|
60
|
+
throw new Error(`Path ${channel} field "${field}" must have one value within each series.`);
|
|
61
|
+
}
|
|
62
|
+
result.set(key, value);
|
|
63
|
+
});
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function derivePathSeriesFieldValues(
|
|
68
|
+
rows,
|
|
69
|
+
series,
|
|
70
|
+
field,
|
|
71
|
+
channel,
|
|
72
|
+
encoding
|
|
73
|
+
) {
|
|
74
|
+
const fields = Object.keys(series[0]?.key ?? {});
|
|
75
|
+
const values = seriesValueIndex(rows, fields, field, channel, encoding);
|
|
76
|
+
return series.map(item => values.get(JSON.stringify(Object.values(item.key))));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validatePathSeriesAppearance(rows, layer) {
|
|
80
|
+
if (!["line", "area"].includes(layer.mark?.type) ||
|
|
81
|
+
layer.encoding?.parallel !== undefined) return;
|
|
82
|
+
const grouping = readPathSeriesFields(rows, layer);
|
|
83
|
+
const channels = layer.mark.type === "line"
|
|
84
|
+
? ["color", "stroke", "strokeDash", "strokeWidth", "opacity"]
|
|
85
|
+
: ["color", "stroke"];
|
|
86
|
+
for (const channel of channels) {
|
|
87
|
+
const encoding = layer.encoding?.[channel];
|
|
88
|
+
if (encoding?.field === undefined) continue;
|
|
89
|
+
const allowed = ["color", "stroke"].includes(channel)
|
|
90
|
+
? channel === "color"
|
|
91
|
+
? ["nominal", "ordinal"]
|
|
92
|
+
: ["nominal", "ordinal", "quantitative", "temporal"]
|
|
93
|
+
: channel === "strokeDash" ? ["nominal"] : ["quantitative"];
|
|
94
|
+
if (!allowed.includes(encoding.fieldType)) {
|
|
95
|
+
throw new Error(`Path ${channel} encoding has an unsupported field type.`);
|
|
96
|
+
}
|
|
97
|
+
seriesValueIndex(rows, grouping, encoding.field, channel, encoding);
|
|
98
|
+
}
|
|
99
|
+
return grouping;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function readSeriesIdentity(rows, layer) {
|
|
103
|
+
const group = layer.encoding?.group;
|
|
104
|
+
const fields = group === undefined ? [] : normalizeGroupFields(group.fields ?? group.field);
|
|
105
|
+
for (const field of fields) readNominalField(rows, field);
|
|
106
|
+
const values = rows.map(row => fields.length === 0 ? undefined : fields.length === 1
|
|
107
|
+
? row[fields[0]] : JSON.stringify(fields.map(field => row[field])));
|
|
108
|
+
return { fields, values, domain: [...new Set(values)] };
|
|
109
|
+
}
|