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,340 +1,340 @@
|
|
|
1
|
-
import { LineFitter } from './lineFitter.js';
|
|
2
|
-
|
|
3
|
-
describe('LineFitter', () => {
|
|
4
|
-
test('constructor should initialize values correctly', () => {
|
|
5
|
-
const fitter = new LineFitter();
|
|
6
|
-
expect(fitter.count).toBe(0);
|
|
7
|
-
expect(fitter.sumx).toBe(0);
|
|
8
|
-
expect(fitter.sumy).toBe(0);
|
|
9
|
-
expect(fitter.sumx2).toBe(0);
|
|
10
|
-
expect(fitter.sumxy).toBe(0);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test('add method should update values correctly', () => {
|
|
14
|
-
const fitter = new LineFitter();
|
|
15
|
-
fitter.add(1, 2);
|
|
16
|
-
expect(fitter.count).toBe(1);
|
|
17
|
-
expect(fitter.sumx).toBe(1);
|
|
18
|
-
expect(fitter.sumy).toBe(2);
|
|
19
|
-
expect(fitter.sumx2).toBe(1);
|
|
20
|
-
expect(fitter.sumxy).toBe(2);
|
|
21
|
-
|
|
22
|
-
fitter.add(3, 4);
|
|
23
|
-
expect(fitter.count).toBe(2);
|
|
24
|
-
expect(fitter.sumx).toBe(1 + 3);
|
|
25
|
-
expect(fitter.sumy).toBe(2 + 4);
|
|
26
|
-
expect(fitter.sumx2).toBe(1 * 1 + 3 * 3);
|
|
27
|
-
expect(fitter.sumxy).toBe(1 * 2 + 3 * 4);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('slope', () => {
|
|
31
|
-
test('should return NaN if count is 0', () => {
|
|
32
|
-
const fitter = new LineFitter();
|
|
33
|
-
expect(fitter.slope()).toBeNaN();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('should return NaN if count is 1', () => {
|
|
37
|
-
const fitter = new LineFitter();
|
|
38
|
-
fitter.add(1, 1);
|
|
39
|
-
expect(fitter.slope()).toBeNaN();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('should calculate slope correctly for 2 points', () => {
|
|
43
|
-
const fitter = new LineFitter();
|
|
44
|
-
fitter.add(1, 1);
|
|
45
|
-
fitter.add(2, 2);
|
|
46
|
-
expect(fitter.slope()).toBe(1);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('should return 0 for a horizontal line', () => {
|
|
50
|
-
const fitter = new LineFitter();
|
|
51
|
-
fitter.add(1, 1);
|
|
52
|
-
fitter.add(2, 1);
|
|
53
|
-
expect(fitter.slope()).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('should return NaN for a vertical line (all x are same)', () => {
|
|
57
|
-
// Denominator: count * sumx2 - sumx * sumx
|
|
58
|
-
// If all x are x_c: count * (count * x_c^2) - (count * x_c)^2 = 0
|
|
59
|
-
// Numerator: count * sumxy - sumx * sumy
|
|
60
|
-
// If all x are x_c: count * (x_c * sumy) - (count * x_c) * sumy = 0
|
|
61
|
-
// So, 0/0 = NaN
|
|
62
|
-
const fitter = new LineFitter();
|
|
63
|
-
fitter.add(1, 1);
|
|
64
|
-
fitter.add(1, 2);
|
|
65
|
-
expect(fitter.slope()).toBeNaN();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('should return NaN for a vertical line (points added in decreasing y order, all x same)', () => {
|
|
69
|
-
const fitter = new LineFitter();
|
|
70
|
-
fitter.add(1, 2);
|
|
71
|
-
fitter.add(1, 1);
|
|
72
|
-
expect(fitter.slope()).toBeNaN();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('intercept', () => {
|
|
77
|
-
test('should return NaN if count is 0', () => {
|
|
78
|
-
const fitter = new LineFitter();
|
|
79
|
-
expect(fitter.intercept()).toBeNaN();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('should return NaN if count is 1', () => {
|
|
83
|
-
const fitter = new LineFitter();
|
|
84
|
-
fitter.add(1, 5);
|
|
85
|
-
expect(fitter.intercept()).toBeNaN();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('should calculate intercept correctly for 2 points', () => {
|
|
89
|
-
const fitter = new LineFitter();
|
|
90
|
-
fitter.add(1, 1); // y = x, intercept should be 0
|
|
91
|
-
fitter.add(2, 2);
|
|
92
|
-
expect(fitter.intercept()).toBe(0);
|
|
93
|
-
|
|
94
|
-
const fitter2 = new LineFitter();
|
|
95
|
-
fitter2.add(1, 2); // y = x + 1, intercept should be 1
|
|
96
|
-
fitter2.add(2, 3);
|
|
97
|
-
expect(fitter2.intercept()).toBe(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('should calculate intercept for a horizontal line', () => {
|
|
101
|
-
const fitter = new LineFitter();
|
|
102
|
-
fitter.add(1, 5);
|
|
103
|
-
fitter.add(2, 5);
|
|
104
|
-
expect(fitter.intercept()).toBe(5);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('should calculate intercept for a vertical line', () => {
|
|
108
|
-
// As established, slope for a vertical line (all x same) is NaN.
|
|
109
|
-
// intercept = (sumy - slope * sumx) / count = (sumy - NaN * sumx) / count = NaN
|
|
110
|
-
const fitter = new LineFitter();
|
|
111
|
-
fitter.add(1, 1); // x=1, slope is NaN
|
|
112
|
-
fitter.add(1, 2);
|
|
113
|
-
expect(fitter.intercept()).toBeNaN();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const fitter2 = new LineFitter();
|
|
117
|
-
fitter2.add(1, 2); // x=1, slope is NaN
|
|
118
|
-
fitter2.add(1, 1);
|
|
119
|
-
expect(fitter2.intercept()).toBeNaN();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('f', () => {
|
|
124
|
-
test('should return NaN if count is 0', () => {
|
|
125
|
-
const fitter = new LineFitter();
|
|
126
|
-
expect(fitter.f(10)).toBeNaN();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('should return NaN if count is 1', () => {
|
|
130
|
-
const fitter = new LineFitter();
|
|
131
|
-
fitter.add(2, 5);
|
|
132
|
-
expect(fitter.f(10)).toBeNaN();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('should predict y values correctly', () => {
|
|
136
|
-
const fitter = new LineFitter();
|
|
137
|
-
fitter.add(1, 1); // y = x
|
|
138
|
-
fitter.add(2, 2);
|
|
139
|
-
expect(fitter.f(3)).toBe(3);
|
|
140
|
-
expect(fitter.f(0)).toBe(0);
|
|
141
|
-
|
|
142
|
-
const fitter2 = new LineFitter();
|
|
143
|
-
fitter2.add(0, 1); // y = 2x + 1
|
|
144
|
-
fitter2.add(1, 3);
|
|
145
|
-
expect(fitter2.f(2)).toBe(5);
|
|
146
|
-
expect(fitter2.f(-1)).toBe(-1);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('should predict y values for a horizontal line', () => {
|
|
150
|
-
const fitter = new LineFitter();
|
|
151
|
-
fitter.add(1, 5);
|
|
152
|
-
fitter.add(2, 5); // y = 5
|
|
153
|
-
expect(fitter.f(10)).toBe(5);
|
|
154
|
-
expect(fitter.f(-10)).toBe(5);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('should predict y values for a vertical line', () => {
|
|
158
|
-
// slope is NaN for vertical line, intercept is NaN.
|
|
159
|
-
// f(x) = NaN * x + NaN = NaN
|
|
160
|
-
const fitter = new LineFitter();
|
|
161
|
-
fitter.add(1, 1); // x = 1. slope = NaN
|
|
162
|
-
fitter.add(1, 3);
|
|
163
|
-
expect(fitter.f(1)).toBeNaN();
|
|
164
|
-
expect(fitter.f(2)).toBeNaN();
|
|
165
|
-
expect(fitter.f(0)).toBeNaN();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const fitter2 = new LineFitter();
|
|
169
|
-
fitter2.add(1, 3); // x = 1. slope = NaN
|
|
170
|
-
fitter2.add(1, 1);
|
|
171
|
-
expect(fitter2.f(1)).toBeNaN();
|
|
172
|
-
expect(fitter2.f(2)).toBeNaN();
|
|
173
|
-
expect(fitter2.f(0)).toBeNaN();
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('fo (x-intercept)', () => {
|
|
178
|
-
test('should return NaN if count is 0', () => {
|
|
179
|
-
const fitter = new LineFitter();
|
|
180
|
-
expect(fitter.fo()).toBeNaN();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('should return NaN if count is 1', () => {
|
|
184
|
-
const fitter = new LineFitter();
|
|
185
|
-
fitter.add(1, 1);
|
|
186
|
-
expect(fitter.fo()).toBeNaN();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('should calculate x-intercept correctly for a sloped line', () => {
|
|
190
|
-
const fitter = new LineFitter();
|
|
191
|
-
fitter.add(1, 1); // y = x. Slope = 1, Intercept = 0.
|
|
192
|
-
fitter.add(2, 2); // x-intercept = -0/1 = 0
|
|
193
|
-
expect(fitter.fo()).toBeCloseTo(0);
|
|
194
|
-
|
|
195
|
-
const fitter2 = new LineFitter();
|
|
196
|
-
fitter2.add(1, 0); // y = x - 1. Slope = 1, Intercept = -1
|
|
197
|
-
fitter2.add(2, 1); // x-intercept = -(-1)/1 = 1
|
|
198
|
-
expect(fitter2.fo()).toBeCloseTo(1);
|
|
199
|
-
|
|
200
|
-
const fitter3 = new LineFitter();
|
|
201
|
-
fitter3.add(0, 2); // y = -x + 2. Slope = -1, Intercept = 2
|
|
202
|
-
fitter3.add(1, 1); // x-intercept = -2/(-1) = 2
|
|
203
|
-
expect(fitter3.fo()).toBeCloseTo(2);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test('should handle horizontal line (y=c, c!=0)', () => {
|
|
207
|
-
const fitter = new LineFitter();
|
|
208
|
-
fitter.add(1, 2); // y = 2. Slope = 0, Intercept = 2.
|
|
209
|
-
fitter.add(2, 2); // x-intercept = -2/0 = -Infinity
|
|
210
|
-
expect(fitter.fo()).toBe(-Infinity);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('should handle horizontal line (y=0)', () => {
|
|
214
|
-
const fitter = new LineFitter();
|
|
215
|
-
fitter.add(1, 0); // y = 0. Slope = 0, Intercept = 0.
|
|
216
|
-
fitter.add(2, 0); // x-intercept = -0/0 = NaN
|
|
217
|
-
expect(fitter.fo()).toBeNaN();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('should return NaN for a vertical line (slope is NaN)', () => {
|
|
221
|
-
const fitter = new LineFitter();
|
|
222
|
-
fitter.add(1, 1); // x = 1. Slope = NaN
|
|
223
|
-
fitter.add(1, 2); // intercept = NaN
|
|
224
|
-
expect(fitter.fo()).toBeNaN(); // -NaN/NaN = NaN
|
|
225
|
-
|
|
226
|
-
const fitter2 = new LineFitter();
|
|
227
|
-
fitter2.add(1, 2); // x = 1. Slope = NaN
|
|
228
|
-
fitter2.add(1, 1); // intercept = NaN
|
|
229
|
-
expect(fitter2.fo()).toBeNaN();
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe('scale', () => {
|
|
234
|
-
test('should return NaN if count is 0', () => {
|
|
235
|
-
const fitter = new LineFitter();
|
|
236
|
-
expect(fitter.scale()).toBeNaN();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test('should return NaN if count is 1', () => {
|
|
240
|
-
const fitter = new LineFitter();
|
|
241
|
-
fitter.add(1, 1);
|
|
242
|
-
expect(fitter.scale()).toBeNaN();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test('should return the same as slope() for 2 or more points', () => {
|
|
246
|
-
const fitter = new LineFitter();
|
|
247
|
-
fitter.add(1, 1);
|
|
248
|
-
fitter.add(2, 2); // Slope = 1
|
|
249
|
-
expect(fitter.scale()).toBe(fitter.slope());
|
|
250
|
-
expect(fitter.scale()).toBe(1);
|
|
251
|
-
|
|
252
|
-
const fitter2 = new LineFitter();
|
|
253
|
-
fitter2.add(1, 1);
|
|
254
|
-
fitter2.add(2, 1); // Slope = 0 (horizontal)
|
|
255
|
-
expect(fitter2.scale()).toBe(fitter2.slope());
|
|
256
|
-
expect(fitter2.scale()).toBe(0);
|
|
257
|
-
|
|
258
|
-
const fitter3 = new LineFitter();
|
|
259
|
-
fitter3.add(1, 1);
|
|
260
|
-
fitter3.add(1, 2); // Slope = NaN (vertical)
|
|
261
|
-
expect(fitter3.scale()).toBe(fitter3.slope());
|
|
262
|
-
expect(fitter3.scale()).toBeNaN();
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe('caching behavior', () => {
|
|
267
|
-
test('should cache slope and intercept calculations', () => {
|
|
268
|
-
const fitter = new LineFitter();
|
|
269
|
-
fitter.add(1, 1);
|
|
270
|
-
fitter.add(2, 2);
|
|
271
|
-
|
|
272
|
-
// First call should compute and cache
|
|
273
|
-
const slope1 = fitter.slope();
|
|
274
|
-
const intercept1 = fitter.intercept();
|
|
275
|
-
|
|
276
|
-
// Subsequent calls should return cached values
|
|
277
|
-
const slope2 = fitter.slope();
|
|
278
|
-
const intercept2 = fitter.intercept();
|
|
279
|
-
|
|
280
|
-
expect(slope1).toBe(slope2);
|
|
281
|
-
expect(intercept1).toBe(intercept2);
|
|
282
|
-
expect(slope1).toBe(1);
|
|
283
|
-
expect(intercept1).toBe(0);
|
|
284
|
-
|
|
285
|
-
// Check that cache is populated
|
|
286
|
-
expect(fitter._cachedSlope).toBe(1);
|
|
287
|
-
expect(fitter._cachedIntercept).toBe(0);
|
|
288
|
-
expect(fitter._cacheValid).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test('should invalidate cache when new data is added', () => {
|
|
292
|
-
const fitter = new LineFitter();
|
|
293
|
-
fitter.add(1, 1);
|
|
294
|
-
fitter.add(2, 2);
|
|
295
|
-
|
|
296
|
-
// Cache should be valid after first calculation
|
|
297
|
-
fitter.slope();
|
|
298
|
-
expect(fitter._cacheValid).toBe(true);
|
|
299
|
-
|
|
300
|
-
// Adding new data should invalidate cache
|
|
301
|
-
fitter.add(3, 4);
|
|
302
|
-
expect(fitter._cacheValid).toBe(false);
|
|
303
|
-
|
|
304
|
-
// New calculation should work with updated data
|
|
305
|
-
const newSlope = fitter.slope();
|
|
306
|
-
expect(newSlope).toBeCloseTo(1.5, 5); // New slope with data [1,1], [2,2], [3,4]
|
|
307
|
-
expect(fitter._cacheValid).toBe(true);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('should handle multiple method calls efficiently with caching', () => {
|
|
311
|
-
const fitter = new LineFitter();
|
|
312
|
-
fitter.add(0, 1);
|
|
313
|
-
fitter.add(1, 3);
|
|
314
|
-
fitter.add(2, 5); // y = 2x + 1
|
|
315
|
-
|
|
316
|
-
// Simulate multiple calls like in trendline.js
|
|
317
|
-
const slope1 = fitter.slope();
|
|
318
|
-
const intercept1 = fitter.intercept(); // This used to call slope() again
|
|
319
|
-
const f1 = fitter.f(3); // This calls both slope() and intercept()
|
|
320
|
-
const slope2 = fitter.slope(); // Another direct call
|
|
321
|
-
|
|
322
|
-
expect(slope1).toBe(2);
|
|
323
|
-
expect(slope2).toBe(2);
|
|
324
|
-
expect(intercept1).toBe(1);
|
|
325
|
-
expect(f1).toBe(7); // 2*3 + 1 = 7
|
|
326
|
-
|
|
327
|
-
// All should use cached values
|
|
328
|
-
expect(fitter._cachedSlope).toBe(2);
|
|
329
|
-
expect(fitter._cachedIntercept).toBe(1);
|
|
330
|
-
expect(fitter._cacheValid).toBe(true);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('should initialize cache properties correctly', () => {
|
|
334
|
-
const fitter = new LineFitter();
|
|
335
|
-
expect(fitter._cachedSlope).toBe(null);
|
|
336
|
-
expect(fitter._cachedIntercept).toBe(null);
|
|
337
|
-
expect(fitter._cacheValid).toBe(false);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
});
|
|
1
|
+
import { LineFitter } from './lineFitter.js';
|
|
2
|
+
|
|
3
|
+
describe('LineFitter', () => {
|
|
4
|
+
test('constructor should initialize values correctly', () => {
|
|
5
|
+
const fitter = new LineFitter();
|
|
6
|
+
expect(fitter.count).toBe(0);
|
|
7
|
+
expect(fitter.sumx).toBe(0);
|
|
8
|
+
expect(fitter.sumy).toBe(0);
|
|
9
|
+
expect(fitter.sumx2).toBe(0);
|
|
10
|
+
expect(fitter.sumxy).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('add method should update values correctly', () => {
|
|
14
|
+
const fitter = new LineFitter();
|
|
15
|
+
fitter.add(1, 2);
|
|
16
|
+
expect(fitter.count).toBe(1);
|
|
17
|
+
expect(fitter.sumx).toBe(1);
|
|
18
|
+
expect(fitter.sumy).toBe(2);
|
|
19
|
+
expect(fitter.sumx2).toBe(1);
|
|
20
|
+
expect(fitter.sumxy).toBe(2);
|
|
21
|
+
|
|
22
|
+
fitter.add(3, 4);
|
|
23
|
+
expect(fitter.count).toBe(2);
|
|
24
|
+
expect(fitter.sumx).toBe(1 + 3);
|
|
25
|
+
expect(fitter.sumy).toBe(2 + 4);
|
|
26
|
+
expect(fitter.sumx2).toBe(1 * 1 + 3 * 3);
|
|
27
|
+
expect(fitter.sumxy).toBe(1 * 2 + 3 * 4);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('slope', () => {
|
|
31
|
+
test('should return NaN if count is 0', () => {
|
|
32
|
+
const fitter = new LineFitter();
|
|
33
|
+
expect(fitter.slope()).toBeNaN();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should return NaN if count is 1', () => {
|
|
37
|
+
const fitter = new LineFitter();
|
|
38
|
+
fitter.add(1, 1);
|
|
39
|
+
expect(fitter.slope()).toBeNaN();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should calculate slope correctly for 2 points', () => {
|
|
43
|
+
const fitter = new LineFitter();
|
|
44
|
+
fitter.add(1, 1);
|
|
45
|
+
fitter.add(2, 2);
|
|
46
|
+
expect(fitter.slope()).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should return 0 for a horizontal line', () => {
|
|
50
|
+
const fitter = new LineFitter();
|
|
51
|
+
fitter.add(1, 1);
|
|
52
|
+
fitter.add(2, 1);
|
|
53
|
+
expect(fitter.slope()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should return NaN for a vertical line (all x are same)', () => {
|
|
57
|
+
// Denominator: count * sumx2 - sumx * sumx
|
|
58
|
+
// If all x are x_c: count * (count * x_c^2) - (count * x_c)^2 = 0
|
|
59
|
+
// Numerator: count * sumxy - sumx * sumy
|
|
60
|
+
// If all x are x_c: count * (x_c * sumy) - (count * x_c) * sumy = 0
|
|
61
|
+
// So, 0/0 = NaN
|
|
62
|
+
const fitter = new LineFitter();
|
|
63
|
+
fitter.add(1, 1);
|
|
64
|
+
fitter.add(1, 2);
|
|
65
|
+
expect(fitter.slope()).toBeNaN();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should return NaN for a vertical line (points added in decreasing y order, all x same)', () => {
|
|
69
|
+
const fitter = new LineFitter();
|
|
70
|
+
fitter.add(1, 2);
|
|
71
|
+
fitter.add(1, 1);
|
|
72
|
+
expect(fitter.slope()).toBeNaN();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('intercept', () => {
|
|
77
|
+
test('should return NaN if count is 0', () => {
|
|
78
|
+
const fitter = new LineFitter();
|
|
79
|
+
expect(fitter.intercept()).toBeNaN();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should return NaN if count is 1', () => {
|
|
83
|
+
const fitter = new LineFitter();
|
|
84
|
+
fitter.add(1, 5);
|
|
85
|
+
expect(fitter.intercept()).toBeNaN();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should calculate intercept correctly for 2 points', () => {
|
|
89
|
+
const fitter = new LineFitter();
|
|
90
|
+
fitter.add(1, 1); // y = x, intercept should be 0
|
|
91
|
+
fitter.add(2, 2);
|
|
92
|
+
expect(fitter.intercept()).toBe(0);
|
|
93
|
+
|
|
94
|
+
const fitter2 = new LineFitter();
|
|
95
|
+
fitter2.add(1, 2); // y = x + 1, intercept should be 1
|
|
96
|
+
fitter2.add(2, 3);
|
|
97
|
+
expect(fitter2.intercept()).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should calculate intercept for a horizontal line', () => {
|
|
101
|
+
const fitter = new LineFitter();
|
|
102
|
+
fitter.add(1, 5);
|
|
103
|
+
fitter.add(2, 5);
|
|
104
|
+
expect(fitter.intercept()).toBe(5);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should calculate intercept for a vertical line', () => {
|
|
108
|
+
// As established, slope for a vertical line (all x same) is NaN.
|
|
109
|
+
// intercept = (sumy - slope * sumx) / count = (sumy - NaN * sumx) / count = NaN
|
|
110
|
+
const fitter = new LineFitter();
|
|
111
|
+
fitter.add(1, 1); // x=1, slope is NaN
|
|
112
|
+
fitter.add(1, 2);
|
|
113
|
+
expect(fitter.intercept()).toBeNaN();
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
const fitter2 = new LineFitter();
|
|
117
|
+
fitter2.add(1, 2); // x=1, slope is NaN
|
|
118
|
+
fitter2.add(1, 1);
|
|
119
|
+
expect(fitter2.intercept()).toBeNaN();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('f', () => {
|
|
124
|
+
test('should return NaN if count is 0', () => {
|
|
125
|
+
const fitter = new LineFitter();
|
|
126
|
+
expect(fitter.f(10)).toBeNaN();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should return NaN if count is 1', () => {
|
|
130
|
+
const fitter = new LineFitter();
|
|
131
|
+
fitter.add(2, 5);
|
|
132
|
+
expect(fitter.f(10)).toBeNaN();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should predict y values correctly', () => {
|
|
136
|
+
const fitter = new LineFitter();
|
|
137
|
+
fitter.add(1, 1); // y = x
|
|
138
|
+
fitter.add(2, 2);
|
|
139
|
+
expect(fitter.f(3)).toBe(3);
|
|
140
|
+
expect(fitter.f(0)).toBe(0);
|
|
141
|
+
|
|
142
|
+
const fitter2 = new LineFitter();
|
|
143
|
+
fitter2.add(0, 1); // y = 2x + 1
|
|
144
|
+
fitter2.add(1, 3);
|
|
145
|
+
expect(fitter2.f(2)).toBe(5);
|
|
146
|
+
expect(fitter2.f(-1)).toBe(-1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should predict y values for a horizontal line', () => {
|
|
150
|
+
const fitter = new LineFitter();
|
|
151
|
+
fitter.add(1, 5);
|
|
152
|
+
fitter.add(2, 5); // y = 5
|
|
153
|
+
expect(fitter.f(10)).toBe(5);
|
|
154
|
+
expect(fitter.f(-10)).toBe(5);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should predict y values for a vertical line', () => {
|
|
158
|
+
// slope is NaN for vertical line, intercept is NaN.
|
|
159
|
+
// f(x) = NaN * x + NaN = NaN
|
|
160
|
+
const fitter = new LineFitter();
|
|
161
|
+
fitter.add(1, 1); // x = 1. slope = NaN
|
|
162
|
+
fitter.add(1, 3);
|
|
163
|
+
expect(fitter.f(1)).toBeNaN();
|
|
164
|
+
expect(fitter.f(2)).toBeNaN();
|
|
165
|
+
expect(fitter.f(0)).toBeNaN();
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
const fitter2 = new LineFitter();
|
|
169
|
+
fitter2.add(1, 3); // x = 1. slope = NaN
|
|
170
|
+
fitter2.add(1, 1);
|
|
171
|
+
expect(fitter2.f(1)).toBeNaN();
|
|
172
|
+
expect(fitter2.f(2)).toBeNaN();
|
|
173
|
+
expect(fitter2.f(0)).toBeNaN();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('fo (x-intercept)', () => {
|
|
178
|
+
test('should return NaN if count is 0', () => {
|
|
179
|
+
const fitter = new LineFitter();
|
|
180
|
+
expect(fitter.fo()).toBeNaN();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('should return NaN if count is 1', () => {
|
|
184
|
+
const fitter = new LineFitter();
|
|
185
|
+
fitter.add(1, 1);
|
|
186
|
+
expect(fitter.fo()).toBeNaN();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should calculate x-intercept correctly for a sloped line', () => {
|
|
190
|
+
const fitter = new LineFitter();
|
|
191
|
+
fitter.add(1, 1); // y = x. Slope = 1, Intercept = 0.
|
|
192
|
+
fitter.add(2, 2); // x-intercept = -0/1 = 0
|
|
193
|
+
expect(fitter.fo()).toBeCloseTo(0);
|
|
194
|
+
|
|
195
|
+
const fitter2 = new LineFitter();
|
|
196
|
+
fitter2.add(1, 0); // y = x - 1. Slope = 1, Intercept = -1
|
|
197
|
+
fitter2.add(2, 1); // x-intercept = -(-1)/1 = 1
|
|
198
|
+
expect(fitter2.fo()).toBeCloseTo(1);
|
|
199
|
+
|
|
200
|
+
const fitter3 = new LineFitter();
|
|
201
|
+
fitter3.add(0, 2); // y = -x + 2. Slope = -1, Intercept = 2
|
|
202
|
+
fitter3.add(1, 1); // x-intercept = -2/(-1) = 2
|
|
203
|
+
expect(fitter3.fo()).toBeCloseTo(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should handle horizontal line (y=c, c!=0)', () => {
|
|
207
|
+
const fitter = new LineFitter();
|
|
208
|
+
fitter.add(1, 2); // y = 2. Slope = 0, Intercept = 2.
|
|
209
|
+
fitter.add(2, 2); // x-intercept = -2/0 = -Infinity
|
|
210
|
+
expect(fitter.fo()).toBe(-Infinity);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should handle horizontal line (y=0)', () => {
|
|
214
|
+
const fitter = new LineFitter();
|
|
215
|
+
fitter.add(1, 0); // y = 0. Slope = 0, Intercept = 0.
|
|
216
|
+
fitter.add(2, 0); // x-intercept = -0/0 = NaN
|
|
217
|
+
expect(fitter.fo()).toBeNaN();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should return NaN for a vertical line (slope is NaN)', () => {
|
|
221
|
+
const fitter = new LineFitter();
|
|
222
|
+
fitter.add(1, 1); // x = 1. Slope = NaN
|
|
223
|
+
fitter.add(1, 2); // intercept = NaN
|
|
224
|
+
expect(fitter.fo()).toBeNaN(); // -NaN/NaN = NaN
|
|
225
|
+
|
|
226
|
+
const fitter2 = new LineFitter();
|
|
227
|
+
fitter2.add(1, 2); // x = 1. Slope = NaN
|
|
228
|
+
fitter2.add(1, 1); // intercept = NaN
|
|
229
|
+
expect(fitter2.fo()).toBeNaN();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('scale', () => {
|
|
234
|
+
test('should return NaN if count is 0', () => {
|
|
235
|
+
const fitter = new LineFitter();
|
|
236
|
+
expect(fitter.scale()).toBeNaN();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should return NaN if count is 1', () => {
|
|
240
|
+
const fitter = new LineFitter();
|
|
241
|
+
fitter.add(1, 1);
|
|
242
|
+
expect(fitter.scale()).toBeNaN();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('should return the same as slope() for 2 or more points', () => {
|
|
246
|
+
const fitter = new LineFitter();
|
|
247
|
+
fitter.add(1, 1);
|
|
248
|
+
fitter.add(2, 2); // Slope = 1
|
|
249
|
+
expect(fitter.scale()).toBe(fitter.slope());
|
|
250
|
+
expect(fitter.scale()).toBe(1);
|
|
251
|
+
|
|
252
|
+
const fitter2 = new LineFitter();
|
|
253
|
+
fitter2.add(1, 1);
|
|
254
|
+
fitter2.add(2, 1); // Slope = 0 (horizontal)
|
|
255
|
+
expect(fitter2.scale()).toBe(fitter2.slope());
|
|
256
|
+
expect(fitter2.scale()).toBe(0);
|
|
257
|
+
|
|
258
|
+
const fitter3 = new LineFitter();
|
|
259
|
+
fitter3.add(1, 1);
|
|
260
|
+
fitter3.add(1, 2); // Slope = NaN (vertical)
|
|
261
|
+
expect(fitter3.scale()).toBe(fitter3.slope());
|
|
262
|
+
expect(fitter3.scale()).toBeNaN();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('caching behavior', () => {
|
|
267
|
+
test('should cache slope and intercept calculations', () => {
|
|
268
|
+
const fitter = new LineFitter();
|
|
269
|
+
fitter.add(1, 1);
|
|
270
|
+
fitter.add(2, 2);
|
|
271
|
+
|
|
272
|
+
// First call should compute and cache
|
|
273
|
+
const slope1 = fitter.slope();
|
|
274
|
+
const intercept1 = fitter.intercept();
|
|
275
|
+
|
|
276
|
+
// Subsequent calls should return cached values
|
|
277
|
+
const slope2 = fitter.slope();
|
|
278
|
+
const intercept2 = fitter.intercept();
|
|
279
|
+
|
|
280
|
+
expect(slope1).toBe(slope2);
|
|
281
|
+
expect(intercept1).toBe(intercept2);
|
|
282
|
+
expect(slope1).toBe(1);
|
|
283
|
+
expect(intercept1).toBe(0);
|
|
284
|
+
|
|
285
|
+
// Check that cache is populated
|
|
286
|
+
expect(fitter._cachedSlope).toBe(1);
|
|
287
|
+
expect(fitter._cachedIntercept).toBe(0);
|
|
288
|
+
expect(fitter._cacheValid).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('should invalidate cache when new data is added', () => {
|
|
292
|
+
const fitter = new LineFitter();
|
|
293
|
+
fitter.add(1, 1);
|
|
294
|
+
fitter.add(2, 2);
|
|
295
|
+
|
|
296
|
+
// Cache should be valid after first calculation
|
|
297
|
+
fitter.slope();
|
|
298
|
+
expect(fitter._cacheValid).toBe(true);
|
|
299
|
+
|
|
300
|
+
// Adding new data should invalidate cache
|
|
301
|
+
fitter.add(3, 4);
|
|
302
|
+
expect(fitter._cacheValid).toBe(false);
|
|
303
|
+
|
|
304
|
+
// New calculation should work with updated data
|
|
305
|
+
const newSlope = fitter.slope();
|
|
306
|
+
expect(newSlope).toBeCloseTo(1.5, 5); // New slope with data [1,1], [2,2], [3,4]
|
|
307
|
+
expect(fitter._cacheValid).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('should handle multiple method calls efficiently with caching', () => {
|
|
311
|
+
const fitter = new LineFitter();
|
|
312
|
+
fitter.add(0, 1);
|
|
313
|
+
fitter.add(1, 3);
|
|
314
|
+
fitter.add(2, 5); // y = 2x + 1
|
|
315
|
+
|
|
316
|
+
// Simulate multiple calls like in trendline.js
|
|
317
|
+
const slope1 = fitter.slope();
|
|
318
|
+
const intercept1 = fitter.intercept(); // This used to call slope() again
|
|
319
|
+
const f1 = fitter.f(3); // This calls both slope() and intercept()
|
|
320
|
+
const slope2 = fitter.slope(); // Another direct call
|
|
321
|
+
|
|
322
|
+
expect(slope1).toBe(2);
|
|
323
|
+
expect(slope2).toBe(2);
|
|
324
|
+
expect(intercept1).toBe(1);
|
|
325
|
+
expect(f1).toBe(7); // 2*3 + 1 = 7
|
|
326
|
+
|
|
327
|
+
// All should use cached values
|
|
328
|
+
expect(fitter._cachedSlope).toBe(2);
|
|
329
|
+
expect(fitter._cachedIntercept).toBe(1);
|
|
330
|
+
expect(fitter._cacheValid).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('should initialize cache properties correctly', () => {
|
|
334
|
+
const fitter = new LineFitter();
|
|
335
|
+
expect(fitter._cachedSlope).toBe(null);
|
|
336
|
+
expect(fitter._cachedIntercept).toBe(null);
|
|
337
|
+
expect(fitter._cacheValid).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|