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.
- package/.github/copilot-instructions.md +40 -40
- package/.github/workflows/release.yml +64 -61
- package/.github/workflows/tests.yml +26 -26
- package/.prettierrc +5 -5
- package/CLAUDE.md +44 -44
- package/GEMINI.md +40 -40
- package/LICENSE +21 -21
- package/MIGRATION.md +126 -126
- package/README.md +166 -166
- package/babel.config.js +3 -3
- package/changelog.md +39 -39
- package/dist/chartjs-plugin-trendline.cjs +884 -885
- package/dist/chartjs-plugin-trendline.esm.js +882 -883
- package/dist/chartjs-plugin-trendline.js +890 -891
- package/dist/chartjs-plugin-trendline.min.js +8 -8
- package/dist/chartjs-plugin-trendline.min.js.map +1 -1
- package/example/barChart.html +165 -165
- package/example/barChartWithNullValues.html +168 -168
- package/example/barChart_label.html +174 -174
- package/example/exponentialChart.html +244 -244
- package/example/lineChart.html +210 -210
- package/example/lineChartProjection.html +261 -261
- package/example/lineChartTypeTime.html +190 -190
- package/example/scatterChart.html +136 -136
- package/example/scatterProjection.html +141 -141
- package/example/test-null-handling.html +59 -59
- package/index.html +215 -215
- package/jest.config.js +4 -4
- package/package.json +45 -40
- package/rollup.config.js +54 -54
- package/src/components/label.js +56 -56
- package/src/components/label.test.js +129 -129
- package/src/components/trendline.js +375 -375
- package/src/components/trendline.test.js +789 -789
- package/src/core/plugin.js +78 -79
- package/src/core/plugin.test.js +307 -0
- package/src/index.js +12 -12
- package/src/utils/drawing.js +125 -125
- package/src/utils/drawing.test.js +308 -308
- package/src/utils/exponentialFitter.js +146 -146
- package/src/utils/exponentialFitter.test.js +362 -362
- package/src/utils/lineFitter.js +86 -86
- 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
|
});
|