ggaction 0.0.13 → 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 +78 -0
- package/README.md +20 -11
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +8 -3
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +11 -3
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/categoryOrder/index.js +15 -4
- package/src/actions/charts/area.js +5 -1
- package/src/actions/charts/bar.js +2 -1
- package/src/actions/charts/density.js +6 -2
- package/src/actions/charts/ecdf.js +4 -1
- package/src/actions/charts/endpoints.js +25 -3
- package/src/actions/charts/heatmap.js +2 -1
- package/src/actions/charts/histogram.js +4 -3
- package/src/actions/charts/horizon.js +3 -1
- package/src/actions/charts/interval-regression.js +5 -2
- package/src/actions/charts/line.js +3 -1
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +10 -2
- package/src/actions/charts/polar.js +15 -4
- package/src/actions/charts/radar.js +8 -2
- package/src/actions/charts/radial.js +10 -2
- package/src/actions/charts/rug-strip.js +6 -2
- package/src/actions/charts/scatter.js +3 -1
- package/src/actions/composition/actions.js +13 -4
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +47 -17
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +14 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +14 -0
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +16 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/summary.js +1 -1
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +2 -1
- package/src/actions/encodings/density.js +5 -2
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +20 -13
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +10 -2
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/remove.js +9 -4
- package/src/actions/encodings/ruleAppearance.js +1 -23
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/errorBands/create.js +23 -3
- package/src/actions/errorBands/edit.js +21 -6
- package/src/actions/errorBars/create.js +35 -7
- package/src/actions/errorBars/edit.js +19 -4
- package/src/actions/facets/actions.js +237 -57
- package/src/actions/facets/derive.js +91 -26
- package/src/actions/gradientPlots/components.js +10 -2
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +3 -1
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/labels.js +58 -12
- package/src/actions/guides/axes/lines.js +6 -2
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/tickGroups.js +2 -1
- package/src/actions/guides/axes/ticks.js +6 -2
- package/src/actions/guides/axes/titles.js +11 -3
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +19 -3
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +134 -48
- package/src/actions/guides/legends/categorical/components.js +17 -12
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +97 -15
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +85 -30
- package/src/actions/guides/legends/continuous/common.js +30 -12
- package/src/actions/guides/legends/continuous/gradient.js +10 -7
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +8 -3
- package/src/actions/guides/legends/continuous/opacity.js +8 -5
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +158 -59
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +54 -19
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +13 -3
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +93 -40
- package/src/actions/guides/legends/strokeWidth.js +34 -28
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +24 -5
- package/src/actions/guides/polar/axes/lines.js +10 -4
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +1 -1
- package/src/actions/guides/polar/axes/titles.js +1 -1
- package/src/actions/guides/polar/grids.js +1 -1
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +14 -3
- package/src/actions/marks/arc/actions.js +77 -19
- package/src/actions/marks/area/actions.js +34 -9
- package/src/actions/marks/area/materialize.js +17 -1
- 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 +22 -2
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +37 -14
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +18 -6
- package/src/actions/marks/point/materialize.js +49 -15
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +601 -30
- package/src/actions/marks/remove.js +71 -1
- package/src/actions/marks/rule/actions.js +47 -10
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +74 -9
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +154 -6
- 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/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +2 -1
- package/src/actions/primitives/semanticAction.js +83 -10
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +10 -2
- package/src/actions/regression/edit.js +21 -7
- 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 +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +54 -22
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +66 -11
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +4 -3
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +68 -13
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +315 -67
- package/src/actions/theme/state.js +160 -0
- package/src/actions/violinPlots/create.js +5 -2
- package/src/actions/violinPlots/edit.js +1 -0
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +6 -1
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +340 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/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 +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +343 -0
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +19 -4
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/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 +32 -1
- package/src/selectors/layers.js +4 -0
- package/src/theme/defaults.js +101 -0
- package/types/basic.d.ts +6 -0
- package/types/index.d.ts +85 -0
- package/types/program.d.ts +910 -117
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
inverseLerp,
|
|
4
|
+
numericExtent,
|
|
5
|
+
requireFiniteResult,
|
|
6
|
+
stableFiniteDeviation,
|
|
7
|
+
stableFiniteSum
|
|
8
|
+
} from "./numeric.js";
|
|
9
|
+
|
|
10
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
11
|
+
"type", "field", "as", "groupBy", "method", "variance",
|
|
12
|
+
"zeroDenominator", "baseline", "sortBy"
|
|
13
|
+
]);
|
|
14
|
+
const METHODS = Object.freeze([
|
|
15
|
+
"share", "zscore", "minmax", "index", "change", "percentChange"
|
|
16
|
+
]);
|
|
17
|
+
const ZERO_POLICIES = Object.freeze(["error", "null", "zero"]);
|
|
18
|
+
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
19
|
+
const ORDERS = Object.freeze(["ascending", "descending"]);
|
|
20
|
+
const BASELINE_KEYS = Object.freeze(["position", "value"]);
|
|
21
|
+
const BASELINE_METHODS = new Set(["index", "change", "percentChange"]);
|
|
22
|
+
|
|
23
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
24
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
25
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireField(value, label) {
|
|
29
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
30
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeGroupBy(value) {
|
|
36
|
+
if (value === undefined) return [];
|
|
37
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateGroupBy(groupBy) {
|
|
41
|
+
if (!Array.isArray(groupBy) || groupBy.some(field =>
|
|
42
|
+
typeof field !== "string" || field.length === 0
|
|
43
|
+
)) {
|
|
44
|
+
throw new TypeError("Normalize groupBy must contain field names.");
|
|
45
|
+
}
|
|
46
|
+
if (new Set(groupBy).size !== groupBy.length) {
|
|
47
|
+
throw new Error("Normalize groupBy fields must be unique.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeSortBy(value) {
|
|
52
|
+
if (value === undefined) return [];
|
|
53
|
+
if (!Array.isArray(value)) return value;
|
|
54
|
+
return value.map(sort => isPlainObject(sort)
|
|
55
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
56
|
+
: sort
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateSortBy(sortBy) {
|
|
61
|
+
if (!Array.isArray(sortBy)) throw new TypeError("Normalize sortBy must be an array.");
|
|
62
|
+
const fields = [];
|
|
63
|
+
sortBy.forEach((sort, index) => {
|
|
64
|
+
if (!isPlainObject(sort)) {
|
|
65
|
+
throw new TypeError(`Normalize sortBy[${index}] must be a plain object.`);
|
|
66
|
+
}
|
|
67
|
+
rejectUnknownKeys(sort, SORT_KEYS, `normalize sortBy[${index}]`);
|
|
68
|
+
fields.push(requireField(sort.field, `Normalize sortBy[${index}].field`));
|
|
69
|
+
if (!ORDERS.includes(sort.order)) {
|
|
70
|
+
throw new Error(`Unsupported normalize sort order "${sort.order}".`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (new Set(fields).size !== fields.length) {
|
|
74
|
+
throw new Error("Normalize sortBy fields must be unique.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeBaseline(value) {
|
|
79
|
+
if (!isPlainObject(value)) return value;
|
|
80
|
+
return { ...value };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateBaseline(baseline, sortBy) {
|
|
84
|
+
if (!isPlainObject(baseline)) {
|
|
85
|
+
throw new TypeError("Normalize baseline must be a plain object.");
|
|
86
|
+
}
|
|
87
|
+
rejectUnknownKeys(baseline, BASELINE_KEYS, "normalize baseline");
|
|
88
|
+
const modes = BASELINE_KEYS.filter(key => Object.hasOwn(baseline, key));
|
|
89
|
+
if (modes.length !== 1) {
|
|
90
|
+
throw new Error("Normalize baseline requires exactly one of position or value.");
|
|
91
|
+
}
|
|
92
|
+
if (modes[0] === "value") {
|
|
93
|
+
if (!Number.isFinite(baseline.value)) {
|
|
94
|
+
throw new TypeError("Normalize baseline value must be finite.");
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (!["first", "last"].includes(baseline.position)) {
|
|
98
|
+
throw new Error("Normalize baseline position must be first or last.");
|
|
99
|
+
}
|
|
100
|
+
if (sortBy.length === 0) {
|
|
101
|
+
throw new Error("Normalize baseline position requires non-empty sortBy.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function validateNormalizeTransform(transform) {
|
|
107
|
+
if (!isPlainObject(transform)) {
|
|
108
|
+
throw new TypeError("Normalize transform must be a plain object.");
|
|
109
|
+
}
|
|
110
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "normalize transform");
|
|
111
|
+
if (transform.type !== "normalize") {
|
|
112
|
+
throw new Error(`Unsupported normalize transform "${transform.type}".`);
|
|
113
|
+
}
|
|
114
|
+
requireField(transform.field, "Normalize field");
|
|
115
|
+
requireField(transform.as, "Normalize output field");
|
|
116
|
+
validateGroupBy(transform.groupBy);
|
|
117
|
+
if (!METHODS.includes(transform.method)) {
|
|
118
|
+
throw new Error(`Unsupported normalize method "${transform.method}".`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hasVariance = Object.hasOwn(transform, "variance");
|
|
122
|
+
const hasZero = Object.hasOwn(transform, "zeroDenominator");
|
|
123
|
+
const hasBaseline = Object.hasOwn(transform, "baseline");
|
|
124
|
+
const hasSort = Object.hasOwn(transform, "sortBy");
|
|
125
|
+
if (transform.method === "zscore") {
|
|
126
|
+
if (!["population", "sample"].includes(transform.variance)) {
|
|
127
|
+
throw new Error("Normalize zscore variance must be population or sample.");
|
|
128
|
+
}
|
|
129
|
+
} else if (hasVariance) {
|
|
130
|
+
throw new Error(`Normalize variance is not available for ${transform.method}.`);
|
|
131
|
+
}
|
|
132
|
+
if (hasZero && !ZERO_POLICIES.includes(transform.zeroDenominator)) {
|
|
133
|
+
throw new Error(`Unsupported normalize zeroDenominator "${transform.zeroDenominator}".`);
|
|
134
|
+
}
|
|
135
|
+
if (transform.method === "change" && hasZero) {
|
|
136
|
+
throw new Error("Normalize zeroDenominator is not available for change.");
|
|
137
|
+
}
|
|
138
|
+
if (BASELINE_METHODS.has(transform.method)) {
|
|
139
|
+
validateSortBy(transform.sortBy);
|
|
140
|
+
validateBaseline(transform.baseline, transform.sortBy);
|
|
141
|
+
} else {
|
|
142
|
+
if (hasBaseline) {
|
|
143
|
+
throw new Error(`Normalize baseline is not available for ${transform.method}.`);
|
|
144
|
+
}
|
|
145
|
+
if (hasSort) {
|
|
146
|
+
throw new Error(`Normalize sortBy is not available for ${transform.method}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return transform;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function normalizeNormalizeTransform(args = {}) {
|
|
153
|
+
const {
|
|
154
|
+
field,
|
|
155
|
+
as,
|
|
156
|
+
groupBy,
|
|
157
|
+
method,
|
|
158
|
+
variance,
|
|
159
|
+
zeroDenominator,
|
|
160
|
+
baseline,
|
|
161
|
+
sortBy
|
|
162
|
+
} = args;
|
|
163
|
+
const transform = {
|
|
164
|
+
type: "normalize",
|
|
165
|
+
field,
|
|
166
|
+
as,
|
|
167
|
+
groupBy: normalizeGroupBy(groupBy),
|
|
168
|
+
method
|
|
169
|
+
};
|
|
170
|
+
if (method === "zscore") transform.variance = variance ?? "population";
|
|
171
|
+
else if (Object.hasOwn(args, "variance")) transform.variance = variance;
|
|
172
|
+
if (method !== "change") transform.zeroDenominator = zeroDenominator ?? "error";
|
|
173
|
+
else if (Object.hasOwn(args, "zeroDenominator")) {
|
|
174
|
+
transform.zeroDenominator = zeroDenominator;
|
|
175
|
+
}
|
|
176
|
+
if (BASELINE_METHODS.has(method)) {
|
|
177
|
+
transform.baseline = normalizeBaseline(baseline ?? { position: "first" });
|
|
178
|
+
transform.sortBy = normalizeSortBy(sortBy);
|
|
179
|
+
} else {
|
|
180
|
+
if (Object.hasOwn(args, "baseline")) transform.baseline = normalizeBaseline(baseline);
|
|
181
|
+
if (Object.hasOwn(args, "sortBy")) transform.sortBy = normalizeSortBy(sortBy);
|
|
182
|
+
}
|
|
183
|
+
validateNormalizeTransform(transform);
|
|
184
|
+
return cloneAndFreeze(transform);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function scalarKey(value, label) {
|
|
188
|
+
if (value === null) return "null";
|
|
189
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
190
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
191
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
192
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
193
|
+
}
|
|
194
|
+
throw new TypeError(
|
|
195
|
+
`${label} must contain null, strings, booleans, or finite numbers.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function compareScalar(left, right, label) {
|
|
200
|
+
const leftMissing = left === null || left === undefined;
|
|
201
|
+
const rightMissing = right === null || right === undefined;
|
|
202
|
+
if (leftMissing || rightMissing) {
|
|
203
|
+
if (leftMissing && rightMissing) return 0;
|
|
204
|
+
return leftMissing ? 1 : -1;
|
|
205
|
+
}
|
|
206
|
+
if (typeof left !== typeof right ||
|
|
207
|
+
!["number", "string", "boolean"].includes(typeof left) ||
|
|
208
|
+
typeof left === "number" && (!Number.isFinite(left) || !Number.isFinite(right))) {
|
|
209
|
+
throw new TypeError(`${label} must contain one comparable primitive type.`);
|
|
210
|
+
}
|
|
211
|
+
if (left === right) return 0;
|
|
212
|
+
return left < right ? -1 : 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function compareEntries(left, right, sortBy) {
|
|
216
|
+
for (const sort of sortBy) {
|
|
217
|
+
const comparison = compareScalar(
|
|
218
|
+
left.row[sort.field],
|
|
219
|
+
right.row[sort.field],
|
|
220
|
+
`Normalize sort field "${sort.field}"`
|
|
221
|
+
);
|
|
222
|
+
if (comparison !== 0) return sort.order === "ascending" ? comparison : -comparison;
|
|
223
|
+
}
|
|
224
|
+
return left.index - right.index;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateRows(rows, transform) {
|
|
228
|
+
if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
|
|
229
|
+
throw new TypeError("Normalize source rows must be plain objects.");
|
|
230
|
+
}
|
|
231
|
+
if (rows.length === 0) return;
|
|
232
|
+
const fields = [
|
|
233
|
+
transform.field,
|
|
234
|
+
...transform.groupBy,
|
|
235
|
+
...(transform.sortBy ?? []).map(sort => sort.field)
|
|
236
|
+
];
|
|
237
|
+
rows.forEach((row, index) => {
|
|
238
|
+
if (Object.hasOwn(row, transform.as)) {
|
|
239
|
+
throw new Error(`Normalize output field "${transform.as}" already exists.`);
|
|
240
|
+
}
|
|
241
|
+
for (const field of fields) {
|
|
242
|
+
if (!Object.hasOwn(row, field)) {
|
|
243
|
+
throw new Error(`Normalize source does not contain field "${field}" at row ${index}.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!Number.isFinite(row[transform.field])) {
|
|
247
|
+
throw new TypeError(
|
|
248
|
+
`Normalize field "${transform.field}" must contain finite numbers at row ${index}.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
for (const field of transform.groupBy) {
|
|
252
|
+
scalarKey(row[field], `Normalize group field "${field}"`);
|
|
253
|
+
}
|
|
254
|
+
for (const sort of transform.sortBy ?? []) {
|
|
255
|
+
compareScalar(
|
|
256
|
+
row[sort.field],
|
|
257
|
+
row[sort.field],
|
|
258
|
+
`Normalize sort field "${sort.field}"`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function groupRows(rows, transform) {
|
|
265
|
+
const groups = new Map();
|
|
266
|
+
rows.forEach((row, index) => {
|
|
267
|
+
const key = transform.groupBy.length === 0
|
|
268
|
+
? "all"
|
|
269
|
+
: transform.groupBy.map(field =>
|
|
270
|
+
scalarKey(row[field], `Normalize group field "${field}"`)
|
|
271
|
+
).join("\0");
|
|
272
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
273
|
+
groups.get(key).push({ row, index });
|
|
274
|
+
});
|
|
275
|
+
return [...groups.values()];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function zeroResult(policy, method) {
|
|
279
|
+
if (policy === "null") return null;
|
|
280
|
+
if (policy === "zero") return 0;
|
|
281
|
+
throw new RangeError(`Normalize ${method} denominator is zero.`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function finiteOutput(value, method) {
|
|
285
|
+
return requireFiniteResult(value, `Normalize ${method} output`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function zscore(value, mean, deviation, method) {
|
|
289
|
+
const difference = value - mean;
|
|
290
|
+
return finiteOutput(
|
|
291
|
+
Number.isFinite(difference) ? difference / deviation : value / deviation - mean / deviation,
|
|
292
|
+
method
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function applyGroup(group, transform, outputs) {
|
|
297
|
+
const values = group.map(entry => entry.row[transform.field]);
|
|
298
|
+
if (transform.method === "share" && values.some(value => value < 0)) {
|
|
299
|
+
throw new RangeError("Normalize share does not accept negative values.");
|
|
300
|
+
}
|
|
301
|
+
if (transform.method === "share") {
|
|
302
|
+
const total = stableFiniteSum(values, "Normalize share sum");
|
|
303
|
+
group.forEach((entry, index) => {
|
|
304
|
+
outputs[entry.index] = total === 0
|
|
305
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
306
|
+
: finiteOutput(values[index] / total, transform.method);
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (transform.method === "minmax") {
|
|
311
|
+
const [minimum, maximum] = numericExtent(values);
|
|
312
|
+
group.forEach((entry, index) => {
|
|
313
|
+
outputs[entry.index] = minimum === maximum
|
|
314
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
315
|
+
: finiteOutput(inverseLerp(values[index], minimum, maximum), transform.method);
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (transform.method === "zscore") {
|
|
320
|
+
if (transform.variance === "sample" && values.length < 2) {
|
|
321
|
+
throw new RangeError("Normalize sample zscore requires at least two values.");
|
|
322
|
+
}
|
|
323
|
+
const { mean, deviation } = stableFiniteDeviation(values, {
|
|
324
|
+
sample: transform.variance === "sample",
|
|
325
|
+
label: "Normalize zscore deviation"
|
|
326
|
+
});
|
|
327
|
+
group.forEach((entry, index) => {
|
|
328
|
+
outputs[entry.index] = deviation === 0
|
|
329
|
+
? zeroResult(transform.zeroDenominator, transform.method)
|
|
330
|
+
: zscore(values[index], mean, deviation, transform.method);
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const sorted = [...group].sort((left, right) =>
|
|
336
|
+
compareEntries(left, right, transform.sortBy)
|
|
337
|
+
);
|
|
338
|
+
const baseline = Object.hasOwn(transform.baseline, "value")
|
|
339
|
+
? transform.baseline.value
|
|
340
|
+
: sorted[transform.baseline.position === "first" ? 0 : sorted.length - 1]
|
|
341
|
+
.row[transform.field];
|
|
342
|
+
group.forEach(entry => {
|
|
343
|
+
const value = entry.row[transform.field];
|
|
344
|
+
if (transform.method === "change") {
|
|
345
|
+
outputs[entry.index] = finiteOutput(value - baseline, transform.method);
|
|
346
|
+
} else if (baseline === 0) {
|
|
347
|
+
outputs[entry.index] = zeroResult(transform.zeroDenominator, transform.method);
|
|
348
|
+
} else if (transform.method === "index") {
|
|
349
|
+
outputs[entry.index] = finiteOutput(value / baseline * 100, transform.method);
|
|
350
|
+
} else {
|
|
351
|
+
outputs[entry.index] = finiteOutput(value / baseline - 1, transform.method);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function deriveNormalizedRows(rows, transform) {
|
|
357
|
+
validateNormalizeTransform(transform);
|
|
358
|
+
validateRows(rows, transform);
|
|
359
|
+
if (rows.length === 0) return cloneAndFreeze([]);
|
|
360
|
+
const outputs = new Array(rows.length);
|
|
361
|
+
for (const group of groupRows(rows, transform)) applyGroup(group, transform, outputs);
|
|
362
|
+
return cloneAndFreeze(rows.map((row, index) => ({
|
|
363
|
+
...row,
|
|
364
|
+
[transform.as]: outputs[index]
|
|
365
|
+
})));
|
|
366
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { validateNonEmptyString } from "../core/validation.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
readQuantitativeField,
|
|
5
|
+
readScaleField
|
|
6
|
+
} from "./scales/fields.js";
|
|
3
7
|
|
|
4
8
|
export function normalizeGroupFields(value) {
|
|
5
9
|
const fields = Array.isArray(value) ? [...value] : [value];
|
|
@@ -36,9 +40,13 @@ export function readPathSeriesFields(rows, layer) {
|
|
|
36
40
|
return fields;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
function seriesValueIndex(rows, fields, field, channel) {
|
|
43
|
+
function seriesValueIndex(rows, fields, field, channel, encoding = {}) {
|
|
40
44
|
const quantitative = channel === "strokeWidth" || channel === "opacity";
|
|
41
|
-
const values =
|
|
45
|
+
const values = channel === "stroke"
|
|
46
|
+
? readScaleField(rows, field, encoding.fieldType ?? "nominal", {
|
|
47
|
+
temporalUnit: encoding.temporalUnit
|
|
48
|
+
})
|
|
49
|
+
: quantitative
|
|
42
50
|
? readQuantitativeField(rows, field)
|
|
43
51
|
: readNominalField(rows, field);
|
|
44
52
|
const result = new Map();
|
|
@@ -56,9 +64,15 @@ function seriesValueIndex(rows, fields, field, channel) {
|
|
|
56
64
|
return result;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
export function derivePathSeriesFieldValues(
|
|
67
|
+
export function derivePathSeriesFieldValues(
|
|
68
|
+
rows,
|
|
69
|
+
series,
|
|
70
|
+
field,
|
|
71
|
+
channel,
|
|
72
|
+
encoding
|
|
73
|
+
) {
|
|
60
74
|
const fields = Object.keys(series[0]?.key ?? {});
|
|
61
|
-
const values = seriesValueIndex(rows, fields, field, channel);
|
|
75
|
+
const values = seriesValueIndex(rows, fields, field, channel, encoding);
|
|
62
76
|
return series.map(item => values.get(JSON.stringify(Object.values(item.key))));
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -67,16 +81,20 @@ export function validatePathSeriesAppearance(rows, layer) {
|
|
|
67
81
|
layer.encoding?.parallel !== undefined) return;
|
|
68
82
|
const grouping = readPathSeriesFields(rows, layer);
|
|
69
83
|
const channels = layer.mark.type === "line"
|
|
70
|
-
? ["color", "strokeDash", "strokeWidth", "opacity"]
|
|
84
|
+
? ["color", "stroke", "strokeDash", "strokeWidth", "opacity"]
|
|
85
|
+
: ["color", "stroke"];
|
|
71
86
|
for (const channel of channels) {
|
|
72
87
|
const encoding = layer.encoding?.[channel];
|
|
73
88
|
if (encoding?.field === undefined) continue;
|
|
74
|
-
const allowed =
|
|
89
|
+
const allowed = ["color", "stroke"].includes(channel)
|
|
90
|
+
? channel === "color"
|
|
91
|
+
? ["nominal", "ordinal"]
|
|
92
|
+
: ["nominal", "ordinal", "quantitative", "temporal"]
|
|
75
93
|
: channel === "strokeDash" ? ["nominal"] : ["quantitative"];
|
|
76
94
|
if (!allowed.includes(encoding.fieldType)) {
|
|
77
95
|
throw new Error(`Path ${channel} encoding has an unsupported field type.`);
|
|
78
96
|
}
|
|
79
|
-
seriesValueIndex(rows, grouping, encoding.field, channel);
|
|
97
|
+
seriesValueIndex(rows, grouping, encoding.field, channel, encoding);
|
|
80
98
|
}
|
|
81
99
|
return grouping;
|
|
82
100
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
2
|
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
3
|
+
import { requestedStrokeDetails } from "./strokeStyle.js";
|
|
3
4
|
|
|
4
5
|
export const POINT_SHAPES = cloneAndFreeze([
|
|
5
6
|
"circle",
|
|
@@ -109,12 +110,13 @@ function normalizePolygon(points, x, y, area) {
|
|
|
109
110
|
}));
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
function appearance(fill, stroke, strokeWidth, opacity) {
|
|
113
|
+
function appearance(fill, stroke, strokeWidth, opacity, strokeDetails) {
|
|
113
114
|
return {
|
|
114
115
|
fill,
|
|
115
116
|
...(stroke === undefined ? {} : { stroke }),
|
|
116
117
|
...(strokeWidth === undefined ? {} : { strokeWidth }),
|
|
117
|
-
...(opacity === undefined ? {} : { opacity })
|
|
118
|
+
...(opacity === undefined ? {} : { opacity }),
|
|
119
|
+
...requestedStrokeDetails(strokeDetails, "Point shape")
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -127,7 +129,10 @@ export function createPointShapeGraphic({
|
|
|
127
129
|
stroke,
|
|
128
130
|
strokeWidth,
|
|
129
131
|
opacity,
|
|
130
|
-
angle
|
|
132
|
+
angle,
|
|
133
|
+
lineCap,
|
|
134
|
+
lineJoin,
|
|
135
|
+
miterLimit
|
|
131
136
|
}) {
|
|
132
137
|
const validated = validatePointShape(shape);
|
|
133
138
|
if (![x, y, area].every(Number.isFinite) || area < 0) {
|
|
@@ -136,7 +141,11 @@ export function createPointShapeGraphic({
|
|
|
136
141
|
if (typeof fill !== "string" || fill.length === 0) {
|
|
137
142
|
throw new TypeError("Point shape fill must be a non-empty string.");
|
|
138
143
|
}
|
|
139
|
-
const shared = appearance(fill, stroke, strokeWidth, opacity
|
|
144
|
+
const shared = appearance(fill, stroke, strokeWidth, opacity, {
|
|
145
|
+
...(lineCap === undefined ? {} : { lineCap }),
|
|
146
|
+
...(lineJoin === undefined ? {} : { lineJoin }),
|
|
147
|
+
...(miterLimit === undefined ? {} : { miterLimit })
|
|
148
|
+
});
|
|
140
149
|
if (angle !== undefined && !Number.isFinite(angle)) {
|
|
141
150
|
throw new TypeError("Point shape angle must be finite degrees.");
|
|
142
151
|
}
|
package/src/grammar/polar.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
2
3
|
import { validatePair } from "./scales/validation.js";
|
|
3
4
|
|
|
5
|
+
const POLAR_FRAME_OPTIONS = Object.freeze(["center", "radius"]);
|
|
6
|
+
const POLAR_CENTER_OPTIONS = Object.freeze(["x", "y"]);
|
|
7
|
+
const POLAR_RADIUS_OPTIONS = Object.freeze(["unit", "value"]);
|
|
8
|
+
const POLAR_RADIUS_UNITS = new Set(["fraction", "px"]);
|
|
9
|
+
|
|
4
10
|
function validateBounds(bounds) {
|
|
5
11
|
if (
|
|
6
12
|
!isPlainObject(bounds) ||
|
|
@@ -15,12 +21,88 @@ function validateBounds(bounds) {
|
|
|
15
21
|
return bounds;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
function finiteUnitInterval(value, label) {
|
|
25
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
26
|
+
throw new RangeError(`${label} must be a finite number from 0 through 1.`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizePolarFrameOptions(value) {
|
|
32
|
+
if (value === "auto") return value;
|
|
33
|
+
if (!isPlainObject(value)) {
|
|
34
|
+
throw new TypeError('Polar frame must be "auto" or a plain object.');
|
|
35
|
+
}
|
|
36
|
+
validateOptionObject(value, POLAR_FRAME_OPTIONS, "polar frame");
|
|
37
|
+
const center = value.center ?? {};
|
|
38
|
+
if (!isPlainObject(center)) {
|
|
39
|
+
throw new TypeError("Polar frame center must be a plain object.");
|
|
40
|
+
}
|
|
41
|
+
validateOptionObject(center, POLAR_CENTER_OPTIONS, "polar frame center");
|
|
42
|
+
const radius = value.radius ?? { unit: "fraction", value: 1 };
|
|
43
|
+
if (!isPlainObject(radius)) {
|
|
44
|
+
throw new TypeError("Polar frame radius must be a plain object.");
|
|
45
|
+
}
|
|
46
|
+
validateOptionObject(radius, POLAR_RADIUS_OPTIONS, "polar frame radius", {
|
|
47
|
+
allowEmpty: false,
|
|
48
|
+
emptyError: TypeError
|
|
49
|
+
});
|
|
50
|
+
if (!POLAR_RADIUS_UNITS.has(radius.unit)) {
|
|
51
|
+
throw new Error(`Unknown Polar frame radius unit "${radius.unit}".`);
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isFinite(radius.value) || radius.value <= 0) {
|
|
54
|
+
throw new RangeError("Polar frame radius must be a positive finite number.");
|
|
55
|
+
}
|
|
56
|
+
if (radius.unit === "fraction" && radius.value > 1) {
|
|
57
|
+
throw new RangeError("Polar frame radius fraction must not exceed 1.");
|
|
58
|
+
}
|
|
59
|
+
return cloneAndFreeze({
|
|
60
|
+
center: {
|
|
61
|
+
x: finiteUnitInterval(center.x ?? 0.5, "Polar frame center x"),
|
|
62
|
+
y: finiteUnitInterval(center.y ?? 0.5, "Polar frame center y")
|
|
63
|
+
},
|
|
64
|
+
radius: {
|
|
65
|
+
unit: radius.unit,
|
|
66
|
+
value: radius.value
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolvePolarFrame(bounds, requestedFrame = "auto") {
|
|
19
72
|
validateBounds(bounds);
|
|
73
|
+
if (requestedFrame === undefined || requestedFrame === "auto") {
|
|
74
|
+
return cloneAndFreeze({
|
|
75
|
+
centerX: bounds.x + bounds.width / 2,
|
|
76
|
+
centerY: bounds.y + bounds.height / 2,
|
|
77
|
+
availableRadius: Math.min(bounds.width, bounds.height) / 2
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const requested = normalizePolarFrameOptions(requestedFrame);
|
|
81
|
+
const centerX = bounds.x + bounds.width * requested.center.x;
|
|
82
|
+
const centerY = bounds.y + bounds.height * requested.center.y;
|
|
83
|
+
const maximumRadius = Math.min(
|
|
84
|
+
centerX - bounds.x,
|
|
85
|
+
bounds.x + bounds.width - centerX,
|
|
86
|
+
centerY - bounds.y,
|
|
87
|
+
bounds.y + bounds.height - centerY
|
|
88
|
+
);
|
|
89
|
+
if (!Number.isFinite(maximumRadius) || maximumRadius <= 0) {
|
|
90
|
+
throw new RangeError(
|
|
91
|
+
"Polar frame center must leave a positive radius inside its bounds."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const availableRadius = requested.radius.unit === "fraction"
|
|
95
|
+
? maximumRadius * requested.radius.value
|
|
96
|
+
: requested.radius.value;
|
|
97
|
+
if (availableRadius > maximumRadius) {
|
|
98
|
+
throw new RangeError(
|
|
99
|
+
`Polar frame radius ${availableRadius} exceeds the maximum radius ${maximumRadius}.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
20
102
|
return cloneAndFreeze({
|
|
21
|
-
centerX
|
|
22
|
-
centerY
|
|
23
|
-
availableRadius
|
|
103
|
+
centerX,
|
|
104
|
+
centerY,
|
|
105
|
+
availableRadius
|
|
24
106
|
});
|
|
25
107
|
}
|
|
26
108
|
|
|
@@ -50,7 +132,7 @@ export function validateRadialRange(range, availableRadius) {
|
|
|
50
132
|
return validated;
|
|
51
133
|
}
|
|
52
134
|
|
|
53
|
-
export function resolvePolarScaleRange(range, channel, bounds) {
|
|
135
|
+
export function resolvePolarScaleRange(range, channel, bounds, frame) {
|
|
54
136
|
if (channel === "theta") {
|
|
55
137
|
return range === "auto"
|
|
56
138
|
? cloneAndFreeze([0, 360])
|
|
@@ -59,10 +141,10 @@ export function resolvePolarScaleRange(range, channel, bounds) {
|
|
|
59
141
|
if (channel !== "radius") {
|
|
60
142
|
throw new Error(`Unknown Polar position channel "${channel}".`);
|
|
61
143
|
}
|
|
62
|
-
const
|
|
144
|
+
const resolvedFrame = frame ?? resolvePolarFrame(bounds);
|
|
63
145
|
return range === "auto"
|
|
64
|
-
? cloneAndFreeze([0,
|
|
65
|
-
: validateRadialRange(range,
|
|
146
|
+
? cloneAndFreeze([0, resolvedFrame.availableRadius])
|
|
147
|
+
: validateRadialRange(range, resolvedFrame.availableRadius);
|
|
66
148
|
}
|
|
67
149
|
|
|
68
150
|
export function polarDirection(theta) {
|
|
@@ -139,3 +139,26 @@ export function validateRegressionTransform(transform) {
|
|
|
139
139
|
}
|
|
140
140
|
return transform;
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
export function normalizeRegressionTransform(args = {}) {
|
|
144
|
+
const parameters = normalizeRegressionParameters(args);
|
|
145
|
+
const transform = {
|
|
146
|
+
type: "regression",
|
|
147
|
+
method: parameters.method,
|
|
148
|
+
x: args.x,
|
|
149
|
+
y: args.y,
|
|
150
|
+
...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
|
|
151
|
+
...(parameters.method === "polynomial"
|
|
152
|
+
? { degree: parameters.degree }
|
|
153
|
+
: {}),
|
|
154
|
+
...(parameters.method === "loess"
|
|
155
|
+
? { span: parameters.span }
|
|
156
|
+
: {
|
|
157
|
+
confidenceMethod: parameters.confidenceMethod,
|
|
158
|
+
level: parameters.level,
|
|
159
|
+
interval: parameters.interval
|
|
160
|
+
})
|
|
161
|
+
};
|
|
162
|
+
validateRegressionTransform(transform);
|
|
163
|
+
return cloneAndFreeze(transform);
|
|
164
|
+
}
|