ggaction 0.0.11 → 0.0.13
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 +80 -0
- package/README.md +3 -2
- 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 +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- 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 +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.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/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- 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 +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +79 -24
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- 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 +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +153 -11
- package/src/actions/errorBars/edit.js +365 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- 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 +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- 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 +44 -23
- 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 +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- 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 +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- 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 +138 -14
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +60 -4
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionCompatibility.js +1 -1
- 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 +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +9 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +86 -23
- package/src/layout/guideCollisions.js +12 -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 +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { aggregateRows } from "./aggregate.js";
|
|
2
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "./bars/policy.js";
|
|
3
|
+
import { stableFiniteSum } from "./numeric.js";
|
|
4
|
+
|
|
5
|
+
const CONTENTS = new Set(["category", "value", "share"]);
|
|
6
|
+
const NORMALIZATIONS = new Set(["source", "category"]);
|
|
7
|
+
|
|
8
|
+
export function validateMarkLabelContent(content) {
|
|
9
|
+
if (!CONTENTS.has(content)) throw new Error(`Unsupported text content "${content}".`);
|
|
10
|
+
return content;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateMarkLabelNormalization(normalizeBy) {
|
|
14
|
+
if (!NORMALIZATIONS.has(normalizeBy)) throw new Error(`Unsupported text normalizeBy "${normalizeBy}".`);
|
|
15
|
+
return normalizeBy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMarkLabelContent(source, { content, normalizeBy }) {
|
|
19
|
+
validateMarkLabelContent(content);
|
|
20
|
+
if (!["bar", "arc"].includes(source?.mark?.type)) {
|
|
21
|
+
throw new Error("Semantic text content requires a Bar or Arc source; use field or value for other marks.");
|
|
22
|
+
}
|
|
23
|
+
if (content !== "share" && normalizeBy !== undefined) {
|
|
24
|
+
throw new Error("Text normalizeBy is only supported with share content.");
|
|
25
|
+
}
|
|
26
|
+
const normalization = content === "share" ? normalizeBy ?? "source" : undefined;
|
|
27
|
+
if (normalization !== undefined) validateMarkLabelNormalization(normalization);
|
|
28
|
+
if (normalization === "category" && source.mark.type !== "bar") {
|
|
29
|
+
throw new Error("Category share normalization requires a Bar source.");
|
|
30
|
+
}
|
|
31
|
+
if (source.mark.type === "bar") {
|
|
32
|
+
const grain = resolveBarGrain(source);
|
|
33
|
+
if (grain === BAR_GRAINS.ranged) {
|
|
34
|
+
throw new Error("Ranged Bar text requires an explicit field or value.");
|
|
35
|
+
}
|
|
36
|
+
if (content === "category" && grain === BAR_GRAINS.histogram) {
|
|
37
|
+
throw new Error("Histogram categories are intervals; use an explicit text field or value.");
|
|
38
|
+
}
|
|
39
|
+
} else if (content === "category" && source.encoding?.theta?.fieldType === "quantitative") {
|
|
40
|
+
throw new Error("Category text requires categorical Arc theta.");
|
|
41
|
+
}
|
|
42
|
+
return { content, ...(normalization === undefined ? {} : { normalizeBy: normalization }) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function itemValue(source, item) {
|
|
46
|
+
if (source.mark.type === "bar") {
|
|
47
|
+
if (resolveBarGrain(source) === BAR_GRAINS.histogram) return item.members.length;
|
|
48
|
+
const measure = source.encoding[resolveBarChannels(source).measure];
|
|
49
|
+
return measure.aggregate === "count" ? item.members.length
|
|
50
|
+
: aggregateRows(item.members, measure.field, measure.aggregate);
|
|
51
|
+
}
|
|
52
|
+
if (source.encoding?.radius !== undefined) return item.channels.radius;
|
|
53
|
+
const theta = source.encoding.theta;
|
|
54
|
+
if (theta.fieldType === "quantitative") return item.channels.theta;
|
|
55
|
+
return theta.aggregate === "count" ? item.members.length
|
|
56
|
+
: aggregateRows(item.members, theta.weight, "sum");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function categoryValue(source, item) {
|
|
60
|
+
return item.channels[source.mark.type === "arc" ? "theta" : resolveBarChannels(source).category];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function categoryKey(source, item) {
|
|
64
|
+
return JSON.stringify(resolveBarGrain(source) === BAR_GRAINS.histogram
|
|
65
|
+
? [item.channels.x, item.channels.x2]
|
|
66
|
+
: [categoryValue(source, item)]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveMarkLabelValues(source, items, options) {
|
|
70
|
+
const config = normalizeMarkLabelContent(source, options);
|
|
71
|
+
if (config.content === "category") return items.map(item => categoryValue(source, item));
|
|
72
|
+
const values = items.map(item => itemValue(source, item));
|
|
73
|
+
if (config.content === "value" || items.length === 0) return values;
|
|
74
|
+
if (values.some(value => !Number.isFinite(value) || value < 0)) {
|
|
75
|
+
throw new Error("Share labels require finite non-negative source values.");
|
|
76
|
+
}
|
|
77
|
+
const groups = new Map();
|
|
78
|
+
items.forEach((item, index) => {
|
|
79
|
+
const key = config.normalizeBy === "source" ? "source" : categoryKey(source, item);
|
|
80
|
+
const group = groups.get(key) ?? [];
|
|
81
|
+
group.push(index);
|
|
82
|
+
groups.set(key, group);
|
|
83
|
+
});
|
|
84
|
+
const shares = [];
|
|
85
|
+
for (const indices of groups.values()) {
|
|
86
|
+
const maximum = indices.reduce((max, index) => Math.max(max, values[index]), 0);
|
|
87
|
+
if (maximum === 0) throw new Error("Share label denominator must be positive.");
|
|
88
|
+
// Ratios remain defined even when the unscaled sum would overflow.
|
|
89
|
+
const total = stableFiniteSum(indices.map(index => values[index] / maximum), "Share label denominator");
|
|
90
|
+
for (const index of indices) shares[index] = (values[index] / maximum) / total;
|
|
91
|
+
}
|
|
92
|
+
return shares;
|
|
93
|
+
}
|
|
@@ -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,91 @@
|
|
|
1
|
+
import { validateNonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { readNominalField, readQuantitativeField } from "./scales/fields.js";
|
|
3
|
+
|
|
4
|
+
export function normalizeGroupFields(value) {
|
|
5
|
+
const fields = Array.isArray(value) ? [...value] : [value];
|
|
6
|
+
for (const field of fields) validateNonEmptyString(field, "Path group field");
|
|
7
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length) {
|
|
8
|
+
throw new Error("Path group fields must be non-empty and unique.");
|
|
9
|
+
}
|
|
10
|
+
return fields;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function pathGroupFields(layer) {
|
|
14
|
+
const group = layer.encoding?.group;
|
|
15
|
+
if (group !== undefined) {
|
|
16
|
+
if (group.fieldType !== "nominal") {
|
|
17
|
+
throw new Error(`Path group on mark "${layer.id}" must be nominal.`);
|
|
18
|
+
}
|
|
19
|
+
if (Object.hasOwn(group, "field") && Object.hasOwn(group, "fields")) {
|
|
20
|
+
throw new Error("Path group requires exactly one of field or fields.");
|
|
21
|
+
}
|
|
22
|
+
return normalizeGroupFields(group.fields ?? group.field);
|
|
23
|
+
}
|
|
24
|
+
if (layer.mark?.type !== "line") return [];
|
|
25
|
+
const color = layer.encoding?.color?.field;
|
|
26
|
+
const dash = layer.encoding?.strokeDash?.field;
|
|
27
|
+
if (color !== undefined && dash !== undefined && color !== dash) {
|
|
28
|
+
throw new Error("Line color field must match strokeDash field unless encodeGroup defines the series.");
|
|
29
|
+
}
|
|
30
|
+
return color === undefined && dash === undefined ? [] : [color ?? dash];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readPathSeriesFields(rows, layer) {
|
|
34
|
+
const fields = pathGroupFields(layer);
|
|
35
|
+
for (const field of fields) readNominalField(rows, field);
|
|
36
|
+
return fields;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function seriesValueIndex(rows, fields, field, channel) {
|
|
40
|
+
const quantitative = channel === "strokeWidth" || channel === "opacity";
|
|
41
|
+
const values = quantitative
|
|
42
|
+
? readQuantitativeField(rows, field)
|
|
43
|
+
: readNominalField(rows, field);
|
|
44
|
+
const result = new Map();
|
|
45
|
+
rows.forEach((row, index) => {
|
|
46
|
+
const key = JSON.stringify(fields.map(field => row[field]));
|
|
47
|
+
const value = values[index];
|
|
48
|
+
if (channel === "strokeWidth" && value < 0) {
|
|
49
|
+
throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
|
|
50
|
+
}
|
|
51
|
+
if (result.has(key) && result.get(key) !== value) {
|
|
52
|
+
throw new Error(`Path ${channel} field "${field}" must have one value within each series.`);
|
|
53
|
+
}
|
|
54
|
+
result.set(key, value);
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function derivePathSeriesFieldValues(rows, series, field, channel) {
|
|
60
|
+
const fields = Object.keys(series[0]?.key ?? {});
|
|
61
|
+
const values = seriesValueIndex(rows, fields, field, channel);
|
|
62
|
+
return series.map(item => values.get(JSON.stringify(Object.values(item.key))));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validatePathSeriesAppearance(rows, layer) {
|
|
66
|
+
if (!["line", "area"].includes(layer.mark?.type) ||
|
|
67
|
+
layer.encoding?.parallel !== undefined) return;
|
|
68
|
+
const grouping = readPathSeriesFields(rows, layer);
|
|
69
|
+
const channels = layer.mark.type === "line"
|
|
70
|
+
? ["color", "strokeDash", "strokeWidth", "opacity"] : ["color"];
|
|
71
|
+
for (const channel of channels) {
|
|
72
|
+
const encoding = layer.encoding?.[channel];
|
|
73
|
+
if (encoding?.field === undefined) continue;
|
|
74
|
+
const allowed = channel === "color" ? ["nominal", "ordinal"]
|
|
75
|
+
: channel === "strokeDash" ? ["nominal"] : ["quantitative"];
|
|
76
|
+
if (!allowed.includes(encoding.fieldType)) {
|
|
77
|
+
throw new Error(`Path ${channel} encoding has an unsupported field type.`);
|
|
78
|
+
}
|
|
79
|
+
seriesValueIndex(rows, grouping, encoding.field, channel);
|
|
80
|
+
}
|
|
81
|
+
return grouping;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readSeriesIdentity(rows, layer) {
|
|
85
|
+
const group = layer.encoding?.group;
|
|
86
|
+
const fields = group === undefined ? [] : normalizeGroupFields(group.fields ?? group.field);
|
|
87
|
+
for (const field of fields) readNominalField(rows, field);
|
|
88
|
+
const values = rows.map(row => fields.length === 0 ? undefined : fields.length === 1
|
|
89
|
+
? row[fields[0]] : JSON.stringify(fields.map(field => row[field])));
|
|
90
|
+
return { fields, values, domain: [...new Set(values)] };
|
|
91
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateNonEmptyString } from "../core/validation.js";
|
|
3
|
+
import { resolveJitterSlotWidth } from "./jitter.js";
|
|
4
|
+
|
|
5
|
+
export const POINT_PACKING_ALGORITHM = "point-pack-greedy-v1";
|
|
6
|
+
|
|
7
|
+
export function normalizePointPackingPolicy(options = {}) {
|
|
8
|
+
if (!isPlainObject(options)) {
|
|
9
|
+
throw new TypeError("Point packing options must be a plain object.");
|
|
10
|
+
}
|
|
11
|
+
if (!["x", "y"].includes(options.channel)) {
|
|
12
|
+
throw new Error('Point packing channel must be "x" or "y".');
|
|
13
|
+
}
|
|
14
|
+
let maxOffset;
|
|
15
|
+
if (options.maxOffset !== undefined) {
|
|
16
|
+
if (!isPlainObject(options.maxOffset)) {
|
|
17
|
+
throw new TypeError("Point packing maxOffset must be a plain object.");
|
|
18
|
+
}
|
|
19
|
+
const keys = Object.keys(options.maxOffset);
|
|
20
|
+
if (keys.length !== 1 || !["pixels", "band"].includes(keys[0])) {
|
|
21
|
+
throw new Error("Point packing maxOffset requires exactly one of pixels or band.");
|
|
22
|
+
}
|
|
23
|
+
const value = options.maxOffset[keys[0]];
|
|
24
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
25
|
+
throw new RangeError(`Point packing ${keys[0]} must be positive and finite.`);
|
|
26
|
+
}
|
|
27
|
+
if (keys[0] === "band" && value > 0.5) {
|
|
28
|
+
throw new RangeError("Point packing band must be at most 0.5.");
|
|
29
|
+
}
|
|
30
|
+
maxOffset = { [keys[0]]: value };
|
|
31
|
+
}
|
|
32
|
+
const padding = options.padding ?? 1;
|
|
33
|
+
if (!Number.isFinite(padding) || padding < 0) {
|
|
34
|
+
throw new RangeError("Point packing padding must be non-negative and finite.");
|
|
35
|
+
}
|
|
36
|
+
const key = options.key === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: validateNonEmptyString(options.key, "Point packing key");
|
|
39
|
+
const overflow = options.overflow ?? "error";
|
|
40
|
+
if (!["error", "overlap"].includes(overflow)) {
|
|
41
|
+
throw new Error('Point packing overflow must be "error" or "overlap".');
|
|
42
|
+
}
|
|
43
|
+
return cloneAndFreeze({
|
|
44
|
+
algorithm: POINT_PACKING_ALGORITHM,
|
|
45
|
+
channel: options.channel,
|
|
46
|
+
...(maxOffset === undefined ? {} : { maxOffset }),
|
|
47
|
+
padding,
|
|
48
|
+
...(key === undefined ? {} : { key }),
|
|
49
|
+
overflow
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function offsetLimit(policy, slotWidth) {
|
|
54
|
+
if (policy.maxOffset?.pixels !== undefined) return policy.maxOffset.pixels;
|
|
55
|
+
if (policy.maxOffset?.band !== undefined) return slotWidth * policy.maxOffset.band;
|
|
56
|
+
return slotWidth / 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function candidateOffsets(entry, placed, minimum, maximum, padding) {
|
|
60
|
+
const candidates = [0, minimum - entry.base, maximum - entry.base];
|
|
61
|
+
for (const other of placed) {
|
|
62
|
+
if (Math.abs(entry.fixed - other.fixed) >=
|
|
63
|
+
entry.fixedHalfExtent + other.fixedHalfExtent + padding) continue;
|
|
64
|
+
const distance = entry.halfExtent + other.halfExtent + padding;
|
|
65
|
+
candidates.push(other.final - entry.base - distance, other.final - entry.base + distance);
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(candidates
|
|
68
|
+
.map(value => Math.max(minimum - entry.base, Math.min(maximum - entry.base, value)))
|
|
69
|
+
.map(value => Object.is(value, -0) ? 0 : value))]
|
|
70
|
+
.sort((left, right) => Math.abs(left) - Math.abs(right) || left - right);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collisions(entry, final, placed, padding) {
|
|
74
|
+
return placed.filter(other =>
|
|
75
|
+
Math.abs(entry.fixed - other.fixed) <
|
|
76
|
+
entry.fixedHalfExtent + other.fixedHalfExtent + padding &&
|
|
77
|
+
Math.abs(final - other.final) < entry.halfExtent + other.halfExtent + padding
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolvePointPacking({
|
|
82
|
+
target,
|
|
83
|
+
policy,
|
|
84
|
+
scale,
|
|
85
|
+
entries,
|
|
86
|
+
plotMinimum,
|
|
87
|
+
plotMaximum
|
|
88
|
+
}) {
|
|
89
|
+
validateNonEmptyString(target, "Point packing target");
|
|
90
|
+
if (!Array.isArray(entries)) throw new TypeError("Point packing entries must be an array.");
|
|
91
|
+
if (!Number.isFinite(plotMinimum) || !Number.isFinite(plotMaximum) || plotMaximum < plotMinimum) {
|
|
92
|
+
throw new RangeError("Point packing plot bounds are invalid.");
|
|
93
|
+
}
|
|
94
|
+
const slotWidth = resolveJitterSlotWidth(scale);
|
|
95
|
+
if (slotWidth === undefined) {
|
|
96
|
+
throw new Error("Point packing requires a categorical position scale with a positive slot.");
|
|
97
|
+
}
|
|
98
|
+
const maximumOffset = offsetLimit(policy, slotWidth);
|
|
99
|
+
const seenIndices = new Set();
|
|
100
|
+
const seenIdentities = new Set();
|
|
101
|
+
const ordered = entries.map((entry, offset) => {
|
|
102
|
+
for (const field of ["index", "base", "fixed", "halfExtent", "fixedHalfExtent"]) {
|
|
103
|
+
if (!Number.isFinite(entry[field])) {
|
|
104
|
+
throw new TypeError(`Point packing entry ${offset} ${field} must be finite.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!Number.isInteger(entry.index) || entry.index < 0 ||
|
|
108
|
+
entry.halfExtent < 0 || entry.fixedHalfExtent < 0) {
|
|
109
|
+
throw new RangeError(`Point packing entry ${offset} geometry is invalid.`);
|
|
110
|
+
}
|
|
111
|
+
validateNonEmptyString(entry.identity, `Point packing entry ${offset} identity`);
|
|
112
|
+
if (seenIndices.has(entry.index)) {
|
|
113
|
+
throw new Error(`Point packing entry index ${entry.index} must be unique.`);
|
|
114
|
+
}
|
|
115
|
+
if (seenIdentities.has(entry.identity)) {
|
|
116
|
+
throw new Error(`Point packing entry identity "${entry.identity}" must be unique.`);
|
|
117
|
+
}
|
|
118
|
+
seenIndices.add(entry.index);
|
|
119
|
+
seenIdentities.add(entry.identity);
|
|
120
|
+
return { ...entry };
|
|
121
|
+
}).sort((left, right) =>
|
|
122
|
+
left.fixed - right.fixed ||
|
|
123
|
+
(left.identity < right.identity ? -1 : left.identity > right.identity ? 1 : 0)
|
|
124
|
+
);
|
|
125
|
+
const placed = [];
|
|
126
|
+
for (const entry of ordered) {
|
|
127
|
+
const minimum = Math.max(
|
|
128
|
+
plotMinimum + entry.halfExtent,
|
|
129
|
+
entry.base - slotWidth / 2 + entry.halfExtent,
|
|
130
|
+
entry.base - maximumOffset
|
|
131
|
+
);
|
|
132
|
+
const maximum = Math.min(
|
|
133
|
+
plotMaximum - entry.halfExtent,
|
|
134
|
+
entry.base + slotWidth / 2 - entry.halfExtent,
|
|
135
|
+
entry.base + maximumOffset
|
|
136
|
+
);
|
|
137
|
+
if (minimum > maximum) {
|
|
138
|
+
if (policy.overflow === "error") {
|
|
139
|
+
throw new RangeError(`Point packing for "${target}" has no available category-slot position.`);
|
|
140
|
+
}
|
|
141
|
+
placed.push({ ...entry, final: entry.base, offset: 0, unresolved: true, collisionCount: 0 });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const candidates = candidateOffsets(entry, placed, minimum, maximum, policy.padding)
|
|
145
|
+
.map(offset => ({
|
|
146
|
+
offset,
|
|
147
|
+
final: entry.base + offset,
|
|
148
|
+
collisions: collisions(entry, entry.base + offset, placed, policy.padding)
|
|
149
|
+
}));
|
|
150
|
+
const resolved = candidates.find(candidate => candidate.collisions.length === 0);
|
|
151
|
+
if (resolved !== undefined) {
|
|
152
|
+
placed.push({ ...entry, final: resolved.final, offset: resolved.offset,
|
|
153
|
+
unresolved: false, collisionCount: 0 });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (policy.overflow === "error") {
|
|
157
|
+
throw new RangeError(`Point packing for "${target}" cannot avoid glyph overlap.`);
|
|
158
|
+
}
|
|
159
|
+
const best = candidates.sort((left, right) =>
|
|
160
|
+
left.collisions.length - right.collisions.length ||
|
|
161
|
+
Math.abs(left.offset) - Math.abs(right.offset) || left.offset - right.offset
|
|
162
|
+
)[0];
|
|
163
|
+
placed.push({ ...entry, final: best.final, offset: best.offset,
|
|
164
|
+
unresolved: true, collisionCount: best.collisions.length });
|
|
165
|
+
}
|
|
166
|
+
const items = placed.sort((left, right) => left.index - right.index).map(item => ({
|
|
167
|
+
index: item.index,
|
|
168
|
+
identity: item.identity,
|
|
169
|
+
base: item.base,
|
|
170
|
+
final: item.final,
|
|
171
|
+
finalOffset: item.offset,
|
|
172
|
+
unresolved: item.unresolved,
|
|
173
|
+
collisionCount: item.collisionCount
|
|
174
|
+
}));
|
|
175
|
+
return cloneAndFreeze({
|
|
176
|
+
algorithm: POINT_PACKING_ALGORITHM,
|
|
177
|
+
slotWidth,
|
|
178
|
+
maximumOffset,
|
|
179
|
+
itemCount: items.length,
|
|
180
|
+
unresolvedItemCount: items.filter(item => item.unresolved).length,
|
|
181
|
+
items
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -16,7 +16,7 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
|
|
|
16
16
|
y: Object.freeze(["quantitative", "temporal"])
|
|
17
17
|
}),
|
|
18
18
|
arc: Object.freeze({
|
|
19
|
-
theta: Object.freeze(["ordinal", "nominal"]),
|
|
19
|
+
theta: Object.freeze(["quantitative", "ordinal", "nominal"]),
|
|
20
20
|
radius: Object.freeze(["quantitative"])
|
|
21
21
|
}),
|
|
22
22
|
rule: Object.freeze({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isNominalValue, normalizeTemporalValue, validateSemanticFieldType } from "./scales/index.js";
|
|
2
|
+
|
|
3
|
+
export function normalizePositionDatum(value, fieldType, channel, temporalUnit, mark = "Position") {
|
|
4
|
+
validateSemanticFieldType(fieldType);
|
|
5
|
+
if (fieldType === "quantitative") {
|
|
6
|
+
if (!Number.isFinite(value)) throw new TypeError(`${mark} ${channel} datum must be a finite number.`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (fieldType === "temporal") return normalizeTemporalValue(value, `${channel} datum`, 0, temporalUnit);
|
|
10
|
+
if (!isNominalValue(value)) throw new TypeError(`${mark} ${channel} datum must be a nominal value.`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
package/src/grammar/rects.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const RECT_MODES = Object.freeze({
|
|
2
2
|
discrete: "discrete",
|
|
3
|
-
ranged: "ranged"
|
|
3
|
+
ranged: "ranged",
|
|
4
|
+
xSpan: "x-span",
|
|
5
|
+
ySpan: "y-span"
|
|
4
6
|
});
|
|
5
7
|
|
|
6
8
|
function categorical(encoding) {
|
|
@@ -22,5 +24,11 @@ export function resolveRectMode(layer) {
|
|
|
22
24
|
return RECT_MODES.discrete;
|
|
23
25
|
}
|
|
24
26
|
if (ranged(x, x2) && ranged(y, y2)) return RECT_MODES.ranged;
|
|
27
|
+
if (ranged(x, x2) && y === undefined && y2 === undefined) return RECT_MODES.xSpan;
|
|
28
|
+
if (ranged(y, y2) && x === undefined && x2 === undefined) return RECT_MODES.ySpan;
|
|
25
29
|
return undefined;
|
|
26
30
|
}
|
|
31
|
+
|
|
32
|
+
export function rectUsesFields(layer) {
|
|
33
|
+
return ["x", "y", "x2", "y2", "color"].some(channel => Object.hasOwn(layer.encoding?.[channel] ?? {}, "field"));
|
|
34
|
+
}
|
|
@@ -24,6 +24,8 @@ export function deriveRegression(values, {
|
|
|
24
24
|
method,
|
|
25
25
|
degree,
|
|
26
26
|
span,
|
|
27
|
+
confidenceMethod,
|
|
28
|
+
level,
|
|
27
29
|
confidence,
|
|
28
30
|
interval
|
|
29
31
|
} = {}) {
|
|
@@ -39,6 +41,8 @@ export function deriveRegression(values, {
|
|
|
39
41
|
method,
|
|
40
42
|
degree,
|
|
41
43
|
span,
|
|
44
|
+
confidenceMethod,
|
|
45
|
+
level,
|
|
42
46
|
confidence,
|
|
43
47
|
interval
|
|
44
48
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confidenceCriticalValue } from
|
|
2
|
+
"../statistics/confidenceInterval.js";
|
|
2
3
|
|
|
3
4
|
export {
|
|
4
5
|
REGRESSION_LOWER_FIELD,
|
|
@@ -18,5 +19,9 @@ export function studentTCritical(confidence, degreesOfFreedom) {
|
|
|
18
19
|
if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom <= 0) {
|
|
19
20
|
throw new RangeError("Student-t degrees of freedom must be positive.");
|
|
20
21
|
}
|
|
21
|
-
return
|
|
22
|
+
return confidenceCriticalValue({
|
|
23
|
+
method: "student-t",
|
|
24
|
+
level: confidence,
|
|
25
|
+
degreesOfFreedom
|
|
26
|
+
});
|
|
22
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confidenceCriticalValue } from
|
|
2
|
+
"../statistics/confidenceInterval.js";
|
|
2
3
|
import {
|
|
3
4
|
maximumMagnitude,
|
|
4
5
|
requireFiniteResult,
|
|
@@ -56,7 +57,9 @@ function linearPrediction(model, xValue) {
|
|
|
56
57
|
);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function fitLinearGroup(rows, {
|
|
60
|
+
function fitLinearGroup(rows, {
|
|
61
|
+
x, y, group, confidenceMethod, level
|
|
62
|
+
}) {
|
|
60
63
|
const count = rows.length;
|
|
61
64
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
62
65
|
const label = `Regression group "${groupLabel}"`;
|
|
@@ -152,7 +155,11 @@ function fitLinearGroup(rows, { x, y, group, confidence }) {
|
|
|
152
155
|
return Object.assign(model, {
|
|
153
156
|
residualSumSquares,
|
|
154
157
|
residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
|
|
155
|
-
critical:
|
|
158
|
+
critical: confidenceCriticalValue({
|
|
159
|
+
method: confidenceMethod,
|
|
160
|
+
level,
|
|
161
|
+
degreesOfFreedom
|
|
162
|
+
})
|
|
156
163
|
});
|
|
157
164
|
}
|
|
158
165
|
|
|
@@ -230,7 +237,9 @@ function polynomialResponse(design, values, size, scale = 1) {
|
|
|
230
237
|
));
|
|
231
238
|
}
|
|
232
239
|
|
|
233
|
-
function fitPolynomialGroup(rows, {
|
|
240
|
+
function fitPolynomialGroup(rows, {
|
|
241
|
+
x, y, group, confidenceMethod, level, degree
|
|
242
|
+
}) {
|
|
234
243
|
const count = rows.length;
|
|
235
244
|
const parameterCount = degree + 1;
|
|
236
245
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
@@ -310,7 +319,11 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
|
310
319
|
inverse,
|
|
311
320
|
residualSumSquares,
|
|
312
321
|
residualStandardError: Math.sqrt(residualVariance),
|
|
313
|
-
critical:
|
|
322
|
+
critical: confidenceCriticalValue({
|
|
323
|
+
method: confidenceMethod,
|
|
324
|
+
level,
|
|
325
|
+
degreesOfFreedom
|
|
326
|
+
})
|
|
314
327
|
};
|
|
315
328
|
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
316
329
|
if (stableResponse) Object.assign(model, { responseScale, scaledCoefficients });
|
|
@@ -416,7 +429,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
416
429
|
x,
|
|
417
430
|
y,
|
|
418
431
|
group,
|
|
419
|
-
|
|
432
|
+
confidenceMethod: parameters.confidenceMethod,
|
|
433
|
+
level: parameters.level
|
|
420
434
|
});
|
|
421
435
|
}
|
|
422
436
|
if (parameters.method === "polynomial") {
|
|
@@ -424,7 +438,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
424
438
|
x,
|
|
425
439
|
y,
|
|
426
440
|
group,
|
|
427
|
-
|
|
441
|
+
confidenceMethod: parameters.confidenceMethod,
|
|
442
|
+
level: parameters.level,
|
|
428
443
|
degree: parameters.degree
|
|
429
444
|
});
|
|
430
445
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
2
|
import { validateGeneratedItemLimit } from "../../core/validation.js";
|
|
3
|
+
import { normalizeConfidenceInterval } from
|
|
4
|
+
"../statistics/confidenceInterval.js";
|
|
3
5
|
|
|
4
6
|
const MAX_POLYNOMIAL_DEGREE = 32;
|
|
5
7
|
|
|
@@ -14,6 +16,8 @@ export function normalizeRegressionParameters({
|
|
|
14
16
|
method = "linear",
|
|
15
17
|
degree,
|
|
16
18
|
span,
|
|
19
|
+
confidenceMethod,
|
|
20
|
+
level,
|
|
17
21
|
confidence,
|
|
18
22
|
interval
|
|
19
23
|
} = {}) {
|
|
@@ -24,7 +28,10 @@ export function normalizeRegressionParameters({
|
|
|
24
28
|
if (degree !== undefined) {
|
|
25
29
|
throw new Error("Regression degree requires the polynomial method.");
|
|
26
30
|
}
|
|
27
|
-
if (
|
|
31
|
+
if (
|
|
32
|
+
confidenceMethod !== undefined || level !== undefined ||
|
|
33
|
+
confidence !== undefined || interval !== undefined
|
|
34
|
+
) {
|
|
28
35
|
throw new Error("LOESS regression does not support confidence intervals.");
|
|
29
36
|
}
|
|
30
37
|
const resolvedSpan = span ?? 0.75;
|
|
@@ -38,14 +45,18 @@ export function normalizeRegressionParameters({
|
|
|
38
45
|
if (span !== undefined) {
|
|
39
46
|
throw new Error("Regression span requires the loess method.");
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
resolvedConfidence >= 1
|
|
46
|
-
) {
|
|
47
|
-
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
48
|
+
if (level !== undefined && confidence !== undefined && level !== confidence) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Regression level and confidence alias must match when both are provided."
|
|
51
|
+
);
|
|
48
52
|
}
|
|
53
|
+
const confidenceInterval = normalizeConfidenceInterval({
|
|
54
|
+
method: confidenceMethod,
|
|
55
|
+
level: level ?? confidence
|
|
56
|
+
}, {
|
|
57
|
+
defaultMethod: "student-t",
|
|
58
|
+
label: "Regression confidence interval"
|
|
59
|
+
});
|
|
49
60
|
const resolvedInterval = interval ?? "mean";
|
|
50
61
|
if (!["mean", "prediction"].includes(resolvedInterval)) {
|
|
51
62
|
throw new Error(`Unsupported regression interval "${resolvedInterval}".`);
|
|
@@ -65,7 +76,8 @@ export function normalizeRegressionParameters({
|
|
|
65
76
|
return cloneAndFreeze({
|
|
66
77
|
method,
|
|
67
78
|
degree: resolvedDegree,
|
|
68
|
-
|
|
79
|
+
confidenceMethod: confidenceInterval.method,
|
|
80
|
+
level: confidenceInterval.level,
|
|
69
81
|
interval: resolvedInterval
|
|
70
82
|
});
|
|
71
83
|
}
|
|
@@ -74,14 +86,16 @@ export function normalizeRegressionParameters({
|
|
|
74
86
|
}
|
|
75
87
|
return cloneAndFreeze({
|
|
76
88
|
method,
|
|
77
|
-
|
|
89
|
+
confidenceMethod: confidenceInterval.method,
|
|
90
|
+
level: confidenceInterval.level,
|
|
78
91
|
interval: resolvedInterval
|
|
79
92
|
});
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
export function validateRegressionTransform(transform) {
|
|
83
96
|
const supported = [
|
|
84
|
-
"type", "method", "x", "y", "groupBy", "
|
|
97
|
+
"type", "method", "x", "y", "groupBy", "confidenceMethod", "level",
|
|
98
|
+
"confidence", "interval",
|
|
85
99
|
"degree", "span"
|
|
86
100
|
];
|
|
87
101
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
@@ -96,16 +110,29 @@ export function validateRegressionTransform(transform) {
|
|
|
96
110
|
if (transform.groupBy !== undefined) {
|
|
97
111
|
requireRegressionField(transform.groupBy, "Regression groupBy field");
|
|
98
112
|
}
|
|
113
|
+
if (
|
|
114
|
+
transform.confidence !== undefined &&
|
|
115
|
+
(transform.confidenceMethod !== undefined || transform.level !== undefined)
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Regression confidence provenance must use either the legacy confidence field or method and level."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
99
121
|
const normalized = normalizeRegressionParameters({
|
|
100
122
|
method: transform.method,
|
|
101
123
|
degree: transform.degree,
|
|
102
124
|
span: transform.span,
|
|
125
|
+
confidenceMethod: transform.confidenceMethod,
|
|
126
|
+
level: transform.level,
|
|
103
127
|
confidence: transform.confidence,
|
|
104
128
|
interval: transform.interval
|
|
105
129
|
});
|
|
106
130
|
if (normalized.method === "loess") return transform;
|
|
107
|
-
if (
|
|
108
|
-
|
|
131
|
+
if (
|
|
132
|
+
transform.confidence === undefined &&
|
|
133
|
+
(transform.confidenceMethod === undefined || transform.level === undefined)
|
|
134
|
+
) {
|
|
135
|
+
throw new Error("Regression confidence provenance requires method and level.");
|
|
109
136
|
}
|
|
110
137
|
if (transform.interval === undefined) {
|
|
111
138
|
throw new Error(`Unsupported regression interval "${transform.interval}".`);
|