insomni-plot 0.1.0-alpha.0

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 (242) hide show
  1. package/LICENSE.md +674 -0
  2. package/README.md +81 -0
  3. package/dist/core.d.mts +340 -0
  4. package/dist/core.mjs +1047 -0
  5. package/dist/index.d.mts +3426 -0
  6. package/dist/index.mjs +12762 -0
  7. package/dist/interactions-DEFL_F4E.mjs +5395 -0
  8. package/dist/range-presets-CzECsu3V.d.mts +1523 -0
  9. package/package.json +34 -0
  10. package/src/annotations.d.ts +121 -0
  11. package/src/annotations.ts +438 -0
  12. package/src/axis.d.ts +184 -0
  13. package/src/axis.test.ts +131 -0
  14. package/src/axis.ts +765 -0
  15. package/src/colorbar.d.ts +69 -0
  16. package/src/colorbar.ts +294 -0
  17. package/src/colors.d.ts +57 -0
  18. package/src/colors.test.ts +28 -0
  19. package/src/colors.ts +486 -0
  20. package/src/core.ts +299 -0
  21. package/src/format.d.ts +54 -0
  22. package/src/format.ts +138 -0
  23. package/src/grammar/accessibility.d.ts +147 -0
  24. package/src/grammar/accessibility.test.ts +199 -0
  25. package/src/grammar/accessibility.ts +443 -0
  26. package/src/grammar/aes.d.ts +35 -0
  27. package/src/grammar/aes.test.ts +75 -0
  28. package/src/grammar/aes.ts +120 -0
  29. package/src/grammar/annotations.d.ts +86 -0
  30. package/src/grammar/annotations.test.ts +68 -0
  31. package/src/grammar/annotations.ts +336 -0
  32. package/src/grammar/attach-brush.d.ts +44 -0
  33. package/src/grammar/attach-brush.test.ts +214 -0
  34. package/src/grammar/attach-brush.ts +111 -0
  35. package/src/grammar/attach-presets.d.ts +33 -0
  36. package/src/grammar/attach-presets.test.ts +106 -0
  37. package/src/grammar/attach-presets.ts +215 -0
  38. package/src/grammar/chart.d.ts +952 -0
  39. package/src/grammar/chart.test.ts +118 -0
  40. package/src/grammar/chart.ts +1172 -0
  41. package/src/grammar/color-utils.d.ts +29 -0
  42. package/src/grammar/color-utils.test.ts +53 -0
  43. package/src/grammar/color-utils.ts +66 -0
  44. package/src/grammar/constants.d.ts +45 -0
  45. package/src/grammar/constants.ts +61 -0
  46. package/src/grammar/coord.d.ts +183 -0
  47. package/src/grammar/coord.test.ts +355 -0
  48. package/src/grammar/coord.ts +619 -0
  49. package/src/grammar/data/pivot.d.ts +57 -0
  50. package/src/grammar/data/pivot.ts +107 -0
  51. package/src/grammar/emphasis-driver.d.ts +69 -0
  52. package/src/grammar/emphasis-driver.test.ts +199 -0
  53. package/src/grammar/emphasis-driver.ts +205 -0
  54. package/src/grammar/equality.d.ts +3 -0
  55. package/src/grammar/equality.ts +40 -0
  56. package/src/grammar/facet.d.ts +63 -0
  57. package/src/grammar/facet.test.ts +60 -0
  58. package/src/grammar/facet.ts +175 -0
  59. package/src/grammar/geoms/_categorical.d.ts +94 -0
  60. package/src/grammar/geoms/_categorical.ts +0 -0
  61. package/src/grammar/geoms/_distribution.d.ts +52 -0
  62. package/src/grammar/geoms/_distribution.ts +125 -0
  63. package/src/grammar/geoms/_mark.d.ts +69 -0
  64. package/src/grammar/geoms/_mark.ts +136 -0
  65. package/src/grammar/geoms/_shape.d.ts +41 -0
  66. package/src/grammar/geoms/_shape.ts +74 -0
  67. package/src/grammar/geoms/aggregate.d.ts +95 -0
  68. package/src/grammar/geoms/aggregate.test.ts +554 -0
  69. package/src/grammar/geoms/aggregate.ts +840 -0
  70. package/src/grammar/geoms/area.d.ts +32 -0
  71. package/src/grammar/geoms/area.test.ts +165 -0
  72. package/src/grammar/geoms/area.ts +578 -0
  73. package/src/grammar/geoms/band.d.ts +27 -0
  74. package/src/grammar/geoms/band.test.ts +57 -0
  75. package/src/grammar/geoms/band.ts +126 -0
  76. package/src/grammar/geoms/bar.d.ts +56 -0
  77. package/src/grammar/geoms/bar.test.ts +367 -0
  78. package/src/grammar/geoms/bar.ts +1054 -0
  79. package/src/grammar/geoms/boxplot.d.ts +129 -0
  80. package/src/grammar/geoms/boxplot.test.ts +299 -0
  81. package/src/grammar/geoms/boxplot.ts +834 -0
  82. package/src/grammar/geoms/connected-scatter.d.ts +27 -0
  83. package/src/grammar/geoms/connected-scatter.test.ts +157 -0
  84. package/src/grammar/geoms/connected-scatter.ts +63 -0
  85. package/src/grammar/geoms/emphasis.d.ts +76 -0
  86. package/src/grammar/geoms/emphasis.test.ts +135 -0
  87. package/src/grammar/geoms/emphasis.ts +162 -0
  88. package/src/grammar/geoms/histogram.d.ts +75 -0
  89. package/src/grammar/geoms/histogram.test.ts +262 -0
  90. package/src/grammar/geoms/histogram.ts +740 -0
  91. package/src/grammar/geoms/index.d.ts +20 -0
  92. package/src/grammar/geoms/index.ts +77 -0
  93. package/src/grammar/geoms/interval.d.ts +31 -0
  94. package/src/grammar/geoms/interval.test.ts +154 -0
  95. package/src/grammar/geoms/interval.ts +342 -0
  96. package/src/grammar/geoms/line.d.ts +38 -0
  97. package/src/grammar/geoms/line.test.ts +247 -0
  98. package/src/grammar/geoms/line.ts +659 -0
  99. package/src/grammar/geoms/point.d.ts +57 -0
  100. package/src/grammar/geoms/point.test.ts +163 -0
  101. package/src/grammar/geoms/point.ts +545 -0
  102. package/src/grammar/geoms/polar.test.ts +216 -0
  103. package/src/grammar/geoms/ribbon.d.ts +21 -0
  104. package/src/grammar/geoms/ribbon.test.ts +170 -0
  105. package/src/grammar/geoms/ribbon.ts +87 -0
  106. package/src/grammar/geoms/ridgeline.d.ts +89 -0
  107. package/src/grammar/geoms/ridgeline.test.ts +247 -0
  108. package/src/grammar/geoms/ridgeline.ts +1164 -0
  109. package/src/grammar/geoms/rolling.d.ts +43 -0
  110. package/src/grammar/geoms/rolling.test.ts +217 -0
  111. package/src/grammar/geoms/rolling.ts +387 -0
  112. package/src/grammar/geoms/rug.d.ts +28 -0
  113. package/src/grammar/geoms/rug.test.ts +126 -0
  114. package/src/grammar/geoms/rug.ts +214 -0
  115. package/src/grammar/geoms/rule.d.ts +23 -0
  116. package/src/grammar/geoms/rule.test.ts +69 -0
  117. package/src/grammar/geoms/rule.ts +212 -0
  118. package/src/grammar/geoms/smooth.d.ts +54 -0
  119. package/src/grammar/geoms/smooth.test.ts +78 -0
  120. package/src/grammar/geoms/smooth.ts +337 -0
  121. package/src/grammar/geoms/text.d.ts +29 -0
  122. package/src/grammar/geoms/text.test.ts +64 -0
  123. package/src/grammar/geoms/text.ts +234 -0
  124. package/src/grammar/geoms/tile.d.ts +61 -0
  125. package/src/grammar/geoms/tile.test.ts +157 -0
  126. package/src/grammar/geoms/tile.ts +621 -0
  127. package/src/grammar/geoms/types.d.ts +319 -0
  128. package/src/grammar/geoms/types.ts +362 -0
  129. package/src/grammar/geoms/violin.d.ts +85 -0
  130. package/src/grammar/geoms/violin.test.ts +187 -0
  131. package/src/grammar/geoms/violin.ts +672 -0
  132. package/src/grammar/index.d.ts +22 -0
  133. package/src/grammar/index.ts +269 -0
  134. package/src/grammar/interactions/_disposable.d.ts +5 -0
  135. package/src/grammar/interactions/_disposable.ts +23 -0
  136. package/src/grammar/interactions/_z.d.ts +4 -0
  137. package/src/grammar/interactions/_z.ts +16 -0
  138. package/src/grammar/interactions/brush-selection.test.ts +262 -0
  139. package/src/grammar/interactions/brush.d.ts +63 -0
  140. package/src/grammar/interactions/brush.test.ts +483 -0
  141. package/src/grammar/interactions/brush.ts +452 -0
  142. package/src/grammar/interactions/crosshair.d.ts +19 -0
  143. package/src/grammar/interactions/crosshair.test.ts +127 -0
  144. package/src/grammar/interactions/crosshair.ts +76 -0
  145. package/src/grammar/interactions/hit-layer.d.ts +64 -0
  146. package/src/grammar/interactions/hit-layer.ts +246 -0
  147. package/src/grammar/interactions/legend.d.ts +19 -0
  148. package/src/grammar/interactions/legend.ts +101 -0
  149. package/src/grammar/interactions/menu.d.ts +93 -0
  150. package/src/grammar/interactions/menu.test.ts +373 -0
  151. package/src/grammar/interactions/menu.ts +342 -0
  152. package/src/grammar/interactions/selection.d.ts +25 -0
  153. package/src/grammar/interactions/selection.test.ts +289 -0
  154. package/src/grammar/interactions/selection.ts +142 -0
  155. package/src/grammar/interactions/series-readout.d.ts +91 -0
  156. package/src/grammar/interactions/series-readout.test.ts +668 -0
  157. package/src/grammar/interactions/series-readout.ts +422 -0
  158. package/src/grammar/interactions/series-snap.d.ts +70 -0
  159. package/src/grammar/interactions/series-snap.test.ts +214 -0
  160. package/src/grammar/interactions/series-snap.ts +218 -0
  161. package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
  162. package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
  163. package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
  164. package/src/grammar/interactions/tooltip.d.ts +140 -0
  165. package/src/grammar/interactions/tooltip.test.ts +406 -0
  166. package/src/grammar/interactions/tooltip.ts +622 -0
  167. package/src/grammar/interactions/transitions.d.ts +34 -0
  168. package/src/grammar/interactions/transitions.test.ts +172 -0
  169. package/src/grammar/interactions/transitions.ts +160 -0
  170. package/src/grammar/layout.d.ts +68 -0
  171. package/src/grammar/layout.ts +186 -0
  172. package/src/grammar/legend-merge.test.ts +332 -0
  173. package/src/grammar/mount.d.ts +78 -0
  174. package/src/grammar/mount.test.ts +479 -0
  175. package/src/grammar/mount.ts +2112 -0
  176. package/src/grammar/palettes.d.ts +54 -0
  177. package/src/grammar/palettes.test.ts +80 -0
  178. package/src/grammar/palettes.ts +167 -0
  179. package/src/grammar/pan-zoom.test.ts +398 -0
  180. package/src/grammar/phylo.d.ts +65 -0
  181. package/src/grammar/phylo.test.ts +59 -0
  182. package/src/grammar/phylo.ts +112 -0
  183. package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
  184. package/src/grammar/pipeline.d.ts +158 -0
  185. package/src/grammar/pipeline.test.ts +463 -0
  186. package/src/grammar/pipeline.ts +1233 -0
  187. package/src/grammar/profiling.d.ts +8 -0
  188. package/src/grammar/profiling.ts +24 -0
  189. package/src/grammar/scales.d.ts +188 -0
  190. package/src/grammar/scales.test.ts +181 -0
  191. package/src/grammar/scales.ts +800 -0
  192. package/src/grammar/svg.d.ts +3 -0
  193. package/src/grammar/svg.ts +39 -0
  194. package/src/grammar/theme.d.ts +261 -0
  195. package/src/grammar/theme.test.ts +105 -0
  196. package/src/grammar/theme.ts +490 -0
  197. package/src/heatmap/cpu.ts +109 -0
  198. package/src/heatmap/gpu.ts +565 -0
  199. package/src/heatmap/types.ts +177 -0
  200. package/src/heatmap.browser.test.ts +308 -0
  201. package/src/heatmap.test.ts +320 -0
  202. package/src/heatmap.ts +123 -0
  203. package/src/index.d.ts +1 -0
  204. package/src/index.ts +8 -0
  205. package/src/interactions.d.ts +48 -0
  206. package/src/interactions.test.ts +226 -0
  207. package/src/interactions.ts +394 -0
  208. package/src/layout/box.d.ts +48 -0
  209. package/src/layout/box.test.ts +107 -0
  210. package/src/layout/box.ts +143 -0
  211. package/src/legend.d.ts +115 -0
  212. package/src/legend.ts +422 -0
  213. package/src/marks/curve.d.ts +43 -0
  214. package/src/marks/curve.ts +244 -0
  215. package/src/marks/stack.d.ts +53 -0
  216. package/src/marks/stack.ts +184 -0
  217. package/src/marks.d.ts +273 -0
  218. package/src/marks.test.ts +541 -0
  219. package/src/marks.ts +1292 -0
  220. package/src/navigator.test.ts +174 -0
  221. package/src/navigator.ts +393 -0
  222. package/src/range-presets.d.ts +113 -0
  223. package/src/range-presets.test.ts +345 -0
  224. package/src/range-presets.ts +349 -0
  225. package/src/scales.d.ts +98 -0
  226. package/src/scales.test.ts +103 -0
  227. package/src/scales.ts +695 -0
  228. package/src/stats/index.d.ts +200 -0
  229. package/src/stats/index.test.ts +349 -0
  230. package/src/stats/index.ts +740 -0
  231. package/src/stats/regression.d.ts +38 -0
  232. package/src/stats/regression.test.ts +56 -0
  233. package/src/stats/regression.ts +396 -0
  234. package/src/stats/rolling-window.d.ts +55 -0
  235. package/src/stats/rolling-window.test.ts +237 -0
  236. package/src/stats/rolling-window.ts +256 -0
  237. package/src/test-setup.ts +19 -0
  238. package/src/viewport/axis-state.d.ts +72 -0
  239. package/src/viewport/axis-state.ts +476 -0
  240. package/src/viewport.d.ts +170 -0
  241. package/src/viewport.test.ts +363 -0
  242. package/src/viewport.ts +510 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A fitted regression. `predict(x)` returns the mean-response estimate;
3
+ * `seMean(x)` returns the standard error of that estimate (for confidence
4
+ * bands). `summary` carries whatever fit-specific scalars are interesting
5
+ * (slope, intercept, coefs, r²) — exposed for callers that want to render
6
+ * an equation or stat label.
7
+ */
8
+ export interface RegressionFit {
9
+ readonly n: number;
10
+ predict(x: number): number;
11
+ /** Standard error of the mean response at `x`. Returns 0 when undefined. */
12
+ seMean(x: number): number;
13
+ readonly residualStdError: number;
14
+ readonly summary: Readonly<Record<string, number | readonly number[]>>;
15
+ }
16
+ export declare function linearFit(xs: readonly number[], ys: readonly number[]): RegressionFit | null;
17
+ export declare function polyFit(xs: readonly number[], ys: readonly number[], degree: number): RegressionFit | null;
18
+ export interface LoessOptions {
19
+ /** Fraction of the sample used in each local fit. Default `0.5`. */
20
+ span?: number;
21
+ /** Degree of local polynomial: `1` (default) or `2`. */
22
+ degree?: 1 | 2;
23
+ }
24
+ export declare function loessFit(xs: readonly number[], ys: readonly number[], options?: LoessOptions): RegressionFit | null;
25
+ export interface ConfidenceBandPoint {
26
+ x: number;
27
+ yhat: number;
28
+ lo: number;
29
+ hi: number;
30
+ }
31
+ export interface ConfidenceBandOptions {
32
+ samples?: number;
33
+ /** Confidence level. Default `0.95`. */
34
+ level?: number;
35
+ /** Override `[xmin, xmax]`. Defaults to data range. */
36
+ domain?: readonly [number, number];
37
+ }
38
+ export declare function confidenceBand(fit: RegressionFit, xs: readonly number[], options?: ConfidenceBandOptions): ConfidenceBandPoint[];
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+
3
+ import { confidenceBand, linearFit, loessFit, polyFit } from "./regression.ts";
4
+
5
+ describe("regression", () => {
6
+ test("linearFit recovers slope/intercept from a noiseless line", () => {
7
+ const xs = [0, 1, 2, 3, 4, 5];
8
+ const ys = xs.map((x) => 2 * x + 1);
9
+ const fit = linearFit(xs, ys)!;
10
+ expect(fit).not.toBeNull();
11
+ expect(fit.summary.slope).toBeCloseTo(2, 6);
12
+ expect(fit.summary.intercept).toBeCloseTo(1, 6);
13
+ expect(fit.predict(10)).toBeCloseTo(21, 6);
14
+ expect(fit.summary.r2).toBeCloseTo(1, 6);
15
+ });
16
+
17
+ test("linearFit returns null for fewer than 2 finite points", () => {
18
+ expect(linearFit([1], [1])).toBeNull();
19
+ expect(linearFit([1, NaN], [2, 3])).toBeNull();
20
+ });
21
+
22
+ test("polyFit recovers a quadratic", () => {
23
+ const xs = [-3, -2, -1, 0, 1, 2, 3];
24
+ const ys = xs.map((x) => 0.5 * x * x - x + 2);
25
+ const fit = polyFit(xs, ys, 2)!;
26
+ expect(fit).not.toBeNull();
27
+ expect(fit.predict(4)).toBeCloseTo(0.5 * 16 - 4 + 2, 4);
28
+ expect(fit.summary.r2).toBeCloseTo(1, 6);
29
+ });
30
+
31
+ test("loessFit produces stable predictions across the domain", () => {
32
+ const xs = Array.from({ length: 50 }, (_, i) => i / 49);
33
+ const ys = xs.map((x) => Math.sin(x * Math.PI));
34
+ const fit = loessFit(xs, ys, { span: 0.4 })!;
35
+ expect(fit).not.toBeNull();
36
+ // Smoother should track the underlying sine within a generous tolerance.
37
+ expect(fit.predict(0.25)).toBeCloseTo(Math.sin(0.25 * Math.PI), 1);
38
+ expect(fit.predict(0.75)).toBeCloseTo(Math.sin(0.75 * Math.PI), 1);
39
+ });
40
+
41
+ test("confidenceBand emits ordered samples whose interval brackets yhat", () => {
42
+ const xs = Array.from({ length: 30 }, (_, i) => i);
43
+ const ys = xs.map((x) => x * 0.3 + (x % 3 === 0 ? 0.4 : -0.2));
44
+ const fit = linearFit(xs, ys)!;
45
+ const band = confidenceBand(fit, xs, { samples: 10 });
46
+ expect(band).toHaveLength(10);
47
+ for (const p of band) {
48
+ expect(p.lo).toBeLessThanOrEqual(p.yhat);
49
+ expect(p.hi).toBeGreaterThanOrEqual(p.yhat);
50
+ }
51
+ // x is monotonic.
52
+ for (let i = 1; i < band.length; i++) {
53
+ expect(band[i]!.x).toBeGreaterThan(band[i - 1]!.x);
54
+ }
55
+ });
56
+ });
@@ -0,0 +1,396 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Regression
3
+ // ---------------------------------------------------------------------------
4
+ // Pure helpers for fitting smoothers and producing confidence bands. Used by
5
+ // the `smooth` geom; exported via `insomni-plot/core` so callers can fit
6
+ // once and feed the results into custom marks.
7
+
8
+ /**
9
+ * A fitted regression. `predict(x)` returns the mean-response estimate;
10
+ * `seMean(x)` returns the standard error of that estimate (for confidence
11
+ * bands). `summary` carries whatever fit-specific scalars are interesting
12
+ * (slope, intercept, coefs, r²) — exposed for callers that want to render
13
+ * an equation or stat label.
14
+ */
15
+ export interface RegressionFit {
16
+ readonly n: number;
17
+ predict(x: number): number;
18
+ /** Standard error of the mean response at `x`. Returns 0 when undefined. */
19
+ seMean(x: number): number;
20
+ readonly residualStdError: number;
21
+ readonly summary: Readonly<Record<string, number | readonly number[]>>;
22
+ }
23
+
24
+ interface NumericSample {
25
+ xs: number[];
26
+ ys: number[];
27
+ }
28
+
29
+ function filterFinite(xs: readonly number[], ys: readonly number[]): NumericSample {
30
+ const xo: number[] = [];
31
+ const yo: number[] = [];
32
+ const n = Math.min(xs.length, ys.length);
33
+ for (let i = 0; i < n; i++) {
34
+ const x = xs[i]!;
35
+ const y = ys[i]!;
36
+ if (Number.isFinite(x) && Number.isFinite(y)) {
37
+ xo.push(x);
38
+ yo.push(y);
39
+ }
40
+ }
41
+ return { xs: xo, ys: yo };
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // linearFit — ordinary least squares y = β₀ + β₁ x
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export function linearFit(xs: readonly number[], ys: readonly number[]): RegressionFit | null {
49
+ const sample = filterFinite(xs, ys);
50
+ const n = sample.xs.length;
51
+ if (n < 2) return null;
52
+
53
+ let sx = 0;
54
+ let sy = 0;
55
+ for (let i = 0; i < n; i++) {
56
+ sx += sample.xs[i]!;
57
+ sy += sample.ys[i]!;
58
+ }
59
+ const xbar = sx / n;
60
+ const ybar = sy / n;
61
+
62
+ let sxx = 0;
63
+ let sxy = 0;
64
+ let syy = 0;
65
+ for (let i = 0; i < n; i++) {
66
+ const dx = sample.xs[i]! - xbar;
67
+ const dy = sample.ys[i]! - ybar;
68
+ sxx += dx * dx;
69
+ sxy += dx * dy;
70
+ syy += dy * dy;
71
+ }
72
+
73
+ if (sxx === 0) return null;
74
+ const slope = sxy / sxx;
75
+ const intercept = ybar - slope * xbar;
76
+
77
+ // Residual standard error (unbiased) — divide by (n - 2) for two-parameter fit.
78
+ let ssr = 0;
79
+ for (let i = 0; i < n; i++) {
80
+ const r = sample.ys[i]! - (intercept + slope * sample.xs[i]!);
81
+ ssr += r * r;
82
+ }
83
+ const dof = Math.max(1, n - 2);
84
+ const sigma = Math.sqrt(ssr / dof);
85
+ const r2 = syy > 0 ? 1 - ssr / syy : 0;
86
+
87
+ return {
88
+ n,
89
+ predict: (x) => intercept + slope * x,
90
+ seMean: (x) => sigma * Math.sqrt(1 / n + ((x - xbar) * (x - xbar)) / sxx),
91
+ residualStdError: sigma,
92
+ summary: { slope, intercept, r2, xbar, sxx },
93
+ };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // polyFit — least-squares polynomial (degree ≥ 1)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /** Solve a small symmetric positive-definite system via Gauss-Jordan. */
101
+ function solveLinear(a: number[][], b: number[]): number[] | null {
102
+ const n = b.length;
103
+ const m: number[][] = a.map((row, i) => [...row, b[i]!]);
104
+ for (let i = 0; i < n; i++) {
105
+ // Partial pivot.
106
+ let pivot = i;
107
+ let pmax = Math.abs(m[i]![i]!);
108
+ for (let k = i + 1; k < n; k++) {
109
+ const v = Math.abs(m[k]![i]!);
110
+ if (v > pmax) {
111
+ pmax = v;
112
+ pivot = k;
113
+ }
114
+ }
115
+ if (pmax < 1e-12) return null;
116
+ if (pivot !== i) {
117
+ const tmp = m[i]!;
118
+ m[i] = m[pivot]!;
119
+ m[pivot] = tmp;
120
+ }
121
+ const div = m[i]![i]!;
122
+ for (let j = i; j <= n; j++) m[i]![j]! /= div;
123
+ for (let k = 0; k < n; k++) {
124
+ if (k === i) continue;
125
+ const factor = m[k]![i]!;
126
+ if (factor === 0) continue;
127
+ for (let j = i; j <= n; j++) m[k]![j]! -= factor * m[i]![j]!;
128
+ }
129
+ }
130
+ return m.map((row) => row[n]!);
131
+ }
132
+
133
+ function evalPoly(coefs: readonly number[], x: number): number {
134
+ // Horner — coefs[0] = β₀, coefs[1] = β₁, …
135
+ let acc = 0;
136
+ for (let i = coefs.length - 1; i >= 0; i--) acc = acc * x + coefs[i]!;
137
+ return acc;
138
+ }
139
+
140
+ export function polyFit(
141
+ xs: readonly number[],
142
+ ys: readonly number[],
143
+ degree: number,
144
+ ): RegressionFit | null {
145
+ if (degree < 1) return linearFit(xs, ys);
146
+ const sample = filterFinite(xs, ys);
147
+ const n = sample.xs.length;
148
+ const k = degree + 1;
149
+ if (n < k) return null;
150
+
151
+ // Build normal equations X'X β = X'y for design matrix X of column powers.
152
+ const xtx: number[][] = Array.from({ length: k }, () => Array.from({ length: k }, () => 0));
153
+ const xty: number[] = Array.from({ length: k }, () => 0);
154
+ // Cache power sums to skip O(k²·n) when k > 2.
155
+ const pow = Array.from({ length: 2 * k - 1 }, () => 0);
156
+ let sy = 0;
157
+ for (let i = 0; i < n; i++) {
158
+ const x = sample.xs[i]!;
159
+ const y = sample.ys[i]!;
160
+ let p = 1;
161
+ for (let j = 0; j < 2 * k - 1; j++) {
162
+ pow[j]! += p;
163
+ if (j < k) xty[j]! += p * y;
164
+ p *= x;
165
+ }
166
+ sy += y;
167
+ }
168
+ for (let i = 0; i < k; i++) {
169
+ for (let j = 0; j < k; j++) xtx[i]![j]! = pow[i + j]!;
170
+ }
171
+ const coefs = solveLinear(xtx, xty);
172
+ if (!coefs) return null;
173
+
174
+ let ssr = 0;
175
+ let sst = 0;
176
+ const ybar = sy / n;
177
+ for (let i = 0; i < n; i++) {
178
+ const yhat = evalPoly(coefs, sample.xs[i]!);
179
+ const r = sample.ys[i]! - yhat;
180
+ const d = sample.ys[i]! - ybar;
181
+ ssr += r * r;
182
+ sst += d * d;
183
+ }
184
+ const dof = Math.max(1, n - k);
185
+ const sigma = Math.sqrt(ssr / dof);
186
+ const r2 = sst > 0 ? 1 - ssr / sst : 0;
187
+
188
+ // Approximate seMean: a stable cheap proxy is the OLS leverage proxy
189
+ // σ·sqrt(k/n). For exact CI a caller can recompute via (X'X)⁻¹ but for
190
+ // visualization-grade ribbons this is plenty.
191
+ const seBase = sigma * Math.sqrt(k / n);
192
+
193
+ return {
194
+ n,
195
+ predict: (x) => evalPoly(coefs, x),
196
+ seMean: () => seBase,
197
+ residualStdError: sigma,
198
+ summary: { coefs, r2, degree },
199
+ };
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // loessFit — local regression with tricube weights
204
+ // ---------------------------------------------------------------------------
205
+
206
+ /** Tricube weight for normalized distance `u ∈ [0, 1]`; 0 outside. */
207
+ function tricube(u: number): number {
208
+ if (u >= 1) return 0;
209
+ const v = 1 - u * u * u;
210
+ return v * v * v;
211
+ }
212
+
213
+ export interface LoessOptions {
214
+ /** Fraction of the sample used in each local fit. Default `0.5`. */
215
+ span?: number;
216
+ /** Degree of local polynomial: `1` (default) or `2`. */
217
+ degree?: 1 | 2;
218
+ }
219
+
220
+ export function loessFit(
221
+ xs: readonly number[],
222
+ ys: readonly number[],
223
+ options: LoessOptions = {},
224
+ ): RegressionFit | null {
225
+ const sample = filterFinite(xs, ys);
226
+ const n = sample.xs.length;
227
+ if (n < 2) return null;
228
+ const span = Math.max(2 / n, Math.min(1, options.span ?? 0.5));
229
+ const degree = options.degree ?? 1;
230
+ const window = Math.max(2, Math.ceil(span * n));
231
+
232
+ // Sort once by x — neighborhood selection then is just a slice walk.
233
+ const order = Array.from({ length: n }, (_, i) => i).sort(
234
+ (a, b) => sample.xs[a]! - sample.xs[b]!,
235
+ );
236
+ const sx = order.map((i) => sample.xs[i]!);
237
+ const sy = order.map((i) => sample.ys[i]!);
238
+
239
+ // Residual standard error from a cheap sliding fit — used as a global σ
240
+ // for the (constant) confidence band rather than per-x bandwidth tracking.
241
+ let ssr = 0;
242
+ function localFit(x: number): number {
243
+ // Find the `window` nearest neighbors to `x` by linear scan from the
244
+ // bisection point. Adequate for typical n ≤ a few thousand.
245
+ let lo = 0;
246
+ let hi = n;
247
+ while (lo < hi) {
248
+ const mid = (lo + hi) >>> 1;
249
+ if (sx[mid]! < x) lo = mid + 1;
250
+ else hi = mid;
251
+ }
252
+ let l = lo;
253
+ let r = lo;
254
+ while (r - l < window) {
255
+ const dl = l > 0 ? x - sx[l - 1]! : Infinity;
256
+ const dr = r < n ? sx[r]! - x : Infinity;
257
+ if (dl <= dr) l = Math.max(0, l - 1);
258
+ else r = Math.min(n, r + 1);
259
+ if (l === 0 && r === n) break;
260
+ }
261
+ const maxDist = Math.max(Math.abs(x - sx[l]!), Math.abs(sx[r - 1]! - x), 1e-12);
262
+
263
+ // Weighted least squares for local polynomial.
264
+ if (degree === 1) {
265
+ let sw = 0;
266
+ let swx = 0;
267
+ let swy = 0;
268
+ let swxx = 0;
269
+ let swxy = 0;
270
+ for (let i = l; i < r; i++) {
271
+ const w = tricube(Math.abs(sx[i]! - x) / maxDist);
272
+ if (w === 0) continue;
273
+ const xi = sx[i]!;
274
+ const yi = sy[i]!;
275
+ sw += w;
276
+ swx += w * xi;
277
+ swy += w * yi;
278
+ swxx += w * xi * xi;
279
+ swxy += w * xi * yi;
280
+ }
281
+ const denom = sw * swxx - swx * swx;
282
+ if (Math.abs(denom) < 1e-12 || sw === 0) return swy / Math.max(sw, 1e-12);
283
+ const slope = (sw * swxy - swx * swy) / denom;
284
+ const intercept = (swy - slope * swx) / sw;
285
+ return intercept + slope * x;
286
+ }
287
+
288
+ // degree === 2 — solve weighted normal equations of size 3.
289
+ const xtx: number[][] = [
290
+ [0, 0, 0],
291
+ [0, 0, 0],
292
+ [0, 0, 0],
293
+ ];
294
+ const xty = [0, 0, 0];
295
+ for (let i = l; i < r; i++) {
296
+ const w = tricube(Math.abs(sx[i]! - x) / maxDist);
297
+ if (w === 0) continue;
298
+ const xi = sx[i]!;
299
+ const yi = sy[i]!;
300
+ const x2 = xi * xi;
301
+ xtx[0]![0]! += w;
302
+ xtx[0]![1]! += w * xi;
303
+ xtx[0]![2]! += w * x2;
304
+ xtx[1]![1]! += w * x2;
305
+ xtx[1]![2]! += w * xi * x2;
306
+ xtx[2]![2]! += w * x2 * x2;
307
+ xty[0]! += w * yi;
308
+ xty[1]! += w * xi * yi;
309
+ xty[2]! += w * x2 * yi;
310
+ }
311
+ xtx[1]![0] = xtx[0]![1]!;
312
+ xtx[2]![0] = xtx[0]![2]!;
313
+ xtx[2]![1] = xtx[1]![2]!;
314
+ const coefs = solveLinear(xtx, xty);
315
+ if (!coefs) return ys.length > 0 ? ys[0]! : 0;
316
+ return coefs[0]! + coefs[1]! * x + coefs[2]! * x * x;
317
+ }
318
+
319
+ // Pre-fit to compute residual SE for a (rough) constant-band CI.
320
+ for (let i = 0; i < n; i++) {
321
+ const r = sy[i]! - localFit(sx[i]!);
322
+ ssr += r * r;
323
+ }
324
+ const sigma = Math.sqrt(ssr / Math.max(1, n - 2));
325
+
326
+ return {
327
+ n,
328
+ predict: (x) => localFit(x),
329
+ seMean: () => sigma * Math.sqrt(1 / n),
330
+ residualStdError: sigma,
331
+ summary: { span, degree },
332
+ };
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // confidenceBand — sample a fit on a regular grid and return ribbon points
337
+ // ---------------------------------------------------------------------------
338
+
339
+ export interface ConfidenceBandPoint {
340
+ x: number;
341
+ yhat: number;
342
+ lo: number;
343
+ hi: number;
344
+ }
345
+
346
+ export interface ConfidenceBandOptions {
347
+ samples?: number;
348
+ /** Confidence level. Default `0.95`. */
349
+ level?: number;
350
+ /** Override `[xmin, xmax]`. Defaults to data range. */
351
+ domain?: readonly [number, number];
352
+ }
353
+
354
+ /** Two-sided z for a normal-approx confidence level (large-n). */
355
+ function zForLevel(level: number): number {
356
+ // Quick lookup matches typical levels; falls back to ≈1.96.
357
+ if (level >= 0.99) return 2.576;
358
+ if (level >= 0.975) return 2.241;
359
+ if (level >= 0.95) return 1.96;
360
+ if (level >= 0.9) return 1.645;
361
+ if (level >= 0.8) return 1.282;
362
+ return 1.96;
363
+ }
364
+
365
+ export function confidenceBand(
366
+ fit: RegressionFit,
367
+ xs: readonly number[],
368
+ options: ConfidenceBandOptions = {},
369
+ ): ConfidenceBandPoint[] {
370
+ const samples = Math.max(2, options.samples ?? 64);
371
+ const z = zForLevel(options.level ?? 0.95);
372
+ let lo: number;
373
+ let hi: number;
374
+ if (options.domain) {
375
+ [lo, hi] = options.domain;
376
+ } else {
377
+ lo = Infinity;
378
+ hi = -Infinity;
379
+ for (const v of xs) {
380
+ if (!Number.isFinite(v)) continue;
381
+ if (v < lo) lo = v;
382
+ if (v > hi) hi = v;
383
+ }
384
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi) return [];
385
+ }
386
+ const out = Array.from<ConfidenceBandPoint>({ length: samples });
387
+ const step = (hi - lo) / (samples - 1);
388
+ for (let i = 0; i < samples; i++) {
389
+ const x = lo + step * i;
390
+ const yhat = fit.predict(x);
391
+ const se = fit.seMean(x);
392
+ const half = z * se;
393
+ out[i] = { x, yhat, lo: yhat - half, hi: yhat + half };
394
+ }
395
+ return out;
396
+ }
@@ -0,0 +1,55 @@
1
+ export type RollingStatisticKind = "mean" | "median" | "min" | "max" | "sum" | "stddev";
2
+ /**
3
+ * Statistic to compute over each window. Strings cover the common cases;
4
+ * `{ kind: 'quantile', p }` picks an arbitrary quantile (0..1); a function
5
+ * receives the windowed `(values, items)` and returns any scalar — use it for
6
+ * custom stats (mode, trimmed mean, sign of slope, …).
7
+ */
8
+ export type RollingStatistic<T> = RollingStatisticKind | {
9
+ kind: "quantile";
10
+ p: number;
11
+ } | ((values: readonly number[], items: readonly T[]) => number);
12
+ /**
13
+ * Window size.
14
+ *
15
+ * - A bare number is interpreted as `unit: 'count'` (k nearest items by axis
16
+ * value, including self). Most predictable across axis types.
17
+ * - `{ value, unit: 'domain' }` — sliding window of `value` data-axis units,
18
+ * centered on the current item. For a time axis pass milliseconds (e.g.
19
+ * `7 * 24 * 3_600_000` for a 7-day window). For a numeric axis the value is
20
+ * in the axis's own units.
21
+ * - `{ value, unit: 'count' }` — explicit count-window form.
22
+ */
23
+ export type RollingWindow = number | {
24
+ value: number;
25
+ unit: "domain" | "count";
26
+ };
27
+ export type RollingAxis = "x" | "y";
28
+ export interface RollingWindowOptions<T> {
29
+ x: (datum: T, index: number) => number | Date;
30
+ y: (datum: T, index: number) => number;
31
+ window: RollingWindow;
32
+ /** Default `"mean"`. */
33
+ statistic?: RollingStatistic<T>;
34
+ /**
35
+ * Points failing the filter are excluded from every window *and* not
36
+ * emitted on the output series — they vanish from both the aggregation
37
+ * and the line that connects the results.
38
+ */
39
+ filter?: (datum: T, index: number) => boolean;
40
+ /**
41
+ * Axis the window slides along. Default `"x"` (slide on x, aggregate y);
42
+ * `"y"` flips (slide on y, aggregate x). Output is sorted by the slide
43
+ * axis so a `lineMark` connecting the points draws monotonically.
44
+ */
45
+ axis?: RollingAxis;
46
+ }
47
+ export interface RollingPoint {
48
+ x: number;
49
+ y: number;
50
+ /** Index into the original input `data` array. */
51
+ sourceIndex: number;
52
+ /** Number of points (after filter) that fell in this window. */
53
+ count: number;
54
+ }
55
+ export declare function rollingWindow<T>(data: readonly T[], options: RollingWindowOptions<T>): RollingPoint[];