ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,4 +1,5 @@
1
1
  import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
+ import { validateGeneratedItemLimit } from "../core/validation.js";
2
3
 
3
4
  const COMMAND_KEYS = Object.freeze({
4
5
  M: Object.freeze(["op", "x", "y"]),
@@ -37,6 +38,7 @@ export function validatePathCommands(commands) {
37
38
  if (!Array.isArray(commands) || commands.length < 2) {
38
39
  throw new TypeError("path.commands must contain at least two commands.");
39
40
  }
41
+ validateGeneratedItemLimit(commands.length, "path.commands length");
40
42
  commands.forEach(validateCommand);
41
43
  if (commands[0].op !== "M") {
42
44
  throw new Error("path.commands must start with M.");
@@ -77,6 +79,10 @@ export function buildLinearPathCommands(points, { close = false } = {}) {
77
79
  if (typeof close !== "boolean") {
78
80
  throw new TypeError("Linear path close must be a boolean.");
79
81
  }
82
+ validateGeneratedItemLimit(
83
+ points.length + Number(close),
84
+ "Linear path command count"
85
+ );
80
86
  const commands = [
81
87
  { op: "M", x: points[0].x, y: points[0].y },
82
88
  ...points.slice(1).map(point => ({ op: "L", x: point.x, y: point.y }))
@@ -27,7 +27,7 @@ function validateRadius(value, label, frame) {
27
27
  return value;
28
28
  }
29
29
 
30
- function pointAt(frame, theta, radius) {
30
+ export function resolvePolarPoint(frame, theta, radius) {
31
31
  const radians = theta * Math.PI / 180;
32
32
  return {
33
33
  x: frame.centerX + radius * Math.sin(radians),
@@ -47,8 +47,8 @@ function arcCommands(frame, radius, startTheta, endTheta) {
47
47
  const endRadians = end * Math.PI / 180;
48
48
  const deltaRadians = endRadians - startRadians;
49
49
  const control = 4 / 3 * Math.tan(deltaRadians / 4);
50
- const from = pointAt(frame, start, radius);
51
- const to = pointAt(frame, end, radius);
50
+ const from = resolvePolarPoint(frame, start, radius);
51
+ const to = resolvePolarPoint(frame, end, radius);
52
52
  const fromDerivative = {
53
53
  x: radius * Math.cos(startRadians),
54
54
  y: radius * Math.sin(startRadians)
@@ -147,13 +147,13 @@ export function buildAnnularSectorCommands({
147
147
  const start = startTheta + direction * padAngle / 2;
148
148
  const end = endTheta - direction * padAngle / 2;
149
149
  const commands = [
150
- { op: "M", ...pointAt(frame, start, outerRadius) },
150
+ { op: "M", ...resolvePolarPoint(frame, start, outerRadius) },
151
151
  ...arcCommands(frame, outerRadius, start, end)
152
152
  ];
153
153
  if (innerRadius === 0) {
154
154
  commands.push({ op: "L", x: frame.centerX, y: frame.centerY });
155
155
  } else {
156
- commands.push({ op: "L", ...pointAt(frame, end, innerRadius) });
156
+ commands.push({ op: "L", ...resolvePolarPoint(frame, end, innerRadius) });
157
157
  commands.push(...arcCommands(frame, innerRadius, end, start));
158
158
  }
159
159
  commands.push({ op: "Z" });
@@ -4,6 +4,11 @@ import {
4
4
  readQuantitativeField
5
5
  } from "../scales/index.js";
6
6
  import { fitRegressionGroup, predictRegressionAt } from "./models.js";
7
+ import { requireFiniteResult } from "../numeric.js";
8
+ import {
9
+ validateGeneratedItemLimit,
10
+ validateWorkLimit
11
+ } from "../../core/validation.js";
7
12
  import {
8
13
  normalizeRegressionParameters,
9
14
  requireRegressionField
@@ -41,16 +46,34 @@ export function deriveRegression(values, {
41
46
  readQuantitativeField(values, y);
42
47
  if (groupBy !== undefined) readNominalField(values, groupBy);
43
48
 
44
- const groups = groupBy === undefined
45
- ? [undefined]
46
- : [...new Set(values.map(row => row[groupBy]))];
49
+ const groupedRows = new Map();
50
+ if (groupBy === undefined) {
51
+ groupedRows.set(undefined, values);
52
+ } else {
53
+ for (const row of values) {
54
+ const group = row[groupBy];
55
+ const rows = groupedRows.get(group) ?? [];
56
+ rows.push(row);
57
+ groupedRows.set(group, rows);
58
+ }
59
+ }
60
+ const groups = [...groupedRows.keys()];
47
61
  const models = [];
48
62
  const rows = [];
63
+ let work = 0;
49
64
 
50
65
  for (const group of groups) {
51
- const groupRows = groupBy === undefined
52
- ? values
53
- : values.filter(row => row[groupBy] === group);
66
+ const groupRows = groupedRows.get(group);
67
+ const xCount = new Set(groupRows.map(row => row[x])).size;
68
+ validateGeneratedItemLimit(rows.length + xCount, "Regression generated row count");
69
+ if (parameters.method === "polynomial") {
70
+ const size = parameters.degree + 1;
71
+ work += groupRows.length * size ** 2 + size ** 3;
72
+ } else if (parameters.method === "loess") {
73
+ work += groupRows.length * xCount *
74
+ Math.ceil(Math.log2(groupRows.length + 1));
75
+ }
76
+ validateWorkLimit(work, "Regression computation");
54
77
  const model = fitRegressionGroup(groupRows, {
55
78
  x,
56
79
  y,
@@ -63,13 +86,29 @@ export function deriveRegression(values, {
63
86
 
64
87
  for (const xValue of xValues) {
65
88
  const prediction = predictRegressionAt(model, xValue, parameters);
89
+ const fitted = requireFiniteResult(
90
+ prediction.prediction,
91
+ `Regression group "${group === undefined ? "all" : String(group)}" prediction`
92
+ );
93
+ const lower = parameters.method === "loess"
94
+ ? undefined
95
+ : requireFiniteResult(
96
+ fitted - prediction.margin,
97
+ `Regression group "${group === undefined ? "all" : String(group)}" lower interval`
98
+ );
99
+ const upper = parameters.method === "loess"
100
+ ? undefined
101
+ : requireFiniteResult(
102
+ fitted + prediction.margin,
103
+ `Regression group "${group === undefined ? "all" : String(group)}" upper interval`
104
+ );
66
105
  rows.push({
67
106
  ...(groupBy === undefined ? {} : { [groupBy]: group }),
68
107
  [x]: xValue,
69
- [y]: prediction.prediction,
108
+ [y]: fitted,
70
109
  ...(parameters.method === "loess" ? {} : {
71
- [REGRESSION_LOWER_FIELD]: prediction.prediction - prediction.margin,
72
- [REGRESSION_UPPER_FIELD]: prediction.prediction + prediction.margin
110
+ [REGRESSION_LOWER_FIELD]: lower,
111
+ [REGRESSION_UPPER_FIELD]: upper
73
112
  })
74
113
  });
75
114
  }
@@ -1,15 +1,75 @@
1
1
  import { studentTCriticalValue } from "../statistics/studentT.js";
2
+ import {
3
+ maximumMagnitude,
4
+ requireFiniteResult,
5
+ restoreFiniteScale,
6
+ stableFiniteMean,
7
+ stableFiniteSquareSum
8
+ } from "../numeric.js";
9
+
10
+ function restoreFiniteRatio(value, numeratorScale, denominatorScale, label) {
11
+ if (value === 0) return 0;
12
+ return requireFiniteResult(
13
+ Math.sign(value) * Math.exp(
14
+ Math.log(Math.abs(value)) +
15
+ Math.log(numeratorScale) -
16
+ Math.log(denominatorScale)
17
+ ),
18
+ label
19
+ );
20
+ }
21
+
22
+ function normalizedOffset(value, center, scale, byDifference = true) {
23
+ return byDifference
24
+ ? (value - center) / scale
25
+ : value / scale - center / scale;
26
+ }
27
+
28
+ function normalizedCoordinates(values, center) {
29
+ const differences = values.map(value => value - center);
30
+ const byDifference = differences.every(Number.isFinite);
31
+ const scale = byDifference
32
+ ? maximumMagnitude(differences)
33
+ : Math.max(maximumMagnitude(values), Math.abs(center));
34
+ return [
35
+ values.map(value => normalizedOffset(value, center, scale, byDifference)),
36
+ scale,
37
+ byDifference
38
+ ];
39
+ }
40
+
41
+ function normalizedLinearX(model, value) {
42
+ return normalizedOffset(
43
+ value, model.meanX, model.xScale, model.normalizedXByDifference !== false
44
+ );
45
+ }
46
+
47
+ function linearPrediction(model, xValue) {
48
+ if (model.normalizedSxx === undefined) {
49
+ return model.intercept + model.slope * xValue;
50
+ }
51
+ return restoreFiniteScale(
52
+ model.normalizedMeanY +
53
+ model.normalizedResponseSlope * normalizedLinearX(model, xValue),
54
+ model.responseScale,
55
+ "Linear regression prediction"
56
+ );
57
+ }
2
58
 
3
59
  function fitLinearGroup(rows, { x, y, group, confidence }) {
4
60
  const count = rows.length;
5
61
  const groupLabel = group === undefined ? "all" : String(group);
62
+ const label = `Regression group "${groupLabel}"`;
6
63
  if (count < 3) {
7
64
  throw new Error(
8
- `Regression group "${groupLabel}" requires at least three rows.`
65
+ `${label} requires at least three rows.`
9
66
  );
10
67
  }
11
- const meanX = rows.reduce((sum, row) => sum + row[x], 0) / count;
12
- const meanY = rows.reduce((sum, row) => sum + row[y], 0) / count;
68
+ const xValues = rows.map(row => row[x]);
69
+ const yValues = rows.map(row => row[y]);
70
+ const ordinaryYTotal = yValues.reduce((sum, value) => sum + value, 0);
71
+ const meanX = stableFiniteMean(xValues, "Linear regression x mean");
72
+ const meanY = stableFiniteMean(yValues, "Linear regression y mean");
13
73
  let sxx = 0;
14
74
  let sxy = 0;
15
75
  for (const row of rows) {
@@ -19,28 +79,81 @@ function fitLinearGroup(rows, { x, y, group, confidence }) {
19
79
  }
20
80
  if (sxx === 0) {
21
81
  throw new Error(
22
- `Regression group "${groupLabel}" requires varying x values.`
82
+ `${label} requires varying x values.`
83
+ );
84
+ }
85
+ const stableLinear = !Number.isFinite(ordinaryYTotal) ||
86
+ !Number.isFinite(sxx) || !Number.isFinite(sxy);
87
+ let normalizedXByDifference = true;
88
+ let xScale;
89
+ let responseScale;
90
+ let normalizedMeanY;
91
+ let normalizedResponseSlope;
92
+ let normalizedSxx;
93
+ if (stableLinear) {
94
+ let normalizedX;
95
+ [normalizedX, xScale, normalizedXByDifference] =
96
+ normalizedCoordinates(xValues, meanX);
97
+ xScale ||= 1;
98
+ responseScale = maximumMagnitude(yValues) || 1;
99
+ normalizedMeanY = meanY / responseScale;
100
+ normalizedSxx = normalizedX.reduce(
101
+ (sum, value) => sum + value ** 2,
102
+ 0
103
+ );
104
+ if (normalizedSxx === 0) {
105
+ throw new Error(`${label} requires varying x values.`);
106
+ }
107
+ normalizedResponseSlope = rows.reduce(
108
+ (sum, row, index) => sum + normalizedX[index] * (
109
+ row[y] / responseScale - normalizedMeanY
110
+ ),
111
+ 0
112
+ ) / normalizedSxx;
113
+ }
114
+ const slope = stableLinear
115
+ ? restoreFiniteRatio(
116
+ normalizedResponseSlope, responseScale, xScale, `${label} slope`
117
+ )
118
+ : requireFiniteResult(sxy / sxx, `${label} slope`);
119
+ let intercept = meanY - slope * meanX;
120
+ if (!Number.isFinite(intercept) && stableLinear) {
121
+ intercept = restoreFiniteScale(
122
+ normalizedMeanY + normalizedResponseSlope * normalizedOffset(
123
+ 0, meanX, xScale, normalizedXByDifference
124
+ ),
125
+ responseScale,
126
+ `${label} intercept`
23
127
  );
24
128
  }
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);
129
+ requireFiniteResult(intercept, `${label} intercept`);
31
130
  const degreesOfFreedom = count - 2;
32
- return {
131
+ const model = {
33
132
  count,
34
133
  degreesOfFreedom,
35
134
  meanX,
36
135
  meanY,
37
- sxx,
38
136
  slope,
39
- intercept,
137
+ intercept
138
+ };
139
+ if (Number.isFinite(sxx)) model.sxx = sxx;
140
+ if (stableLinear) Object.assign(model, {
141
+ xScale,
142
+ responseScale,
143
+ normalizedMeanY,
144
+ normalizedResponseSlope,
145
+ normalizedSxx
146
+ });
147
+ if (!normalizedXByDifference) model.normalizedXByDifference = false;
148
+ const residualSumSquares = stableFiniteSquareSum(
149
+ rows.map(row => row[y] - linearPrediction(model, row[x])),
150
+ `${label} residual sum of squares`
151
+ );
152
+ return Object.assign(model, {
40
153
  residualSumSquares,
41
154
  residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
42
155
  critical: studentTCriticalValue(confidence, degreesOfFreedom)
43
- };
156
+ });
44
157
  }
45
158
 
46
159
  function solveLinearSystem(matrix, vector) {
@@ -94,11 +207,25 @@ function binomial(n, k) {
94
207
  }
95
208
 
96
209
  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,
210
+ return coefficients.map((_, degree) => {
211
+ const sum = stable => coefficients.reduce(
212
+ (total, coefficient, power) => power < degree
213
+ ? total
214
+ : total + coefficient * binomial(power, degree) *
215
+ (stable ? -center / scale : -center) ** (power - degree) /
216
+ scale ** (stable ? degree : power),
217
+ 0
218
+ );
219
+ const direct = sum(false);
220
+ return requireFiniteResult(Number.isFinite(direct) ? direct :
221
+ sum(true), `Polynomial regression coefficient ${degree}`);
222
+ });
223
+ }
224
+
225
+ function polynomialResponse(design, values, size, scale = 1) {
226
+ return Array.from({ length: size }, (_, column) => design.reduce(
227
+ (sum, basis, index) =>
228
+ sum + basis[column] * (values[index] / scale),
102
229
  0
103
230
  ));
104
231
  }
@@ -107,29 +234,30 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
107
234
  const count = rows.length;
108
235
  const parameterCount = degree + 1;
109
236
  const groupLabel = group === undefined ? "all" : String(group);
237
+ const label = `Polynomial regression group "${groupLabel}"`;
110
238
  if (
111
239
  count < degree + 2 ||
112
240
  new Set(rows.map(row => row[x])).size < parameterCount
113
241
  ) {
114
242
  throw new Error(
115
- `Polynomial regression group "${groupLabel}" requires at least ` +
243
+ `${label} requires at least ` +
116
244
  `${degree + 2} rows and ${parameterCount} distinct x values.`
117
245
  );
118
246
  }
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)) {
247
+ const xValues = rows.map(row => row[x]);
248
+ const yValues = rows.map(row => row[y]);
249
+ const center = stableFiniteMean(xValues, "Polynomial regression x center");
250
+ const [normalizedX, scale, normalizedXByDifference] =
251
+ normalizedCoordinates(xValues, center);
252
+ if (!(scale > 0) || !Number.isFinite(scale)) {
122
253
  throw new Error(
123
- `Regression group "${groupLabel}" requires varying x values.`
254
+ `Regression group "${groupLabel}" requires finite varying x values.`
124
255
  );
125
256
  }
126
- const design = rows.map(row => {
127
- const normalized = (row[x] - center) / scale;
128
- return Array.from(
257
+ const design = normalizedX.map(normalized => Array.from(
129
258
  { length: parameterCount },
130
259
  (_, power) => normalized ** power
131
- );
132
- });
260
+ ));
133
261
  const normal = Array.from({ length: parameterCount }, (_, row) =>
134
262
  Array.from({ length: parameterCount }, (_, column) =>
135
263
  design.reduce(
@@ -138,22 +266,36 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
138
266
  )
139
267
  )
140
268
  );
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);
269
+ const response = polynomialResponse(design, yValues, parameterCount);
270
+ const stableResponse = response.some(value => !Number.isFinite(value));
271
+ const responseScale = stableResponse ? maximumMagnitude(yValues) || 1 : undefined;
272
+ const resolvedResponse = stableResponse
273
+ ? polynomialResponse(design, yValues, parameterCount, responseScale)
274
+ : response;
275
+ const scaledCoefficients = solveLinearSystem(normal, resolvedResponse);
276
+ const normalizedCoefficients = stableResponse
277
+ ? scaledCoefficients.map((coefficient, index) => restoreFiniteScale(
278
+ coefficient,
279
+ responseScale,
280
+ `${label} coefficient ${index}`
281
+ ))
282
+ : scaledCoefficients;
148
283
  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
284
+ const fitted = design.map(basis => stableResponse
285
+ ? restoreFiniteScale(
286
+ dot(basis, scaledCoefficients),
287
+ responseScale,
288
+ `${label} prediction`
289
+ )
290
+ : dot(basis, normalizedCoefficients)
291
+ );
292
+ const residualSumSquares = stableFiniteSquareSum(
293
+ fitted.map((value, index) => rows[index][y] - value),
294
+ `${label} residual sum of squares`
153
295
  );
154
296
  const degreesOfFreedom = count - parameterCount;
155
297
  const residualVariance = residualSumSquares / degreesOfFreedom;
156
- return {
298
+ const model = {
157
299
  count,
158
300
  degreesOfFreedom,
159
301
  degree,
@@ -170,36 +312,76 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
170
312
  residualStandardError: Math.sqrt(residualVariance),
171
313
  critical: studentTCriticalValue(confidence, degreesOfFreedom)
172
314
  };
315
+ if (!normalizedXByDifference) model.normalizedXByDifference = false;
316
+ if (stableResponse) Object.assign(model, { responseScale, scaledCoefficients });
317
+ return model;
173
318
  }
174
319
 
175
320
  function evaluatePolynomial(model, xValue) {
176
- const normalized = (xValue - model.center) / model.scale;
321
+ const normalized = normalizedOffset(
322
+ xValue, model.center, model.scale, model.normalizedXByDifference !== false
323
+ );
177
324
  const basis = model.normalizedCoefficients.map(
178
325
  (_, power) => normalized ** power
179
326
  );
180
327
  return {
181
- prediction: dot(basis, model.normalizedCoefficients),
328
+ prediction: model.responseScale === undefined
329
+ ? dot(basis, model.normalizedCoefficients)
330
+ : restoreFiniteScale(
331
+ dot(basis, model.scaledCoefficients),
332
+ model.responseScale,
333
+ "Polynomial regression prediction"
334
+ ),
182
335
  leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
183
336
  };
184
337
  }
185
338
 
339
+ function weightedPrediction(items, x, y, xValue, xScale = 1, yScale = 1) {
340
+ const difference = item => item.row[x] / xScale - xValue / xScale;
341
+ let totalWeight = 0;
342
+ let meanDifference = 0;
343
+ let meanY = 0;
344
+ for (const item of items) {
345
+ totalWeight += item.weight;
346
+ meanDifference += item.weight * difference(item);
347
+ meanY += item.weight * item.row[y] / yScale;
348
+ }
349
+ meanDifference /= totalWeight;
350
+ meanY /= totalWeight;
351
+ let variance = 0;
352
+ let covariance = 0;
353
+ for (const item of items) {
354
+ const centered = difference(item) - meanDifference;
355
+ variance += item.weight * centered ** 2;
356
+ covariance += item.weight * centered * (item.row[y] / yScale - meanY);
357
+ }
358
+ return meanY - (variance === 0 ? 0 : covariance / variance) * meanDifference;
359
+ }
360
+
186
361
  function fitLoessGroup(rows, { x, y, group, span }) {
187
362
  const groupLabel = group === undefined ? "all" : String(group);
363
+ const label = `LOESS regression group "${groupLabel}"`;
188
364
  if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
189
365
  throw new Error(
190
- `LOESS regression group "${groupLabel}" requires at least two rows ` +
366
+ `${label} requires at least two rows ` +
191
367
  "and varying x values."
192
368
  );
193
369
  }
194
370
  const neighborCount = Math.max(2, Math.ceil(span * rows.length));
195
371
  const xValues = [...new Set(rows.map(row => row[x]))]
196
372
  .sort((left, right) => left - right);
373
+ const xScale = maximumMagnitude(rows.map(row => row[x])) || 1;
374
+ const yScale = maximumMagnitude(rows.map(row => row[y])) || 1;
197
375
  const fits = xValues.map(xValue => {
376
+ const rawDistances = rows.map(row => Math.abs(row[x] - xValue));
377
+ const scaledDistances = rawDistances.some(value => !Number.isFinite(value));
198
378
  const neighbors = rows
199
379
  .map((row, index) => ({
200
380
  row,
201
381
  index,
202
- distance: Math.abs(row[x] - xValue)
382
+ distance: scaledDistances
383
+ ? Math.abs(row[x] / xScale - xValue / xScale)
384
+ : rawDistances[index]
203
385
  }))
204
386
  .sort((left, right) =>
205
387
  left.distance - right.distance || left.index - right.index
@@ -212,29 +394,16 @@ function fitLoessGroup(rows, { x, y, group, span }) {
212
394
  ? 1
213
395
  : (1 - (neighbor.distance / radius) ** 3) ** 3
214
396
  }));
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;
397
+ const ordinary = weightedPrediction(weighted, x, y, xValue);
235
398
  return {
236
399
  x: xValue,
237
- prediction: meanY - slope * meanDifference,
400
+ prediction: Number.isFinite(ordinary)
401
+ ? ordinary
402
+ : restoreFiniteScale(
403
+ weightedPrediction(weighted, x, y, xValue, xScale, yScale),
404
+ yScale,
405
+ `${label} prediction`
406
+ ),
238
407
  neighborIndices: weighted.map(item => item.index)
239
408
  };
240
409
  });
@@ -267,16 +436,26 @@ export function predictRegressionAt(model, xValue, parameters) {
267
436
  ? evaluatePolynomial(model, xValue)
268
437
  : undefined;
269
438
  const prediction = parameters.method === "linear"
270
- ? model.intercept + model.slope * xValue
439
+ ? linearPrediction(model, xValue)
271
440
  : parameters.method === "polynomial"
272
441
  ? polynomial.prediction
273
442
  : model.fits.find(fit => fit.x === xValue).prediction;
274
443
  if (parameters.method === "loess") return { prediction };
275
444
  const leverage = parameters.method === "linear"
276
- ? 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
445
+ ? model.normalizedSxx !== undefined
446
+ ? 1 / model.count + (
447
+ normalizedLinearX(model, xValue)
448
+ ) ** 2 / model.normalizedSxx
449
+ : 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
277
450
  : polynomial.leverage;
278
451
  const standardError = model.residualStandardError * Math.sqrt(
279
452
  leverage + (parameters.interval === "prediction" ? 1 : 0)
280
453
  );
281
- return { prediction, margin: model.critical * standardError };
454
+ return {
455
+ prediction: requireFiniteResult(prediction, "Regression prediction"),
456
+ margin: requireFiniteResult(
457
+ model.critical * standardError,
458
+ "Regression interval margin"
459
+ )
460
+ };
282
461
  }
@@ -1,4 +1,7 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { validateGeneratedItemLimit } from "../../core/validation.js";
3
+
4
+ const MAX_POLYNOMIAL_DEGREE = 32;
2
5
 
3
6
  export function requireRegressionField(field, label) {
4
7
  if (typeof field !== "string" || field.length === 0) {
@@ -54,6 +57,11 @@ export function normalizeRegressionParameters({
54
57
  "Regression polynomial degree must be a positive integer."
55
58
  );
56
59
  }
60
+ validateGeneratedItemLimit(
61
+ resolvedDegree,
62
+ "Regression polynomial degree",
63
+ MAX_POLYNOMIAL_DEGREE
64
+ );
57
65
  return cloneAndFreeze({
58
66
  method,
59
67
  degree: resolvedDegree,