ggaction 0.0.3 → 0.0.4

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +95 -83
  3. package/package.json +11 -6
  4. package/src/actions/data/regression.js +1 -1
  5. package/src/actions/encodings/appearance.js +1 -1
  6. package/src/actions/encodings/color/continuous.js +137 -0
  7. package/src/actions/encodings/color/index.js +151 -0
  8. package/src/actions/encodings/color/layout.js +91 -0
  9. package/src/actions/encodings/color/policy.js +88 -0
  10. package/src/actions/encodings/index.js +2 -2
  11. package/src/actions/encodings/offset.js +1 -1
  12. package/src/actions/encodings/{position.js → position/index.js} +6 -6
  13. package/src/actions/encodings/position/resolve.js +1 -1
  14. package/src/actions/encodings/ranged.js +2 -2
  15. package/src/actions/encodings/strokeDash.js +1 -1
  16. package/src/actions/encodings/text.js +1 -1
  17. package/src/actions/errorBands/options.js +1 -1
  18. package/src/actions/errorBars/options.js +1 -1
  19. package/src/actions/facets/actions.js +1 -1
  20. package/src/actions/facets/derive.js +2 -2
  21. package/src/actions/facets/guides.js +1 -1
  22. package/src/actions/guides/axes/labels.js +1 -1
  23. package/src/actions/guides/axes/ticks.js +1 -1
  24. package/src/actions/guides/axes/titles.js +1 -1
  25. package/src/actions/guides/grids/resolve.js +1 -1
  26. package/src/actions/guides/legends/categorical/layout.js +4 -2
  27. package/src/actions/guides/legends/continuous/interval.js +1 -1
  28. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  29. package/src/actions/guides/legends/edit.js +2 -20
  30. package/src/actions/guides/legends/remove.js +2 -20
  31. package/src/actions/guides/legends/size.js +1 -1
  32. package/src/actions/guides/legends/target.js +25 -0
  33. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  34. package/src/actions/guides/polar/index.js +1 -1
  35. package/src/actions/guides/polar/resolve.js +1 -1
  36. package/src/actions/guides/tickValues.js +1 -1
  37. package/src/actions/intervals/resolve.js +1 -1
  38. package/src/actions/marks/arc/actions.js +2 -2
  39. package/src/actions/marks/area/actions.js +2 -2
  40. package/src/actions/marks/bar/edit.js +1 -1
  41. package/src/actions/marks/line/actions.js +3 -3
  42. package/src/actions/marks/rect/actions.js +1 -1
  43. package/src/actions/marks/rule/actions.js +1 -1
  44. package/src/actions/marks/shared.js +1 -1
  45. package/src/actions/marks/text/actions.js +1 -1
  46. package/src/actions/primitives/semanticValue.js +1 -1
  47. package/src/actions/regression/components.js +1 -1
  48. package/src/actions/regression/create.js +1 -1
  49. package/src/actions/regression/edit.js +1 -1
  50. package/src/actions/scales/consumers/common.js +91 -0
  51. package/src/actions/scales/consumers/families.js +47 -0
  52. package/src/actions/scales/consumers/index.js +41 -0
  53. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  54. package/src/actions/scales/create.js +1 -1
  55. package/src/actions/scales/definitions.js +2 -2
  56. package/src/actions/scales/edit.js +3 -3
  57. package/src/actions/scales/materialize.js +2 -2
  58. package/src/core/ChartProgram.js +22 -236
  59. package/src/core/compositionState.js +169 -0
  60. package/src/core/materializationState.js +65 -0
  61. package/src/core/programState.js +28 -0
  62. package/src/grammar/arcs.js +1 -1
  63. package/src/grammar/areaSeries.js +1 -1
  64. package/src/grammar/bars/aggregate.js +1 -1
  65. package/src/grammar/density.js +1 -1
  66. package/src/grammar/{facets.js → facets/index.js} +5 -5
  67. package/src/grammar/interval.js +2 -96
  68. package/src/grammar/lineSeries.js +1 -1
  69. package/src/grammar/palettes.js +5 -7
  70. package/src/grammar/polarLineCommands.js +1 -1
  71. package/src/grammar/regression/derive.js +97 -0
  72. package/src/grammar/regression/index.js +22 -0
  73. package/src/grammar/regression/models.js +282 -0
  74. package/src/grammar/regression/parameters.js +106 -0
  75. package/src/grammar/rules.js +1 -1
  76. package/src/grammar/scales/color.js +4 -2
  77. package/src/grammar/statistics/studentT.js +98 -0
  78. package/src/grammar/transforms.js +1 -1
  79. package/src/materialization/bars/aggregate.js +1 -1
  80. package/src/materialization/bars/grouped.js +1 -1
  81. package/src/materialization/bars/histogram.js +1 -1
  82. package/src/materialization/bars/ranged.js +1 -1
  83. package/src/materialization/density.js +1 -1
  84. package/src/materialization/dependencies.js +6 -87
  85. package/src/materialization/encodings.js +2 -2
  86. package/src/materialization/facetGuides/index.js +2 -0
  87. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  88. package/src/materialization/facetGuides/placement.js +126 -0
  89. package/src/materialization/facetGuides/preparation.js +188 -0
  90. package/src/materialization/facets.js +1 -1
  91. package/src/materialization/marks/capabilities.js +141 -0
  92. package/src/materialization/marks/index.js +105 -0
  93. package/src/materialization/marks/policies.js +87 -0
  94. package/src/materialization/rect.js +1 -1
  95. package/src/materialization/rowEncoding.js +1 -1
  96. package/src/materialization/scaleGuideDependencies.js +94 -0
  97. package/src/materialization/scales/map.js +1 -1
  98. package/src/materialization/scales/policies/series.js +1 -1
  99. package/src/materialization/scales/resolve.js +1 -1
  100. package/src/materialization/selection/items/bar.js +1 -1
  101. package/src/materialization/selection/styles.js +1 -1
  102. package/src/renderers/canvas/text.js +14 -1
  103. package/types/extension.d.ts +4 -1
  104. package/src/actions/encodings/color.js +0 -424
  105. package/src/grammar/regression.js +0 -578
  106. package/src/grammar/scales.js +0 -1
  107. package/src/materialization/facetGuides.js +0 -426
  108. package/src/materialization/marks.js +0 -316
@@ -1,5 +1,6 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { aggregateScalarValues } from "./aggregate.js";
3
+ import { studentTCriticalValue } from "./statistics/studentT.js";
3
4
 
4
5
  const CENTER_VALUES = Object.freeze(["mean", "median"]);
5
6
  const EXTENT_VALUES = Object.freeze(["stderr", "stdev", "ci", "iqr"]);
@@ -133,92 +134,6 @@ export function normalizeIntervalParameters({
133
134
  });
134
135
  }
135
136
 
136
- const LANCZOS = Object.freeze([
137
- 676.5203681218851,
138
- -1259.1392167224028,
139
- 771.3234287776531,
140
- -176.6150291621406,
141
- 12.507343278686905,
142
- -0.13857109526572012,
143
- 9.984369578019572e-6,
144
- 1.5056327351493116e-7
145
- ]);
146
-
147
- function logGamma(value) {
148
- if (value < 0.5) {
149
- return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
150
- logGamma(1 - value);
151
- }
152
- const shifted = value - 1;
153
- let sum = 0.9999999999998099;
154
- for (const [index, coefficient] of LANCZOS.entries()) {
155
- sum += coefficient / (shifted + index + 1);
156
- }
157
- const t = shifted + LANCZOS.length - 0.5;
158
- return 0.5 * Math.log(2 * Math.PI) +
159
- (shifted + 0.5) * Math.log(t) - t + Math.log(sum);
160
- }
161
-
162
- function betaFraction(a, b, x) {
163
- const maximumIterations = 200;
164
- const epsilon = 3e-14;
165
- const floor = 1e-300;
166
- const sum = a + b;
167
- let c = 1;
168
- let d = 1 - sum * x / (a + 1);
169
- if (Math.abs(d) < floor) d = floor;
170
- d = 1 / d;
171
- let result = d;
172
-
173
- for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
174
- const even = 2 * iteration;
175
- let coefficient = iteration * (b - iteration) * x /
176
- ((a + even - 1) * (a + even));
177
- d = 1 + coefficient * d;
178
- if (Math.abs(d) < floor) d = floor;
179
- c = 1 + coefficient / c;
180
- if (Math.abs(c) < floor) c = floor;
181
- d = 1 / d;
182
- result *= d * c;
183
-
184
- coefficient = -(a + iteration) * (sum + iteration) * x /
185
- ((a + even) * (a + even + 1));
186
- d = 1 + coefficient * d;
187
- if (Math.abs(d) < floor) d = floor;
188
- c = 1 + coefficient / c;
189
- if (Math.abs(c) < floor) c = floor;
190
- d = 1 / d;
191
- const delta = d * c;
192
- result *= delta;
193
- if (Math.abs(delta - 1) <= epsilon) return result;
194
- }
195
- throw new Error("Student-t calculation did not converge.");
196
- }
197
-
198
- function regularizedBeta(x, a, b) {
199
- if (x <= 0) return 0;
200
- if (x >= 1) return 1;
201
- const front = Math.exp(
202
- logGamma(a + b) - logGamma(a) - logGamma(b) +
203
- a * Math.log(x) + b * Math.log1p(-x)
204
- );
205
- return x < (a + 1) / (a + b + 2)
206
- ? front * betaFraction(a, b, x) / a
207
- : 1 - front * betaFraction(b, a, 1 - x) / b;
208
- }
209
-
210
- function studentTCdf(value, degreesOfFreedom) {
211
- if (value === 0) return 0.5;
212
- const x = degreesOfFreedom /
213
- (degreesOfFreedom + value * value);
214
- const tail = 0.5 * regularizedBeta(
215
- x,
216
- degreesOfFreedom / 2,
217
- 0.5
218
- );
219
- return value > 0 ? 1 - tail : tail;
220
- }
221
-
222
137
  export function studentTCritical(degreesOfFreedom, level) {
223
138
  if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
224
139
  throw new RangeError("Student-t degreesOfFreedom must be a positive integer.");
@@ -226,16 +141,7 @@ export function studentTCritical(degreesOfFreedom, level) {
226
141
  if (!Number.isFinite(level) || level <= 0 || level >= 1) {
227
142
  throw new RangeError("Student-t level must be between 0 and 1.");
228
143
  }
229
- const target = 0.5 + level / 2;
230
- let low = 0;
231
- let high = 1;
232
- while (studentTCdf(high, degreesOfFreedom) < target) high *= 2;
233
- for (let iteration = 0; iteration < 100; iteration += 1) {
234
- const midpoint = (low + high) / 2;
235
- if (studentTCdf(midpoint, degreesOfFreedom) < target) low = midpoint;
236
- else high = midpoint;
237
- }
238
- return (low + high) / 2;
144
+ return studentTCriticalValue(level, degreesOfFreedom);
239
145
  }
240
146
 
241
147
  function isMissing(value) {
@@ -3,7 +3,7 @@ import {
3
3
  readNominalField,
4
4
  readQuantitativeField,
5
5
  readTemporalField
6
- } from "./scales.js";
6
+ } from "./scales/index.js";
7
7
  import {
8
8
  aggregateRows,
9
9
  isAggregate,
@@ -190,16 +190,14 @@ export function resolvePalette(value, domainCount) {
190
190
  }
191
191
 
192
192
  export function resolveContinuousPalette(value, count = 31) {
193
- if (!Number.isInteger(count) || count < 2) {
194
- throw new RangeError("Continuous palette count must be an integer of at least 2.");
195
- }
196
193
  const palette = normalizePalette(value);
197
- if (palette.count !== undefined) {
198
- throw new Error("Continuous palette does not accept count.");
194
+ const resolvedCount = palette.count ?? count;
195
+ if (!Number.isInteger(resolvedCount) || resolvedCount < 2) {
196
+ throw new RangeError("Continuous palette count must be an integer of at least 2.");
199
197
  }
200
198
  const discrete = DISCRETE[palette.name];
201
- if (discrete !== undefined) return discrete;
202
- return resolvePalette({ ...palette, count }, count);
199
+ if (discrete !== undefined && palette.count === undefined) return discrete;
200
+ return resolvePalette({ ...palette, count: resolvedCount }, resolvedCount);
203
201
  }
204
202
 
205
203
  export function paletteFamily(name) {
@@ -3,7 +3,7 @@ import { polarToCartesian } from "./polar.js";
3
3
  import {
4
4
  mapContinuousScaleValues,
5
5
  mapOrdinalPositionValues
6
- } from "./scales.js";
6
+ } from "./scales/index.js";
7
7
 
8
8
  function mappedTheta(values, fieldType, scale) {
9
9
  return ["nominal", "ordinal"].includes(fieldType)
@@ -0,0 +1,97 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import {
3
+ readNominalField,
4
+ readQuantitativeField
5
+ } from "../scales/index.js";
6
+ import { fitRegressionGroup, predictRegressionAt } from "./models.js";
7
+ import {
8
+ normalizeRegressionParameters,
9
+ requireRegressionField
10
+ } from "./parameters.js";
11
+
12
+ export const REGRESSION_LOWER_FIELD = "__regression_ci_lower";
13
+ export const REGRESSION_UPPER_FIELD = "__regression_ci_upper";
14
+
15
+ export function deriveRegression(values, {
16
+ x,
17
+ y,
18
+ groupBy,
19
+ method,
20
+ degree,
21
+ span,
22
+ confidence,
23
+ interval
24
+ } = {}) {
25
+ if (!Array.isArray(values)) {
26
+ throw new TypeError("Regression values must be an array.");
27
+ }
28
+ requireRegressionField(x, "Regression x field");
29
+ requireRegressionField(y, "Regression y field");
30
+ if (groupBy !== undefined) {
31
+ requireRegressionField(groupBy, "Regression groupBy field");
32
+ }
33
+ const parameters = normalizeRegressionParameters({
34
+ method,
35
+ degree,
36
+ span,
37
+ confidence,
38
+ interval
39
+ });
40
+ readQuantitativeField(values, x);
41
+ readQuantitativeField(values, y);
42
+ if (groupBy !== undefined) readNominalField(values, groupBy);
43
+
44
+ const groups = groupBy === undefined
45
+ ? [undefined]
46
+ : [...new Set(values.map(row => row[groupBy]))];
47
+ const models = [];
48
+ const rows = [];
49
+
50
+ for (const group of groups) {
51
+ const groupRows = groupBy === undefined
52
+ ? values
53
+ : values.filter(row => row[groupBy] === group);
54
+ const model = fitRegressionGroup(groupRows, {
55
+ x,
56
+ y,
57
+ group,
58
+ parameters
59
+ });
60
+ const xValues = [...new Set(groupRows.map(row => row[x]))]
61
+ .sort((left, right) => left - right);
62
+ models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
63
+
64
+ for (const xValue of xValues) {
65
+ const prediction = predictRegressionAt(model, xValue, parameters);
66
+ rows.push({
67
+ ...(groupBy === undefined ? {} : { [groupBy]: group }),
68
+ [x]: xValue,
69
+ [y]: prediction.prediction,
70
+ ...(parameters.method === "loess" ? {} : {
71
+ [REGRESSION_LOWER_FIELD]: prediction.prediction - prediction.margin,
72
+ [REGRESSION_UPPER_FIELD]: prediction.prediction + prediction.margin
73
+ })
74
+ });
75
+ }
76
+ }
77
+
78
+ return cloneAndFreeze({
79
+ fields: {
80
+ x,
81
+ y,
82
+ ...(groupBy === undefined ? {} : { group: groupBy }),
83
+ ...(parameters.method === "loess" ? {} : {
84
+ lower: REGRESSION_LOWER_FIELD,
85
+ upper: REGRESSION_UPPER_FIELD
86
+ })
87
+ },
88
+ parameters,
89
+ groups,
90
+ models,
91
+ values: rows
92
+ });
93
+ }
94
+
95
+ export function deriveLinearRegression(values, options = {}) {
96
+ return deriveRegression(values, { ...options, method: "linear" });
97
+ }
@@ -0,0 +1,22 @@
1
+ import { studentTCriticalValue } from "../statistics/studentT.js";
2
+
3
+ export {
4
+ REGRESSION_LOWER_FIELD,
5
+ REGRESSION_UPPER_FIELD,
6
+ deriveLinearRegression,
7
+ deriveRegression
8
+ } from "./derive.js";
9
+ export {
10
+ normalizeRegressionParameters,
11
+ validateRegressionTransform
12
+ } from "./parameters.js";
13
+
14
+ export function studentTCritical(confidence, degreesOfFreedom) {
15
+ if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
16
+ throw new RangeError("Regression confidence must be between 0 and 1.");
17
+ }
18
+ if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom <= 0) {
19
+ throw new RangeError("Student-t degrees of freedom must be positive.");
20
+ }
21
+ return studentTCriticalValue(confidence, degreesOfFreedom);
22
+ }
@@ -0,0 +1,282 @@
1
+ import { studentTCriticalValue } from "../statistics/studentT.js";
2
+
3
+ function fitLinearGroup(rows, { x, y, group, confidence }) {
4
+ const count = rows.length;
5
+ const groupLabel = group === undefined ? "all" : String(group);
6
+ if (count < 3) {
7
+ throw new Error(
8
+ `Regression group "${groupLabel}" requires at least three rows.`
9
+ );
10
+ }
11
+ const meanX = rows.reduce((sum, row) => sum + row[x], 0) / count;
12
+ const meanY = rows.reduce((sum, row) => sum + row[y], 0) / count;
13
+ let sxx = 0;
14
+ let sxy = 0;
15
+ for (const row of rows) {
16
+ const xDifference = row[x] - meanX;
17
+ sxx += xDifference ** 2;
18
+ sxy += xDifference * (row[y] - meanY);
19
+ }
20
+ if (sxx === 0) {
21
+ throw new Error(
22
+ `Regression group "${groupLabel}" requires varying x values.`
23
+ );
24
+ }
25
+ const slope = sxy / sxx;
26
+ const intercept = meanY - slope * meanX;
27
+ const residualSumSquares = rows.reduce((sum, row) => {
28
+ const residual = row[y] - (intercept + slope * row[x]);
29
+ return sum + residual ** 2;
30
+ }, 0);
31
+ const degreesOfFreedom = count - 2;
32
+ return {
33
+ count,
34
+ degreesOfFreedom,
35
+ meanX,
36
+ meanY,
37
+ sxx,
38
+ slope,
39
+ intercept,
40
+ residualSumSquares,
41
+ residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
42
+ critical: studentTCriticalValue(confidence, degreesOfFreedom)
43
+ };
44
+ }
45
+
46
+ function solveLinearSystem(matrix, vector) {
47
+ const size = matrix.length;
48
+ const augmented = matrix.map((row, index) => [...row, vector[index]]);
49
+ for (let column = 0; column < size; column += 1) {
50
+ let pivot = column;
51
+ for (let row = column + 1; row < size; row += 1) {
52
+ if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) {
53
+ pivot = row;
54
+ }
55
+ }
56
+ if (Math.abs(augmented[pivot][column]) < 1e-12) {
57
+ throw new Error("Polynomial regression design is singular.");
58
+ }
59
+ [augmented[column], augmented[pivot]] = [
60
+ augmented[pivot], augmented[column]
61
+ ];
62
+ const divisor = augmented[column][column];
63
+ for (let index = column; index <= size; index += 1) {
64
+ augmented[column][index] /= divisor;
65
+ }
66
+ for (let row = 0; row < size; row += 1) {
67
+ if (row === column) continue;
68
+ const factor = augmented[row][column];
69
+ for (let index = column; index <= size; index += 1) {
70
+ augmented[row][index] -= factor * augmented[column][index];
71
+ }
72
+ }
73
+ }
74
+ return augmented.map(row => row[size]);
75
+ }
76
+
77
+ function invertSymmetricMatrix(matrix) {
78
+ return matrix.map((_, column) => solveLinearSystem(
79
+ matrix,
80
+ matrix.map((__, row) => row === column ? 1 : 0)
81
+ ));
82
+ }
83
+
84
+ function dot(left, right) {
85
+ return left.reduce((sum, value, index) => sum + value * right[index], 0);
86
+ }
87
+
88
+ function binomial(n, k) {
89
+ let result = 1;
90
+ for (let index = 1; index <= k; index += 1) {
91
+ result *= (n - index + 1) / index;
92
+ }
93
+ return result;
94
+ }
95
+
96
+ function rawPolynomialCoefficients(coefficients, center, scale) {
97
+ return coefficients.map((_, degree) => coefficients.reduce(
98
+ (sum, coefficient, power) => power < degree
99
+ ? sum
100
+ : sum + coefficient * binomial(power, degree) *
101
+ (-center) ** (power - degree) / scale ** power,
102
+ 0
103
+ ));
104
+ }
105
+
106
+ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
107
+ const count = rows.length;
108
+ const parameterCount = degree + 1;
109
+ const groupLabel = group === undefined ? "all" : String(group);
110
+ if (
111
+ count < degree + 2 ||
112
+ new Set(rows.map(row => row[x])).size < parameterCount
113
+ ) {
114
+ throw new Error(
115
+ `Polynomial regression group "${groupLabel}" requires at least ` +
116
+ `${degree + 2} rows and ${parameterCount} distinct x values.`
117
+ );
118
+ }
119
+ const center = rows.reduce((sum, row) => sum + row[x], 0) / count;
120
+ const scale = Math.max(...rows.map(row => Math.abs(row[x] - center)));
121
+ if (!(scale > 0)) {
122
+ throw new Error(
123
+ `Regression group "${groupLabel}" requires varying x values.`
124
+ );
125
+ }
126
+ const design = rows.map(row => {
127
+ const normalized = (row[x] - center) / scale;
128
+ return Array.from(
129
+ { length: parameterCount },
130
+ (_, power) => normalized ** power
131
+ );
132
+ });
133
+ const normal = Array.from({ length: parameterCount }, (_, row) =>
134
+ Array.from({ length: parameterCount }, (_, column) =>
135
+ design.reduce(
136
+ (sum, basis) => sum + basis[row] * basis[column],
137
+ 0
138
+ )
139
+ )
140
+ );
141
+ const response = Array.from({ length: parameterCount }, (_, column) =>
142
+ design.reduce(
143
+ (sum, basis, index) => sum + basis[column] * rows[index][y],
144
+ 0
145
+ )
146
+ );
147
+ const normalizedCoefficients = solveLinearSystem(normal, response);
148
+ const inverse = invertSymmetricMatrix(normal);
149
+ const fitted = design.map(basis => dot(basis, normalizedCoefficients));
150
+ const residualSumSquares = fitted.reduce(
151
+ (sum, value, index) => sum + (rows[index][y] - value) ** 2,
152
+ 0
153
+ );
154
+ const degreesOfFreedom = count - parameterCount;
155
+ const residualVariance = residualSumSquares / degreesOfFreedom;
156
+ return {
157
+ count,
158
+ degreesOfFreedom,
159
+ degree,
160
+ coefficients: rawPolynomialCoefficients(
161
+ normalizedCoefficients,
162
+ center,
163
+ scale
164
+ ),
165
+ normalizedCoefficients,
166
+ center,
167
+ scale,
168
+ inverse,
169
+ residualSumSquares,
170
+ residualStandardError: Math.sqrt(residualVariance),
171
+ critical: studentTCriticalValue(confidence, degreesOfFreedom)
172
+ };
173
+ }
174
+
175
+ function evaluatePolynomial(model, xValue) {
176
+ const normalized = (xValue - model.center) / model.scale;
177
+ const basis = model.normalizedCoefficients.map(
178
+ (_, power) => normalized ** power
179
+ );
180
+ return {
181
+ prediction: dot(basis, model.normalizedCoefficients),
182
+ leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
183
+ };
184
+ }
185
+
186
+ function fitLoessGroup(rows, { x, y, group, span }) {
187
+ const groupLabel = group === undefined ? "all" : String(group);
188
+ if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
189
+ throw new Error(
190
+ `LOESS regression group "${groupLabel}" requires at least two rows ` +
191
+ "and varying x values."
192
+ );
193
+ }
194
+ const neighborCount = Math.max(2, Math.ceil(span * rows.length));
195
+ const xValues = [...new Set(rows.map(row => row[x]))]
196
+ .sort((left, right) => left - right);
197
+ const fits = xValues.map(xValue => {
198
+ const neighbors = rows
199
+ .map((row, index) => ({
200
+ row,
201
+ index,
202
+ distance: Math.abs(row[x] - xValue)
203
+ }))
204
+ .sort((left, right) =>
205
+ left.distance - right.distance || left.index - right.index
206
+ )
207
+ .slice(0, neighborCount);
208
+ const radius = neighbors.at(-1).distance;
209
+ const weighted = neighbors.map(neighbor => ({
210
+ ...neighbor,
211
+ weight: radius === 0
212
+ ? 1
213
+ : (1 - (neighbor.distance / radius) ** 3) ** 3
214
+ }));
215
+ const totalWeight = weighted.reduce(
216
+ (sum, item) => sum + item.weight,
217
+ 0
218
+ );
219
+ const meanDifference = weighted.reduce(
220
+ (sum, item) => sum + item.weight * (item.row[x] - xValue),
221
+ 0
222
+ ) / totalWeight;
223
+ const meanY = weighted.reduce(
224
+ (sum, item) => sum + item.weight * item.row[y],
225
+ 0
226
+ ) / totalWeight;
227
+ let variance = 0;
228
+ let covariance = 0;
229
+ for (const item of weighted) {
230
+ const difference = item.row[x] - xValue - meanDifference;
231
+ variance += item.weight * difference ** 2;
232
+ covariance += item.weight * difference * (item.row[y] - meanY);
233
+ }
234
+ const slope = variance === 0 ? 0 : covariance / variance;
235
+ return {
236
+ x: xValue,
237
+ prediction: meanY - slope * meanDifference,
238
+ neighborIndices: weighted.map(item => item.index)
239
+ };
240
+ });
241
+ return { count: rows.length, span, neighborCount, fits };
242
+ }
243
+
244
+ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
245
+ if (parameters.method === "linear") {
246
+ return fitLinearGroup(rows, {
247
+ x,
248
+ y,
249
+ group,
250
+ confidence: parameters.confidence
251
+ });
252
+ }
253
+ if (parameters.method === "polynomial") {
254
+ return fitPolynomialGroup(rows, {
255
+ x,
256
+ y,
257
+ group,
258
+ confidence: parameters.confidence,
259
+ degree: parameters.degree
260
+ });
261
+ }
262
+ return fitLoessGroup(rows, { x, y, group, span: parameters.span });
263
+ }
264
+
265
+ export function predictRegressionAt(model, xValue, parameters) {
266
+ const polynomial = parameters.method === "polynomial"
267
+ ? evaluatePolynomial(model, xValue)
268
+ : undefined;
269
+ const prediction = parameters.method === "linear"
270
+ ? model.intercept + model.slope * xValue
271
+ : parameters.method === "polynomial"
272
+ ? polynomial.prediction
273
+ : model.fits.find(fit => fit.x === xValue).prediction;
274
+ if (parameters.method === "loess") return { prediction };
275
+ const leverage = parameters.method === "linear"
276
+ ? 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
277
+ : polynomial.leverage;
278
+ const standardError = model.residualStandardError * Math.sqrt(
279
+ leverage + (parameters.interval === "prediction" ? 1 : 0)
280
+ );
281
+ return { prediction, margin: model.critical * standardError };
282
+ }
@@ -0,0 +1,106 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+
3
+ export function requireRegressionField(field, label) {
4
+ if (typeof field !== "string" || field.length === 0) {
5
+ throw new TypeError(`${label} must be a non-empty string.`);
6
+ }
7
+ return field;
8
+ }
9
+
10
+ export function normalizeRegressionParameters({
11
+ method = "linear",
12
+ degree,
13
+ span,
14
+ confidence,
15
+ interval
16
+ } = {}) {
17
+ if (!["linear", "polynomial", "loess"].includes(method)) {
18
+ throw new Error(`Unsupported regression method "${method}".`);
19
+ }
20
+ if (method === "loess") {
21
+ if (degree !== undefined) {
22
+ throw new Error("Regression degree requires the polynomial method.");
23
+ }
24
+ if (confidence !== undefined || interval !== undefined) {
25
+ throw new Error("LOESS regression does not support confidence intervals.");
26
+ }
27
+ const resolvedSpan = span ?? 0.75;
28
+ if (!Number.isFinite(resolvedSpan) || resolvedSpan <= 0 || resolvedSpan > 1) {
29
+ throw new RangeError(
30
+ "Regression LOESS span must be greater than zero and at most one."
31
+ );
32
+ }
33
+ return cloneAndFreeze({ method, span: resolvedSpan });
34
+ }
35
+ if (span !== undefined) {
36
+ throw new Error("Regression span requires the loess method.");
37
+ }
38
+ const resolvedConfidence = confidence ?? 0.95;
39
+ if (
40
+ !Number.isFinite(resolvedConfidence) ||
41
+ resolvedConfidence <= 0 ||
42
+ resolvedConfidence >= 1
43
+ ) {
44
+ throw new RangeError("Regression confidence must be between 0 and 1.");
45
+ }
46
+ const resolvedInterval = interval ?? "mean";
47
+ if (!["mean", "prediction"].includes(resolvedInterval)) {
48
+ throw new Error(`Unsupported regression interval "${resolvedInterval}".`);
49
+ }
50
+ if (method === "polynomial") {
51
+ const resolvedDegree = degree ?? 2;
52
+ if (!Number.isInteger(resolvedDegree) || resolvedDegree < 1) {
53
+ throw new RangeError(
54
+ "Regression polynomial degree must be a positive integer."
55
+ );
56
+ }
57
+ return cloneAndFreeze({
58
+ method,
59
+ degree: resolvedDegree,
60
+ confidence: resolvedConfidence,
61
+ interval: resolvedInterval
62
+ });
63
+ }
64
+ if (degree !== undefined) {
65
+ throw new Error("Regression degree requires the polynomial method.");
66
+ }
67
+ return cloneAndFreeze({
68
+ method,
69
+ confidence: resolvedConfidence,
70
+ interval: resolvedInterval
71
+ });
72
+ }
73
+
74
+ export function validateRegressionTransform(transform) {
75
+ const supported = [
76
+ "type", "method", "x", "y", "groupBy", "confidence", "interval",
77
+ "degree", "span"
78
+ ];
79
+ const unknown = Object.keys(transform).find(key => !supported.includes(key));
80
+ if (unknown !== undefined) {
81
+ throw new Error(`Unknown regression transform property "${unknown}".`);
82
+ }
83
+ if (transform.type !== "regression") {
84
+ throw new Error(`Unsupported regression transform "${transform.type}".`);
85
+ }
86
+ requireRegressionField(transform.x, "Regression x field");
87
+ requireRegressionField(transform.y, "Regression y field");
88
+ if (transform.groupBy !== undefined) {
89
+ requireRegressionField(transform.groupBy, "Regression groupBy field");
90
+ }
91
+ const normalized = normalizeRegressionParameters({
92
+ method: transform.method,
93
+ degree: transform.degree,
94
+ span: transform.span,
95
+ confidence: transform.confidence,
96
+ interval: transform.interval
97
+ });
98
+ if (normalized.method === "loess") return transform;
99
+ if (!Number.isFinite(transform.confidence)) {
100
+ throw new RangeError("Regression confidence must be between 0 and 1.");
101
+ }
102
+ if (transform.interval === undefined) {
103
+ throw new Error(`Unsupported regression interval "${transform.interval}".`);
104
+ }
105
+ return transform;
106
+ }
@@ -6,7 +6,7 @@ import {
6
6
  readQuantitativeField,
7
7
  readTemporalField,
8
8
  validateSemanticFieldType
9
- } from "./scales.js";
9
+ } from "./scales/index.js";
10
10
 
11
11
  const ENDPOINT_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
12
12
  const VALUE_CHANNELS = Object.freeze([
@@ -277,8 +277,10 @@ export function validateSequentialColorRange(value) {
277
277
  throw new TypeError("Sequential color range requires colors or a palette descriptor.");
278
278
  }
279
279
  const palette = normalizePalette(value.palette);
280
- if (palette.count !== undefined) {
281
- throw new Error("Continuous palette does not accept count.");
280
+ if (palette.count !== undefined && palette.count < 2) {
281
+ throw new RangeError(
282
+ "Sequential palette count must be an integer of at least 2."
283
+ );
282
284
  }
283
285
  return cloneAndFreeze({ palette });
284
286
  }