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,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
+ });