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,340 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
3
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
4
|
+
|
|
5
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
6
|
+
"type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
|
|
7
|
+
]);
|
|
8
|
+
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
9
|
+
const METHODS = Object.freeze(["constant", "forward", "backward", "linear"]);
|
|
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 validateFieldList(value, label) {
|
|
24
|
+
if (!Array.isArray(value) || value.length === 0 || value.some(field =>
|
|
25
|
+
typeof field !== "string" || field.length === 0
|
|
26
|
+
)) {
|
|
27
|
+
throw new TypeError(`${label} must be a non-empty field array.`);
|
|
28
|
+
}
|
|
29
|
+
if (new Set(value).size !== value.length) throw new Error(`${label} fields must be unique.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeFieldList(value, { allowEmpty = false } = {}) {
|
|
33
|
+
if (value === undefined) return allowEmpty ? [] : value;
|
|
34
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeSortBy(value) {
|
|
38
|
+
if (value === undefined) return [];
|
|
39
|
+
if (!Array.isArray(value)) return value;
|
|
40
|
+
return value.map(sort => isPlainObject(sort)
|
|
41
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
42
|
+
: sort
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateSortBy(sortBy) {
|
|
47
|
+
if (!Array.isArray(sortBy)) throw new TypeError("Impute sortBy must be an array.");
|
|
48
|
+
const fields = [];
|
|
49
|
+
sortBy.forEach((sort, index) => {
|
|
50
|
+
if (!isPlainObject(sort)) {
|
|
51
|
+
throw new TypeError(`Impute sortBy[${index}] must be a plain object.`);
|
|
52
|
+
}
|
|
53
|
+
rejectUnknownKeys(sort, SORT_KEYS, `impute sortBy[${index}]`);
|
|
54
|
+
fields.push(requireField(sort.field, `Impute sortBy[${index}].field`));
|
|
55
|
+
if (!["ascending", "descending"].includes(sort.order)) {
|
|
56
|
+
throw new Error(`Unsupported impute sort order "${sort.order}".`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (new Set(fields).size !== fields.length) {
|
|
60
|
+
throw new Error("Impute sortBy fields must be unique.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scalarType(value, label) {
|
|
65
|
+
if (value === null || value === undefined) return undefined;
|
|
66
|
+
if (typeof value === "string" || typeof value === "boolean") return typeof value;
|
|
67
|
+
if (typeof value === "number" && Number.isFinite(value)) return "number";
|
|
68
|
+
throw new TypeError(`${label} must contain scalar values; NaN and Infinity are invalid.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scalarKey(value, label) {
|
|
72
|
+
const type = scalarType(value, label);
|
|
73
|
+
if (type === undefined) return "null";
|
|
74
|
+
if (type === "number") return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
75
|
+
if (type === "string") return `string:${value.length}:${value}`;
|
|
76
|
+
return `${type}:${value}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateImputeTransform(transform) {
|
|
80
|
+
if (!isPlainObject(transform)) {
|
|
81
|
+
throw new TypeError("Impute transform must be a plain object.");
|
|
82
|
+
}
|
|
83
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "impute transform");
|
|
84
|
+
if (transform.type !== "impute") {
|
|
85
|
+
throw new Error(`Unsupported impute transform "${transform.type}".`);
|
|
86
|
+
}
|
|
87
|
+
validateFieldList(transform.fields, "Impute fields");
|
|
88
|
+
if (!Array.isArray(transform.groupBy) || transform.groupBy.some(field =>
|
|
89
|
+
typeof field !== "string" || field.length === 0
|
|
90
|
+
)) {
|
|
91
|
+
throw new TypeError("Impute groupBy must contain field names.");
|
|
92
|
+
}
|
|
93
|
+
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
94
|
+
throw new Error("Impute groupBy fields must be unique.");
|
|
95
|
+
}
|
|
96
|
+
validateSortBy(transform.sortBy);
|
|
97
|
+
if (!METHODS.includes(transform.method)) {
|
|
98
|
+
throw new Error(`Unsupported impute method "${transform.method}".`);
|
|
99
|
+
}
|
|
100
|
+
const hasValue = Object.hasOwn(transform, "value");
|
|
101
|
+
if (transform.method === "constant") {
|
|
102
|
+
if (!hasValue) throw new TypeError("Impute constant requires a value.");
|
|
103
|
+
if (transform.value === undefined) {
|
|
104
|
+
throw new TypeError("Impute constant value must be a JSON-safe scalar.");
|
|
105
|
+
}
|
|
106
|
+
scalarType(transform.value, "Impute constant value");
|
|
107
|
+
} else {
|
|
108
|
+
if (hasValue) throw new Error(`Impute value is not available for ${transform.method}.`);
|
|
109
|
+
if (transform.sortBy.length === 0) {
|
|
110
|
+
throw new Error(`Impute ${transform.method} requires non-empty sortBy.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (transform.method === "linear") {
|
|
114
|
+
if (transform.sortBy.length !== 1 || transform.sortBy[0].order !== "ascending") {
|
|
115
|
+
throw new Error("Impute linear requires exactly one ascending sortBy field.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!['keep', 'error'].includes(transform.edges)) {
|
|
119
|
+
throw new Error(`Unsupported impute edges policy "${transform.edges}".`);
|
|
120
|
+
}
|
|
121
|
+
if (transform.maxGap !== undefined &&
|
|
122
|
+
(!Number.isSafeInteger(transform.maxGap) || transform.maxGap <= 0)) {
|
|
123
|
+
throw new RangeError("Impute maxGap must be a positive safe integer.");
|
|
124
|
+
}
|
|
125
|
+
return transform;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeImputeTransform(args = {}) {
|
|
129
|
+
const transform = {
|
|
130
|
+
type: "impute",
|
|
131
|
+
fields: normalizeFieldList(args.fields),
|
|
132
|
+
groupBy: normalizeFieldList(args.groupBy, { allowEmpty: true }),
|
|
133
|
+
sortBy: normalizeSortBy(args.sortBy),
|
|
134
|
+
method: args.method,
|
|
135
|
+
edges: args.edges ?? "keep"
|
|
136
|
+
};
|
|
137
|
+
if (Object.hasOwn(args, "value")) transform.value = args.value;
|
|
138
|
+
if (Object.hasOwn(args, "maxGap")) transform.maxGap = args.maxGap;
|
|
139
|
+
validateImputeTransform(transform);
|
|
140
|
+
return cloneAndFreeze(transform);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compareScalar(left, right, label) {
|
|
144
|
+
const leftType = scalarType(left, label);
|
|
145
|
+
const rightType = scalarType(right, label);
|
|
146
|
+
if (leftType === undefined || rightType === undefined) {
|
|
147
|
+
if (leftType === rightType) return 0;
|
|
148
|
+
return leftType === undefined ? 1 : -1;
|
|
149
|
+
}
|
|
150
|
+
if (leftType !== rightType) throw new TypeError(`${label} must contain one scalar type.`);
|
|
151
|
+
if (left === right) return 0;
|
|
152
|
+
return left < right ? -1 : 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compareEntries(left, right, sortBy) {
|
|
156
|
+
for (const sort of sortBy) {
|
|
157
|
+
const comparison = compareScalar(
|
|
158
|
+
left.row[sort.field], right.row[sort.field], `Impute sort field "${sort.field}"`
|
|
159
|
+
);
|
|
160
|
+
if (comparison !== 0) return sort.order === "ascending" ? comparison : -comparison;
|
|
161
|
+
}
|
|
162
|
+
return left.index - right.index;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateRows(rows, transform) {
|
|
166
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
167
|
+
throw new TypeError("Impute source rows must be plain objects.");
|
|
168
|
+
}
|
|
169
|
+
const required = [...transform.fields, ...transform.groupBy,
|
|
170
|
+
...transform.sortBy.map(sort => sort.field)];
|
|
171
|
+
rows.forEach((row, index) => {
|
|
172
|
+
for (const field of required) {
|
|
173
|
+
if (!Object.hasOwn(row, field)) {
|
|
174
|
+
throw new Error(`Impute source does not contain field "${field}" at row ${index}.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const field of transform.fields) {
|
|
178
|
+
scalarType(row[field], `Impute field "${field}" at row ${index}`);
|
|
179
|
+
}
|
|
180
|
+
for (const field of transform.groupBy) {
|
|
181
|
+
scalarKey(row[field], `Impute group field "${field}" at row ${index}`);
|
|
182
|
+
}
|
|
183
|
+
for (const sort of transform.sortBy) {
|
|
184
|
+
scalarType(row[sort.field], `Impute sort field "${sort.field}" at row ${index}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function partitions(entries, transform) {
|
|
190
|
+
const groups = new Map();
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const key = transform.groupBy.length === 0 ? "all" : transform.groupBy.map(field =>
|
|
193
|
+
scalarKey(entry.row[field], `Impute group field "${field}"`)
|
|
194
|
+
).join("\0");
|
|
195
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
196
|
+
groups.get(key).push(entry);
|
|
197
|
+
}
|
|
198
|
+
return [...groups.values()];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateFieldTypes(entries, transform) {
|
|
202
|
+
for (const field of transform.fields) {
|
|
203
|
+
const types = new Set(entries.flatMap(entry => {
|
|
204
|
+
const type = scalarType(entry.row[field], `Impute field "${field}"`);
|
|
205
|
+
return type === undefined ? [] : [type];
|
|
206
|
+
}));
|
|
207
|
+
const constantType = transform.method === "constant"
|
|
208
|
+
? scalarType(transform.value, "Impute constant value")
|
|
209
|
+
: undefined;
|
|
210
|
+
if (constantType !== undefined) types.add(constantType);
|
|
211
|
+
if (types.size > 1) {
|
|
212
|
+
throw new TypeError(`Impute field "${field}" must contain one scalar type.`);
|
|
213
|
+
}
|
|
214
|
+
if (transform.method === "linear" &&
|
|
215
|
+
(types.size > 0 && !types.has("number"))) {
|
|
216
|
+
throw new TypeError(`Impute linear field "${field}" must contain finite numbers.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateSortFieldTypes(partition, transform) {
|
|
222
|
+
for (const sort of transform.sortBy) {
|
|
223
|
+
const types = new Set(partition.flatMap(entry => {
|
|
224
|
+
const type = scalarType(
|
|
225
|
+
entry.row[sort.field],
|
|
226
|
+
`Impute sort field "${sort.field}"`
|
|
227
|
+
);
|
|
228
|
+
return type === undefined ? [] : [type];
|
|
229
|
+
}));
|
|
230
|
+
if (types.size > 1) {
|
|
231
|
+
throw new TypeError(`Impute sort field "${sort.field}" must contain one scalar type.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function linearPositions(partition, transform) {
|
|
237
|
+
if (transform.method !== "linear") return undefined;
|
|
238
|
+
const field = transform.sortBy[0].field;
|
|
239
|
+
const types = new Set(partition.map((entry, index) =>
|
|
240
|
+
scalarType(entry.row[field], `Impute linear sort field "${field}" at row ${index}`)
|
|
241
|
+
));
|
|
242
|
+
if (types.size !== 1 || types.has(undefined) ||
|
|
243
|
+
!["number", "string"].includes([...types][0])) {
|
|
244
|
+
throw new TypeError("Impute linear sort field must contain one numeric or temporal-string type.");
|
|
245
|
+
}
|
|
246
|
+
const type = [...types][0];
|
|
247
|
+
const positions = partition.map((entry, index) => type === "number"
|
|
248
|
+
? entry.row[field]
|
|
249
|
+
: normalizeTemporalValue(entry.row[field], field, index, "auto")
|
|
250
|
+
);
|
|
251
|
+
for (let index = 1; index < positions.length; index += 1) {
|
|
252
|
+
if (positions[index] === positions[index - 1]) {
|
|
253
|
+
throw new Error("Impute linear sort positions must be unique.");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return positions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function missingRuns(partition, field) {
|
|
260
|
+
const runs = [];
|
|
261
|
+
let start;
|
|
262
|
+
for (let index = 0; index <= partition.length; index += 1) {
|
|
263
|
+
const missing = index < partition.length &&
|
|
264
|
+
(partition[index].row[field] === null || partition[index].row[field] === undefined);
|
|
265
|
+
if (missing && start === undefined) start = index;
|
|
266
|
+
if (!missing && start !== undefined) {
|
|
267
|
+
runs.push([start, index - 1]);
|
|
268
|
+
start = undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return runs;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function setField(row, field, value) {
|
|
275
|
+
Object.defineProperty(row, field, {
|
|
276
|
+
value, enumerable: true, writable: true, configurable: true
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function fillRun(partition, field, run, transform, positions) {
|
|
281
|
+
const [start, end] = run;
|
|
282
|
+
const length = end - start + 1;
|
|
283
|
+
if (transform.maxGap !== undefined && length > transform.maxGap) return;
|
|
284
|
+
if (transform.method === "constant") {
|
|
285
|
+
for (let index = start; index <= end; index += 1) {
|
|
286
|
+
setField(partition[index].row, field, transform.value);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const left = start - 1;
|
|
291
|
+
const right = end + 1;
|
|
292
|
+
const hasLeft = left >= 0;
|
|
293
|
+
const hasRight = right < partition.length;
|
|
294
|
+
const usable = transform.method === "forward" ? hasLeft
|
|
295
|
+
: transform.method === "backward" ? hasRight
|
|
296
|
+
: hasLeft && hasRight;
|
|
297
|
+
if (!usable) {
|
|
298
|
+
if (transform.edges === "error") {
|
|
299
|
+
throw new RangeError(`Impute ${transform.method} cannot fill an edge run in field "${field}".`);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (transform.method === "forward" || transform.method === "backward") {
|
|
304
|
+
const anchor = transform.method === "forward" ? left : right;
|
|
305
|
+
const value = partition[anchor].row[field];
|
|
306
|
+
for (let index = start; index <= end; index += 1) {
|
|
307
|
+
setField(partition[index].row, field, value);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const leftValue = partition[left].row[field];
|
|
312
|
+
const rightValue = partition[right].row[field];
|
|
313
|
+
for (let index = start; index <= end; index += 1) {
|
|
314
|
+
const proportion = inverseLerp(positions[index], positions[left], positions[right]);
|
|
315
|
+
setField(
|
|
316
|
+
partition[index].row,
|
|
317
|
+
field,
|
|
318
|
+
interpolateNumber(leftValue, rightValue, proportion)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function deriveImputedRows(rows, transform) {
|
|
324
|
+
validateImputeTransform(transform);
|
|
325
|
+
validateRows(rows, transform);
|
|
326
|
+
const entries = rows.map((row, index) => ({ index, row: { ...row } }));
|
|
327
|
+
validateFieldTypes(entries, transform);
|
|
328
|
+
const groups = partitions(entries, transform);
|
|
329
|
+
for (const partition of groups) {
|
|
330
|
+
validateSortFieldTypes(partition, transform);
|
|
331
|
+
partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
|
|
332
|
+
const positions = linearPositions(partition, transform);
|
|
333
|
+
for (const field of transform.fields) {
|
|
334
|
+
for (const run of missingRuns(partition, field)) {
|
|
335
|
+
fillRun(partition, field, run, transform, positions);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return cloneAndFreeze(entries.sort((a, b) => a.index - b.index).map(entry => entry.row));
|
|
340
|
+
}
|
package/src/grammar/interval.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
3
3
|
import { requireFiniteResult, stableDecimal } from "./numeric.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
confidenceCriticalValue,
|
|
6
|
+
normalizeConfidenceInterval
|
|
7
|
+
} from "./statistics/confidenceInterval.js";
|
|
5
8
|
|
|
6
9
|
const CENTER_VALUES = ["mean", "median"];
|
|
7
10
|
const EXTENT_VALUES = ["stderr", "stdev", "ci", "iqr"];
|
|
@@ -11,6 +14,7 @@ const TRANSFORM_KEYS = [
|
|
|
11
14
|
"groupBy",
|
|
12
15
|
"center",
|
|
13
16
|
"extent",
|
|
17
|
+
"method",
|
|
14
18
|
"level",
|
|
15
19
|
"as"
|
|
16
20
|
];
|
|
@@ -84,13 +88,12 @@ export function validateIntervalTransform(transform) {
|
|
|
84
88
|
throw new Error("Median intervals require iqr, and iqr requires median.");
|
|
85
89
|
}
|
|
86
90
|
if (transform.extent === "ci") {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
91
|
+
normalizeConfidenceInterval(transform, {
|
|
92
|
+
defaultMethod: "student-t",
|
|
93
|
+
label: "Interval CI"
|
|
94
|
+
});
|
|
95
|
+
} else if (transform.method !== undefined) {
|
|
96
|
+
throw new Error("Interval method is supported only for ci extent.");
|
|
94
97
|
} else if (transform.level !== undefined) {
|
|
95
98
|
throw new Error("Interval level is supported only for ci extent.");
|
|
96
99
|
}
|
|
@@ -111,16 +114,27 @@ function normalizeGrouping(groupBy) {
|
|
|
111
114
|
export function normalizeIntervalParameters({
|
|
112
115
|
center = "mean",
|
|
113
116
|
extent = "ci",
|
|
117
|
+
method,
|
|
114
118
|
level
|
|
115
119
|
} = {}) {
|
|
116
|
-
const
|
|
120
|
+
const confidence = extent === "ci"
|
|
121
|
+
? normalizeConfidenceInterval({ method, level }, {
|
|
122
|
+
defaultMethod: "student-t",
|
|
123
|
+
label: "Interval CI"
|
|
124
|
+
})
|
|
125
|
+
: undefined;
|
|
117
126
|
const candidate = {
|
|
118
127
|
type: "interval",
|
|
119
128
|
field: "__interval_input",
|
|
120
129
|
groupBy: [],
|
|
121
130
|
center,
|
|
122
131
|
extent,
|
|
123
|
-
...(
|
|
132
|
+
...(confidence === undefined
|
|
133
|
+
? {
|
|
134
|
+
...(method === undefined ? {} : { method }),
|
|
135
|
+
...(level === undefined ? {} : { level })
|
|
136
|
+
}
|
|
137
|
+
: confidence),
|
|
124
138
|
as: {
|
|
125
139
|
center: "__interval_center",
|
|
126
140
|
lower: "__interval_lower",
|
|
@@ -131,18 +145,16 @@ export function normalizeIntervalParameters({
|
|
|
131
145
|
return cloneAndFreeze({
|
|
132
146
|
center,
|
|
133
147
|
extent,
|
|
134
|
-
...(
|
|
148
|
+
...(confidence === undefined ? {} : confidence)
|
|
135
149
|
});
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
export function studentTCritical(degreesOfFreedom, level) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return studentTCriticalValue(level, degreesOfFreedom);
|
|
153
|
+
return confidenceCriticalValue({
|
|
154
|
+
method: "student-t",
|
|
155
|
+
level,
|
|
156
|
+
degreesOfFreedom
|
|
157
|
+
});
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
function isMissing(value) {
|
|
@@ -170,7 +182,11 @@ function deriveGroup(values, transform) {
|
|
|
170
182
|
? aggregateScalarValues(values, "stdev")
|
|
171
183
|
: transform.extent === "stderr"
|
|
172
184
|
? aggregateScalarValues(values, "stderr")
|
|
173
|
-
:
|
|
185
|
+
: confidenceCriticalValue({
|
|
186
|
+
method: transform.method,
|
|
187
|
+
level: transform.level,
|
|
188
|
+
degreesOfFreedom: values.length - 1
|
|
189
|
+
}) *
|
|
174
190
|
aggregateScalarValues(values, "stderr");
|
|
175
191
|
return {
|
|
176
192
|
center,
|
|
@@ -232,12 +248,13 @@ export function normalizeIntervalTransform({
|
|
|
232
248
|
groupBy,
|
|
233
249
|
center,
|
|
234
250
|
extent,
|
|
251
|
+
method,
|
|
235
252
|
level,
|
|
236
253
|
as
|
|
237
254
|
}) {
|
|
238
255
|
nonEmptyString(field, "Interval field");
|
|
239
256
|
const grouping = normalizeGrouping(groupBy);
|
|
240
|
-
const parameters = normalizeIntervalParameters({ center, extent, level });
|
|
257
|
+
const parameters = normalizeIntervalParameters({ center, extent, method, level });
|
|
241
258
|
const transform = {
|
|
242
259
|
type: "interval",
|
|
243
260
|
field,
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
findHistogramBinIndex,
|
|
17
17
|
resolveHistogramBins
|
|
18
18
|
} from "./histogram.js";
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
validatePathSeriesAppearance
|
|
21
|
+
} from "./pathSeries.js";
|
|
21
22
|
|
|
22
23
|
function requireLineEncoding(layer) {
|
|
23
24
|
if (layer?.mark?.type !== "line") {
|
|
@@ -70,32 +71,6 @@ function requireLineEncoding(layer) {
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function readSeriesFields(rows, layer) {
|
|
74
|
-
const fields = [];
|
|
75
|
-
const values = new Map();
|
|
76
|
-
|
|
77
|
-
for (const channel of SERIES_CHANNELS) {
|
|
78
|
-
const encoding = layer.encoding?.[channel];
|
|
79
|
-
|
|
80
|
-
if (encoding?.field === undefined) continue;
|
|
81
|
-
const categorical = channel === "color"
|
|
82
|
-
? ["nominal", "ordinal"].includes(encoding.fieldType)
|
|
83
|
-
: encoding.fieldType === "nominal";
|
|
84
|
-
if (!categorical) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Line ${channel} encoding on mark "${layer.id}" must be categorical.`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!values.has(encoding.field)) {
|
|
91
|
-
fields.push(encoding.field);
|
|
92
|
-
values.set(encoding.field, readNominalField(rows, encoding.field));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { fields, values };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
74
|
function groupKey(values) {
|
|
100
75
|
return JSON.stringify(values);
|
|
101
76
|
}
|
|
@@ -148,14 +123,14 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
|
|
|
148
123
|
validateAggregateFieldValues(rows, y.field, y.fieldType);
|
|
149
124
|
}
|
|
150
125
|
const xValues = x.fieldType === "temporal"
|
|
151
|
-
? readTemporalField(rows, x.field)
|
|
126
|
+
? readTemporalField(rows, x.field, x.temporalUnit)
|
|
152
127
|
: readQuantitativeField(rows, x.field);
|
|
153
128
|
const yValues = isAggregate
|
|
154
129
|
? undefined
|
|
155
130
|
: y.fieldType === "temporal"
|
|
156
|
-
? readTemporalField(rows, y.field)
|
|
131
|
+
? readTemporalField(rows, y.field, y.temporalUnit)
|
|
157
132
|
: readQuantitativeField(rows, y.field);
|
|
158
|
-
const seriesFields =
|
|
133
|
+
const seriesFields = validatePathSeriesAppearance(rows, layer);
|
|
159
134
|
const binBoundaries = binnedAggregate
|
|
160
135
|
? options.xBinBoundaries ?? resolveLineBins(rows, layer).boundaries
|
|
161
136
|
: undefined;
|
|
@@ -170,10 +145,10 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
|
|
|
170
145
|
const orderValues = readQuantitativeField(rows, pathOrder.field);
|
|
171
146
|
const groups = new Map();
|
|
172
147
|
for (let index = 0; index < rows.length; index += 1) {
|
|
173
|
-
const dimensions = seriesFields.
|
|
174
|
-
field =>
|
|
148
|
+
const dimensions = seriesFields.map(
|
|
149
|
+
field => rows[index][field]
|
|
175
150
|
);
|
|
176
|
-
const series = groupedSeries(groups, seriesFields
|
|
151
|
+
const series = groupedSeries(groups, seriesFields, dimensions);
|
|
177
152
|
series.values.push({ x: xValues[index], y: yValues[index] });
|
|
178
153
|
series.orderValues ??= [];
|
|
179
154
|
series.orderValues.push(orderValues[index]);
|
|
@@ -197,10 +172,10 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
|
|
|
197
172
|
if (directRows) {
|
|
198
173
|
const groups = new Map();
|
|
199
174
|
for (let index = 0; index < rows.length; index += 1) {
|
|
200
|
-
const dimensions = seriesFields.
|
|
201
|
-
field =>
|
|
175
|
+
const dimensions = seriesFields.map(
|
|
176
|
+
field => rows[index][field]
|
|
202
177
|
);
|
|
203
|
-
const series = groupedSeries(groups, seriesFields
|
|
178
|
+
const series = groupedSeries(groups, seriesFields, dimensions);
|
|
204
179
|
series.values.push({ x: xValues[index], y: yValues[index] });
|
|
205
180
|
}
|
|
206
181
|
const orderBy = directTemporal && y.fieldType === "temporal" ? "y" : "x";
|
|
@@ -220,8 +195,8 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
|
|
|
220
195
|
const aggregateGroups = new Map();
|
|
221
196
|
|
|
222
197
|
for (let index = 0; index < rows.length; index += 1) {
|
|
223
|
-
const dimensions = seriesFields.
|
|
224
|
-
field =>
|
|
198
|
+
const dimensions = seriesFields.map(
|
|
199
|
+
field => rows[index][field]
|
|
225
200
|
);
|
|
226
201
|
const binIndex = binnedAggregate
|
|
227
202
|
? findHistogramBinIndex(xValues[index], binBoundaries)
|
|
@@ -255,7 +230,7 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
|
|
|
255
230
|
if (value === undefined) continue;
|
|
256
231
|
const series = groupedSeries(
|
|
257
232
|
seriesGroups,
|
|
258
|
-
seriesFields
|
|
233
|
+
seriesFields,
|
|
259
234
|
group.dimensions
|
|
260
235
|
);
|
|
261
236
|
|
|
@@ -315,7 +290,7 @@ function readThetaValues(rows, encoding) {
|
|
|
315
290
|
return readNominalField(rows, encoding.field);
|
|
316
291
|
}
|
|
317
292
|
return encoding.fieldType === "temporal"
|
|
318
|
-
? readTemporalField(rows, encoding.field)
|
|
293
|
+
? readTemporalField(rows, encoding.field, encoding.temporalUnit)
|
|
319
294
|
: readQuantitativeField(rows, encoding.field);
|
|
320
295
|
}
|
|
321
296
|
|
|
@@ -341,14 +316,14 @@ export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
|
|
|
341
316
|
const thetaValues = readThetaValues(rows, theta);
|
|
342
317
|
const radiusValues = readQuantitativeField(rows, radius.field);
|
|
343
318
|
const sortValues = thetaOrder(thetaValues, theta.fieldType, thetaDomain);
|
|
344
|
-
const seriesFields =
|
|
319
|
+
const seriesFields = validatePathSeriesAppearance(rows, layer);
|
|
345
320
|
const groups = new Map();
|
|
346
321
|
|
|
347
322
|
for (let index = 0; index < rows.length; index += 1) {
|
|
348
|
-
const dimensions = seriesFields.
|
|
349
|
-
field =>
|
|
323
|
+
const dimensions = seriesFields.map(
|
|
324
|
+
field => rows[index][field]
|
|
350
325
|
);
|
|
351
|
-
const series = groupedSeries(groups, seriesFields
|
|
326
|
+
const series = groupedSeries(groups, seriesFields, dimensions);
|
|
352
327
|
series.values.push({
|
|
353
328
|
theta: thetaValues[index],
|
|
354
329
|
radius: radiusValues[index],
|
|
@@ -384,21 +359,3 @@ export function deriveLineSeries(rows, layer, options) {
|
|
|
384
359
|
? derivePolarLineSeries(rows, layer, options)
|
|
385
360
|
: deriveCartesianLineSeries(rows, layer, options);
|
|
386
361
|
}
|
|
387
|
-
|
|
388
|
-
export function deriveLineSeriesFieldValues(rows, layer, derived, field) {
|
|
389
|
-
const values = readQuantitativeField(rows, field);
|
|
390
|
-
if (values.some(value => value < 0)) {
|
|
391
|
-
throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
|
|
392
|
-
}
|
|
393
|
-
return cloneAndFreeze(derived.series.map(series => {
|
|
394
|
-
const matching = values.filter((_, index) => Object.entries(series.key)
|
|
395
|
-
.every(([key, value]) => rows[index]?.[key] === value));
|
|
396
|
-
const unique = [...new Set(matching)];
|
|
397
|
-
if (unique.length !== 1) {
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Line strokeWidth field "${field}" must have one value within each series.`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return unique[0];
|
|
403
|
-
}));
|
|
404
|
-
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { normalizeMarkSelector } from "./markSelection.js";
|
|
3
3
|
|
|
4
|
-
const TRANSFORM_KEYS = Object.freeze([
|
|
4
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
5
|
+
"type", "target", "selector", "selectors"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function markFilterSelectors(transform) {
|
|
9
|
+
const hasSelector = Object.hasOwn(transform, "selector");
|
|
10
|
+
const hasSelectors = Object.hasOwn(transform, "selectors");
|
|
11
|
+
if (hasSelector === hasSelectors) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Mark filter transform requires exactly one of selector or selectors."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const selectors = hasSelector ? [transform.selector] : transform.selectors;
|
|
17
|
+
if (!Array.isArray(selectors) || selectors.length === 0) {
|
|
18
|
+
throw new TypeError("Mark filter selectors must be a non-empty array.");
|
|
19
|
+
}
|
|
20
|
+
return cloneAndFreeze(selectors.map(normalizeMarkSelector));
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
export function validateMarkFilterTransform(transform) {
|
|
7
24
|
if (!isPlainObject(transform)) {
|
|
@@ -19,14 +36,15 @@ export function validateMarkFilterTransform(transform) {
|
|
|
19
36
|
if (typeof transform.target !== "string" || transform.target.length === 0) {
|
|
20
37
|
throw new TypeError("Mark filter target must be a non-empty string.");
|
|
21
38
|
}
|
|
22
|
-
|
|
39
|
+
markFilterSelectors(transform);
|
|
23
40
|
}
|
|
24
41
|
|
|
25
|
-
export function normalizeMarkFilterTransform(target,
|
|
42
|
+
export function normalizeMarkFilterTransform(target, selectors) {
|
|
26
43
|
const transform = {
|
|
27
44
|
type: "markFilter",
|
|
28
45
|
target,
|
|
29
|
-
|
|
46
|
+
selectors: (Array.isArray(selectors) ? selectors : [selectors])
|
|
47
|
+
.map(normalizeMarkSelector)
|
|
30
48
|
};
|
|
31
49
|
validateMarkFilterTransform(transform);
|
|
32
50
|
return cloneAndFreeze(transform);
|