chartjs-plugin-trendline 3.2.0 → 3.2.3

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 (43) hide show
  1. package/.github/copilot-instructions.md +40 -40
  2. package/.github/workflows/release.yml +64 -61
  3. package/.github/workflows/tests.yml +26 -26
  4. package/.prettierrc +5 -5
  5. package/CLAUDE.md +44 -44
  6. package/GEMINI.md +40 -40
  7. package/LICENSE +21 -21
  8. package/MIGRATION.md +126 -126
  9. package/README.md +166 -166
  10. package/babel.config.js +3 -3
  11. package/changelog.md +39 -39
  12. package/dist/chartjs-plugin-trendline.cjs +884 -885
  13. package/dist/chartjs-plugin-trendline.esm.js +882 -883
  14. package/dist/chartjs-plugin-trendline.js +890 -891
  15. package/dist/chartjs-plugin-trendline.min.js +8 -8
  16. package/dist/chartjs-plugin-trendline.min.js.map +1 -1
  17. package/example/barChart.html +165 -165
  18. package/example/barChartWithNullValues.html +168 -168
  19. package/example/barChart_label.html +174 -174
  20. package/example/exponentialChart.html +244 -244
  21. package/example/lineChart.html +210 -210
  22. package/example/lineChartProjection.html +261 -261
  23. package/example/lineChartTypeTime.html +190 -190
  24. package/example/scatterChart.html +136 -136
  25. package/example/scatterProjection.html +141 -141
  26. package/example/test-null-handling.html +59 -59
  27. package/index.html +215 -215
  28. package/jest.config.js +4 -4
  29. package/package.json +45 -40
  30. package/rollup.config.js +54 -54
  31. package/src/components/label.js +56 -56
  32. package/src/components/label.test.js +129 -129
  33. package/src/components/trendline.js +375 -375
  34. package/src/components/trendline.test.js +789 -789
  35. package/src/core/plugin.js +78 -79
  36. package/src/core/plugin.test.js +307 -0
  37. package/src/index.js +12 -12
  38. package/src/utils/drawing.js +125 -125
  39. package/src/utils/drawing.test.js +308 -308
  40. package/src/utils/exponentialFitter.js +146 -146
  41. package/src/utils/exponentialFitter.test.js +362 -362
  42. package/src/utils/lineFitter.js +86 -86
  43. package/src/utils/lineFitter.test.js +340 -340
@@ -1,363 +1,363 @@
1
- import { ExponentialFitter } from './exponentialFitter.js';
2
-
3
- describe('ExponentialFitter', () => {
4
- describe('constructor', () => {
5
- test('should initialize with default values', () => {
6
- const fitter = new ExponentialFitter();
7
- expect(fitter.count).toBe(0);
8
- expect(fitter.sumx).toBe(0);
9
- expect(fitter.sumlny).toBe(0);
10
- expect(fitter.sumx2).toBe(0);
11
- expect(fitter.sumxlny).toBe(0);
12
- expect(fitter.minx).toBe(Number.MAX_VALUE);
13
- expect(fitter.maxx).toBe(Number.MIN_VALUE);
14
- expect(fitter.hasValidData).toBe(true);
15
- });
16
- });
17
-
18
- describe('add', () => {
19
- test('should add valid positive points correctly', () => {
20
- const fitter = new ExponentialFitter();
21
- fitter.add(0, 1);
22
- fitter.add(1, 2);
23
- fitter.add(2, 4);
24
-
25
- expect(fitter.count).toBe(3);
26
- expect(fitter.hasValidData).toBe(true);
27
- expect(fitter.minx).toBe(0);
28
- expect(fitter.maxx).toBe(2);
29
- });
30
-
31
- test('should reject negative y values', () => {
32
- const fitter = new ExponentialFitter();
33
- fitter.add(0, -1);
34
- expect(fitter.hasValidData).toBe(false);
35
- expect(fitter.count).toBe(0);
36
- });
37
-
38
- test('should reject zero y values', () => {
39
- const fitter = new ExponentialFitter();
40
- fitter.add(0, 0);
41
- expect(fitter.hasValidData).toBe(false);
42
- expect(fitter.count).toBe(0);
43
- });
44
-
45
- test('should handle infinite logarithm values', () => {
46
- const fitter = new ExponentialFitter();
47
- fitter.add(0, Infinity);
48
- expect(fitter.hasValidData).toBe(false);
49
- expect(fitter.count).toBe(0);
50
- });
51
- });
52
-
53
- describe('exponential fitting', () => {
54
- test('should fit perfect exponential growth y = 2^x', () => {
55
- const fitter = new ExponentialFitter();
56
- fitter.add(0, 1); // 2^0 = 1
57
- fitter.add(1, 2); // 2^1 = 2
58
- fitter.add(2, 4); // 2^2 = 4
59
- fitter.add(3, 8); // 2^3 = 8
60
-
61
- // For y = 2^x, we have y = e^(ln(2)*x)
62
- // So coefficient should be ~1 and growth rate should be ~ln(2) ≈ 0.693
63
- expect(fitter.coefficient()).toBeCloseTo(1, 3);
64
- expect(fitter.growthRate()).toBeCloseTo(Math.log(2), 3);
65
- });
66
-
67
- test('should fit exponential decay y = e^(-x)', () => {
68
- const fitter = new ExponentialFitter();
69
- fitter.add(0, 1); // e^0 = 1
70
- fitter.add(1, Math.exp(-1)); // e^(-1) ≈ 0.368
71
- fitter.add(2, Math.exp(-2)); // e^(-2) ≈ 0.135
72
- fitter.add(3, Math.exp(-3)); // e^(-3) ≈ 0.050
73
-
74
- expect(fitter.coefficient()).toBeCloseTo(1, 3);
75
- expect(fitter.growthRate()).toBeCloseTo(-1, 3);
76
- });
77
-
78
- test('should fit exponential with coefficient y = 3*e^(0.5*x)', () => {
79
- const fitter = new ExponentialFitter();
80
- const a = 3;
81
- const b = 0.5;
82
-
83
- for (let x = 0; x <= 4; x++) {
84
- const y = a * Math.exp(b * x);
85
- fitter.add(x, y);
86
- }
87
-
88
- expect(fitter.coefficient()).toBeCloseTo(a, 3);
89
- expect(fitter.growthRate()).toBeCloseTo(b, 3);
90
- });
91
- });
92
-
93
- describe('f', () => {
94
- test('should return correct fitted values for exponential function', () => {
95
- const fitter = new ExponentialFitter();
96
- fitter.add(0, 1);
97
- fitter.add(1, 2);
98
- fitter.add(2, 4);
99
- fitter.add(3, 8);
100
-
101
- expect(fitter.f(0)).toBeCloseTo(1, 2);
102
- expect(fitter.f(1)).toBeCloseTo(2, 2);
103
- expect(fitter.f(2)).toBeCloseTo(4, 2);
104
- expect(fitter.f(3)).toBeCloseTo(8, 2);
105
- expect(fitter.f(4)).toBeCloseTo(16, 2);
106
- });
107
-
108
- test('should return 0 for invalid data', () => {
109
- const fitter = new ExponentialFitter();
110
- fitter.add(0, -1); // This makes hasValidData false
111
-
112
- expect(fitter.f(1)).toBe(0);
113
- });
114
-
115
- test('should return 0 for insufficient data', () => {
116
- const fitter = new ExponentialFitter();
117
- fitter.add(0, 1); // Only one point
118
-
119
- expect(fitter.f(1)).toBe(0);
120
- });
121
- });
122
-
123
- describe('growthRate and coefficient', () => {
124
- test('should return 0 and 1 respectively for invalid data', () => {
125
- const fitter = new ExponentialFitter();
126
- fitter.add(0, -1);
127
-
128
- expect(fitter.growthRate()).toBe(0);
129
- expect(fitter.coefficient()).toBe(1);
130
- });
131
-
132
- test('should return 0 and 1 respectively for insufficient data', () => {
133
- const fitter = new ExponentialFitter();
134
- fitter.add(0, 1);
135
-
136
- expect(fitter.growthRate()).toBe(0);
137
- expect(fitter.coefficient()).toBe(1);
138
- });
139
-
140
- test('should handle degenerate case with same x values', () => {
141
- const fitter = new ExponentialFitter();
142
- fitter.add(1, 2);
143
- fitter.add(1, 4);
144
- fitter.add(1, 8);
145
-
146
- expect(fitter.growthRate()).toBe(0);
147
- });
148
- });
149
-
150
- describe('scale', () => {
151
- test('should return the growth rate', () => {
152
- const fitter = new ExponentialFitter();
153
- fitter.add(0, 1);
154
- fitter.add(1, 2);
155
- fitter.add(2, 4);
156
-
157
- expect(fitter.scale()).toBe(fitter.growthRate());
158
- });
159
- });
160
-
161
- describe('correlation', () => {
162
- test('should return 0 for invalid data', () => {
163
- const fitter = new ExponentialFitter();
164
- fitter.add(0, -1);
165
-
166
- expect(fitter.correlation()).toBe(0);
167
- });
168
-
169
- test('should return 0 for insufficient data', () => {
170
- const fitter = new ExponentialFitter();
171
- fitter.add(0, 1);
172
-
173
- expect(fitter.correlation()).toBe(0);
174
- });
175
-
176
- test('should return a value between 0 and 1', () => {
177
- const fitter = new ExponentialFitter();
178
- fitter.add(0, 1);
179
- fitter.add(1, 2);
180
- fitter.add(2, 4);
181
- fitter.add(3, 8);
182
-
183
- const correlation = fitter.correlation();
184
- expect(correlation).toBeGreaterThanOrEqual(0);
185
- expect(correlation).toBeLessThanOrEqual(1);
186
- });
187
-
188
- test('should return high correlation for perfect exponential data', () => {
189
- const fitter = new ExponentialFitter();
190
- fitter.add(0, 1); // 2^0 = 1
191
- fitter.add(1, 2); // 2^1 = 2
192
- fitter.add(2, 4); // 2^2 = 4
193
- fitter.add(3, 8); // 2^3 = 8
194
-
195
- const correlation = fitter.correlation();
196
- expect(correlation).toBeGreaterThan(0.99); // Perfect exponential should have very high R²
197
- });
198
-
199
- test('should return lower correlation for noisy exponential data', () => {
200
- const fitter = new ExponentialFitter();
201
- fitter.add(0, 1.5); // More noisy data to ensure lower correlation
202
- fitter.add(1, 1.8);
203
- fitter.add(2, 5.2);
204
- fitter.add(3, 6.1);
205
-
206
- const correlation = fitter.correlation();
207
- expect(correlation).toBeGreaterThan(0.5); // Still reasonable correlation
208
- expect(correlation).toBeLessThan(0.95); // But not perfect
209
- });
210
- });
211
-
212
- describe('edge cases', () => {
213
- test('should handle very large values', () => {
214
- const fitter = new ExponentialFitter();
215
- fitter.add(0, 1);
216
- fitter.add(1, 10);
217
- fitter.add(2, 100);
218
-
219
- expect(fitter.hasValidData).toBe(true);
220
- expect(fitter.f(0)).toBeCloseTo(1, 1);
221
- expect(fitter.f(1)).toBeCloseTo(10, 1);
222
- });
223
-
224
- test('should handle very small positive values', () => {
225
- const fitter = new ExponentialFitter();
226
- fitter.add(0, 0.001);
227
- fitter.add(1, 0.01);
228
- fitter.add(2, 0.1);
229
-
230
- expect(fitter.hasValidData).toBe(true);
231
- expect(fitter.f(0)).toBeCloseTo(0.001, 3);
232
- });
233
-
234
- test('should handle overflow gracefully', () => {
235
- const fitter = new ExponentialFitter();
236
- fitter.add(0, 1);
237
- fitter.add(1, 2);
238
- fitter.add(2, 4);
239
-
240
- // For this data, growth rate is ln(2) ≈ 0.693
241
- // So 1000 * 0.693 = 693, which should trigger overflow protection
242
- const result = fitter.f(1000);
243
- expect(result).toBe(0); // Should return 0 when overflow occurs
244
-
245
- // Test a smaller but still large value that should work
246
- const smallerResult = fitter.f(10);
247
- expect(smallerResult).toBeGreaterThan(0);
248
- expect(smallerResult).toBeLessThan(Infinity);
249
- });
250
- });
251
-
252
- describe('caching behavior', () => {
253
- test('should cache coefficient, growth rate, and correlation calculations', () => {
254
- const fitter = new ExponentialFitter();
255
- fitter.add(0, 1);
256
- fitter.add(1, 2);
257
- fitter.add(2, 4);
258
-
259
- // First calls should compute and cache
260
- const growthRate1 = fitter.growthRate();
261
- const coefficient1 = fitter.coefficient();
262
- const correlation1 = fitter.correlation();
263
-
264
- // Subsequent calls should return cached values
265
- const growthRate2 = fitter.growthRate();
266
- const coefficient2 = fitter.coefficient();
267
- const correlation2 = fitter.correlation();
268
-
269
- expect(growthRate1).toBe(growthRate2);
270
- expect(coefficient1).toBe(coefficient2);
271
- expect(correlation1).toBe(correlation2);
272
-
273
- // Check that cache is populated
274
- expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
275
- expect(fitter._cachedCoefficient).toBeCloseTo(1, 3);
276
- expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
277
- expect(fitter._cacheValid).toBe(true);
278
- });
279
-
280
- test('should invalidate cache when new data is added', () => {
281
- const fitter = new ExponentialFitter();
282
- fitter.add(0, 1);
283
- fitter.add(1, 2);
284
-
285
- // Cache should be valid after first calculation
286
- fitter.growthRate();
287
- expect(fitter._cacheValid).toBe(true);
288
-
289
- // Adding new data should invalidate cache
290
- fitter.add(2, 8); // This changes the exponential curve
291
- expect(fitter._cacheValid).toBe(false);
292
-
293
- // New calculation should work with updated data
294
- const newGrowthRate = fitter.growthRate();
295
- expect(newGrowthRate).toBeGreaterThan(Math.log(2)); // Should be higher with [0,1], [1,2], [2,8]
296
- expect(fitter._cacheValid).toBe(true);
297
- });
298
-
299
- test('should handle multiple method calls efficiently with caching', () => {
300
- const fitter = new ExponentialFitter();
301
- fitter.add(0, 2);
302
- fitter.add(1, 4);
303
- fitter.add(2, 8); // y = 2 * 2^x
304
-
305
- // Simulate multiple calls like in trendline.js
306
- const coefficient1 = fitter.coefficient();
307
- const growthRate1 = fitter.growthRate();
308
- const f1 = fitter.f(3); // This calls both coefficient() and growthRate()
309
- const coefficient2 = fitter.coefficient(); // Another direct call
310
- const correlation1 = fitter.correlation();
311
-
312
- expect(coefficient1).toBeCloseTo(2, 3);
313
- expect(coefficient2).toBeCloseTo(2, 3);
314
- expect(growthRate1).toBeCloseTo(Math.log(2), 3);
315
- expect(f1).toBeCloseTo(16, 3); // 2 * 2^3 = 16
316
- expect(correlation1).toBeGreaterThan(0.99);
317
-
318
- // All should use cached values
319
- expect(fitter._cachedCoefficient).toBeCloseTo(2, 3);
320
- expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
321
- expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
322
- expect(fitter._cacheValid).toBe(true);
323
- });
324
-
325
- test('should initialize cache properties correctly', () => {
326
- const fitter = new ExponentialFitter();
327
- expect(fitter._cachedGrowthRate).toBe(null);
328
- expect(fitter._cachedCoefficient).toBe(null);
329
- expect(fitter._cachedCorrelation).toBe(null);
330
- expect(fitter._cacheValid).toBe(false);
331
- });
332
-
333
- test('should handle caching with invalid data correctly', () => {
334
- const fitter = new ExponentialFitter();
335
- fitter.add(0, -1); // Invalid data
336
-
337
- // Should return default values without caching (early return)
338
- const growthRate = fitter.growthRate();
339
- const coefficient = fitter.coefficient();
340
- const correlation = fitter.correlation();
341
-
342
- expect(growthRate).toBe(0);
343
- expect(coefficient).toBe(1);
344
- expect(correlation).toBe(0);
345
- expect(fitter._cacheValid).toBe(false); // Cache not set for invalid data
346
- });
347
-
348
- test('should handle caching with insufficient data correctly', () => {
349
- const fitter = new ExponentialFitter();
350
- fitter.add(0, 1); // Only one point
351
-
352
- // Should return default values without caching (early return)
353
- const growthRate = fitter.growthRate();
354
- const coefficient = fitter.coefficient();
355
- const correlation = fitter.correlation();
356
-
357
- expect(growthRate).toBe(0);
358
- expect(coefficient).toBe(1);
359
- expect(correlation).toBe(0);
360
- expect(fitter._cacheValid).toBe(false); // Cache not set for insufficient data
361
- });
362
- });
1
+ import { ExponentialFitter } from './exponentialFitter.js';
2
+
3
+ describe('ExponentialFitter', () => {
4
+ describe('constructor', () => {
5
+ test('should initialize with default values', () => {
6
+ const fitter = new ExponentialFitter();
7
+ expect(fitter.count).toBe(0);
8
+ expect(fitter.sumx).toBe(0);
9
+ expect(fitter.sumlny).toBe(0);
10
+ expect(fitter.sumx2).toBe(0);
11
+ expect(fitter.sumxlny).toBe(0);
12
+ expect(fitter.minx).toBe(Number.MAX_VALUE);
13
+ expect(fitter.maxx).toBe(Number.MIN_VALUE);
14
+ expect(fitter.hasValidData).toBe(true);
15
+ });
16
+ });
17
+
18
+ describe('add', () => {
19
+ test('should add valid positive points correctly', () => {
20
+ const fitter = new ExponentialFitter();
21
+ fitter.add(0, 1);
22
+ fitter.add(1, 2);
23
+ fitter.add(2, 4);
24
+
25
+ expect(fitter.count).toBe(3);
26
+ expect(fitter.hasValidData).toBe(true);
27
+ expect(fitter.minx).toBe(0);
28
+ expect(fitter.maxx).toBe(2);
29
+ });
30
+
31
+ test('should reject negative y values', () => {
32
+ const fitter = new ExponentialFitter();
33
+ fitter.add(0, -1);
34
+ expect(fitter.hasValidData).toBe(false);
35
+ expect(fitter.count).toBe(0);
36
+ });
37
+
38
+ test('should reject zero y values', () => {
39
+ const fitter = new ExponentialFitter();
40
+ fitter.add(0, 0);
41
+ expect(fitter.hasValidData).toBe(false);
42
+ expect(fitter.count).toBe(0);
43
+ });
44
+
45
+ test('should handle infinite logarithm values', () => {
46
+ const fitter = new ExponentialFitter();
47
+ fitter.add(0, Infinity);
48
+ expect(fitter.hasValidData).toBe(false);
49
+ expect(fitter.count).toBe(0);
50
+ });
51
+ });
52
+
53
+ describe('exponential fitting', () => {
54
+ test('should fit perfect exponential growth y = 2^x', () => {
55
+ const fitter = new ExponentialFitter();
56
+ fitter.add(0, 1); // 2^0 = 1
57
+ fitter.add(1, 2); // 2^1 = 2
58
+ fitter.add(2, 4); // 2^2 = 4
59
+ fitter.add(3, 8); // 2^3 = 8
60
+
61
+ // For y = 2^x, we have y = e^(ln(2)*x)
62
+ // So coefficient should be ~1 and growth rate should be ~ln(2) ≈ 0.693
63
+ expect(fitter.coefficient()).toBeCloseTo(1, 3);
64
+ expect(fitter.growthRate()).toBeCloseTo(Math.log(2), 3);
65
+ });
66
+
67
+ test('should fit exponential decay y = e^(-x)', () => {
68
+ const fitter = new ExponentialFitter();
69
+ fitter.add(0, 1); // e^0 = 1
70
+ fitter.add(1, Math.exp(-1)); // e^(-1) ≈ 0.368
71
+ fitter.add(2, Math.exp(-2)); // e^(-2) ≈ 0.135
72
+ fitter.add(3, Math.exp(-3)); // e^(-3) ≈ 0.050
73
+
74
+ expect(fitter.coefficient()).toBeCloseTo(1, 3);
75
+ expect(fitter.growthRate()).toBeCloseTo(-1, 3);
76
+ });
77
+
78
+ test('should fit exponential with coefficient y = 3*e^(0.5*x)', () => {
79
+ const fitter = new ExponentialFitter();
80
+ const a = 3;
81
+ const b = 0.5;
82
+
83
+ for (let x = 0; x <= 4; x++) {
84
+ const y = a * Math.exp(b * x);
85
+ fitter.add(x, y);
86
+ }
87
+
88
+ expect(fitter.coefficient()).toBeCloseTo(a, 3);
89
+ expect(fitter.growthRate()).toBeCloseTo(b, 3);
90
+ });
91
+ });
92
+
93
+ describe('f', () => {
94
+ test('should return correct fitted values for exponential function', () => {
95
+ const fitter = new ExponentialFitter();
96
+ fitter.add(0, 1);
97
+ fitter.add(1, 2);
98
+ fitter.add(2, 4);
99
+ fitter.add(3, 8);
100
+
101
+ expect(fitter.f(0)).toBeCloseTo(1, 2);
102
+ expect(fitter.f(1)).toBeCloseTo(2, 2);
103
+ expect(fitter.f(2)).toBeCloseTo(4, 2);
104
+ expect(fitter.f(3)).toBeCloseTo(8, 2);
105
+ expect(fitter.f(4)).toBeCloseTo(16, 2);
106
+ });
107
+
108
+ test('should return 0 for invalid data', () => {
109
+ const fitter = new ExponentialFitter();
110
+ fitter.add(0, -1); // This makes hasValidData false
111
+
112
+ expect(fitter.f(1)).toBe(0);
113
+ });
114
+
115
+ test('should return 0 for insufficient data', () => {
116
+ const fitter = new ExponentialFitter();
117
+ fitter.add(0, 1); // Only one point
118
+
119
+ expect(fitter.f(1)).toBe(0);
120
+ });
121
+ });
122
+
123
+ describe('growthRate and coefficient', () => {
124
+ test('should return 0 and 1 respectively for invalid data', () => {
125
+ const fitter = new ExponentialFitter();
126
+ fitter.add(0, -1);
127
+
128
+ expect(fitter.growthRate()).toBe(0);
129
+ expect(fitter.coefficient()).toBe(1);
130
+ });
131
+
132
+ test('should return 0 and 1 respectively for insufficient data', () => {
133
+ const fitter = new ExponentialFitter();
134
+ fitter.add(0, 1);
135
+
136
+ expect(fitter.growthRate()).toBe(0);
137
+ expect(fitter.coefficient()).toBe(1);
138
+ });
139
+
140
+ test('should handle degenerate case with same x values', () => {
141
+ const fitter = new ExponentialFitter();
142
+ fitter.add(1, 2);
143
+ fitter.add(1, 4);
144
+ fitter.add(1, 8);
145
+
146
+ expect(fitter.growthRate()).toBe(0);
147
+ });
148
+ });
149
+
150
+ describe('scale', () => {
151
+ test('should return the growth rate', () => {
152
+ const fitter = new ExponentialFitter();
153
+ fitter.add(0, 1);
154
+ fitter.add(1, 2);
155
+ fitter.add(2, 4);
156
+
157
+ expect(fitter.scale()).toBe(fitter.growthRate());
158
+ });
159
+ });
160
+
161
+ describe('correlation', () => {
162
+ test('should return 0 for invalid data', () => {
163
+ const fitter = new ExponentialFitter();
164
+ fitter.add(0, -1);
165
+
166
+ expect(fitter.correlation()).toBe(0);
167
+ });
168
+
169
+ test('should return 0 for insufficient data', () => {
170
+ const fitter = new ExponentialFitter();
171
+ fitter.add(0, 1);
172
+
173
+ expect(fitter.correlation()).toBe(0);
174
+ });
175
+
176
+ test('should return a value between 0 and 1', () => {
177
+ const fitter = new ExponentialFitter();
178
+ fitter.add(0, 1);
179
+ fitter.add(1, 2);
180
+ fitter.add(2, 4);
181
+ fitter.add(3, 8);
182
+
183
+ const correlation = fitter.correlation();
184
+ expect(correlation).toBeGreaterThanOrEqual(0);
185
+ expect(correlation).toBeLessThanOrEqual(1);
186
+ });
187
+
188
+ test('should return high correlation for perfect exponential data', () => {
189
+ const fitter = new ExponentialFitter();
190
+ fitter.add(0, 1); // 2^0 = 1
191
+ fitter.add(1, 2); // 2^1 = 2
192
+ fitter.add(2, 4); // 2^2 = 4
193
+ fitter.add(3, 8); // 2^3 = 8
194
+
195
+ const correlation = fitter.correlation();
196
+ expect(correlation).toBeGreaterThan(0.99); // Perfect exponential should have very high R²
197
+ });
198
+
199
+ test('should return lower correlation for noisy exponential data', () => {
200
+ const fitter = new ExponentialFitter();
201
+ fitter.add(0, 1.5); // More noisy data to ensure lower correlation
202
+ fitter.add(1, 1.8);
203
+ fitter.add(2, 5.2);
204
+ fitter.add(3, 6.1);
205
+
206
+ const correlation = fitter.correlation();
207
+ expect(correlation).toBeGreaterThan(0.5); // Still reasonable correlation
208
+ expect(correlation).toBeLessThan(0.95); // But not perfect
209
+ });
210
+ });
211
+
212
+ describe('edge cases', () => {
213
+ test('should handle very large values', () => {
214
+ const fitter = new ExponentialFitter();
215
+ fitter.add(0, 1);
216
+ fitter.add(1, 10);
217
+ fitter.add(2, 100);
218
+
219
+ expect(fitter.hasValidData).toBe(true);
220
+ expect(fitter.f(0)).toBeCloseTo(1, 1);
221
+ expect(fitter.f(1)).toBeCloseTo(10, 1);
222
+ });
223
+
224
+ test('should handle very small positive values', () => {
225
+ const fitter = new ExponentialFitter();
226
+ fitter.add(0, 0.001);
227
+ fitter.add(1, 0.01);
228
+ fitter.add(2, 0.1);
229
+
230
+ expect(fitter.hasValidData).toBe(true);
231
+ expect(fitter.f(0)).toBeCloseTo(0.001, 3);
232
+ });
233
+
234
+ test('should handle overflow gracefully', () => {
235
+ const fitter = new ExponentialFitter();
236
+ fitter.add(0, 1);
237
+ fitter.add(1, 2);
238
+ fitter.add(2, 4);
239
+
240
+ // For this data, growth rate is ln(2) ≈ 0.693
241
+ // So 1000 * 0.693 = 693, which should trigger overflow protection
242
+ const result = fitter.f(1000);
243
+ expect(result).toBe(0); // Should return 0 when overflow occurs
244
+
245
+ // Test a smaller but still large value that should work
246
+ const smallerResult = fitter.f(10);
247
+ expect(smallerResult).toBeGreaterThan(0);
248
+ expect(smallerResult).toBeLessThan(Infinity);
249
+ });
250
+ });
251
+
252
+ describe('caching behavior', () => {
253
+ test('should cache coefficient, growth rate, and correlation calculations', () => {
254
+ const fitter = new ExponentialFitter();
255
+ fitter.add(0, 1);
256
+ fitter.add(1, 2);
257
+ fitter.add(2, 4);
258
+
259
+ // First calls should compute and cache
260
+ const growthRate1 = fitter.growthRate();
261
+ const coefficient1 = fitter.coefficient();
262
+ const correlation1 = fitter.correlation();
263
+
264
+ // Subsequent calls should return cached values
265
+ const growthRate2 = fitter.growthRate();
266
+ const coefficient2 = fitter.coefficient();
267
+ const correlation2 = fitter.correlation();
268
+
269
+ expect(growthRate1).toBe(growthRate2);
270
+ expect(coefficient1).toBe(coefficient2);
271
+ expect(correlation1).toBe(correlation2);
272
+
273
+ // Check that cache is populated
274
+ expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
275
+ expect(fitter._cachedCoefficient).toBeCloseTo(1, 3);
276
+ expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
277
+ expect(fitter._cacheValid).toBe(true);
278
+ });
279
+
280
+ test('should invalidate cache when new data is added', () => {
281
+ const fitter = new ExponentialFitter();
282
+ fitter.add(0, 1);
283
+ fitter.add(1, 2);
284
+
285
+ // Cache should be valid after first calculation
286
+ fitter.growthRate();
287
+ expect(fitter._cacheValid).toBe(true);
288
+
289
+ // Adding new data should invalidate cache
290
+ fitter.add(2, 8); // This changes the exponential curve
291
+ expect(fitter._cacheValid).toBe(false);
292
+
293
+ // New calculation should work with updated data
294
+ const newGrowthRate = fitter.growthRate();
295
+ expect(newGrowthRate).toBeGreaterThan(Math.log(2)); // Should be higher with [0,1], [1,2], [2,8]
296
+ expect(fitter._cacheValid).toBe(true);
297
+ });
298
+
299
+ test('should handle multiple method calls efficiently with caching', () => {
300
+ const fitter = new ExponentialFitter();
301
+ fitter.add(0, 2);
302
+ fitter.add(1, 4);
303
+ fitter.add(2, 8); // y = 2 * 2^x
304
+
305
+ // Simulate multiple calls like in trendline.js
306
+ const coefficient1 = fitter.coefficient();
307
+ const growthRate1 = fitter.growthRate();
308
+ const f1 = fitter.f(3); // This calls both coefficient() and growthRate()
309
+ const coefficient2 = fitter.coefficient(); // Another direct call
310
+ const correlation1 = fitter.correlation();
311
+
312
+ expect(coefficient1).toBeCloseTo(2, 3);
313
+ expect(coefficient2).toBeCloseTo(2, 3);
314
+ expect(growthRate1).toBeCloseTo(Math.log(2), 3);
315
+ expect(f1).toBeCloseTo(16, 3); // 2 * 2^3 = 16
316
+ expect(correlation1).toBeGreaterThan(0.99);
317
+
318
+ // All should use cached values
319
+ expect(fitter._cachedCoefficient).toBeCloseTo(2, 3);
320
+ expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
321
+ expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
322
+ expect(fitter._cacheValid).toBe(true);
323
+ });
324
+
325
+ test('should initialize cache properties correctly', () => {
326
+ const fitter = new ExponentialFitter();
327
+ expect(fitter._cachedGrowthRate).toBe(null);
328
+ expect(fitter._cachedCoefficient).toBe(null);
329
+ expect(fitter._cachedCorrelation).toBe(null);
330
+ expect(fitter._cacheValid).toBe(false);
331
+ });
332
+
333
+ test('should handle caching with invalid data correctly', () => {
334
+ const fitter = new ExponentialFitter();
335
+ fitter.add(0, -1); // Invalid data
336
+
337
+ // Should return default values without caching (early return)
338
+ const growthRate = fitter.growthRate();
339
+ const coefficient = fitter.coefficient();
340
+ const correlation = fitter.correlation();
341
+
342
+ expect(growthRate).toBe(0);
343
+ expect(coefficient).toBe(1);
344
+ expect(correlation).toBe(0);
345
+ expect(fitter._cacheValid).toBe(false); // Cache not set for invalid data
346
+ });
347
+
348
+ test('should handle caching with insufficient data correctly', () => {
349
+ const fitter = new ExponentialFitter();
350
+ fitter.add(0, 1); // Only one point
351
+
352
+ // Should return default values without caching (early return)
353
+ const growthRate = fitter.growthRate();
354
+ const coefficient = fitter.coefficient();
355
+ const correlation = fitter.correlation();
356
+
357
+ expect(growthRate).toBe(0);
358
+ expect(coefficient).toBe(1);
359
+ expect(correlation).toBe(0);
360
+ expect(fitter._cacheValid).toBe(false); // Cache not set for insufficient data
361
+ });
362
+ });
363
363
  });