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,308 +1,308 @@
1
- import { getScales, setLineStyle, drawTrendline, fillBelowTrendline } from './drawing.js';
2
-
3
- describe('getScales', () => {
4
- it('should correctly identify x and y scales', () => {
5
- const mockChartInstance = {
6
- scales: {
7
- x: { isHorizontal: () => true, id: 'x-axis' },
8
- y: { isHorizontal: () => false, id: 'y-axis' },
9
- },
10
- };
11
- const { xScale, yScale } = getScales(mockChartInstance);
12
- expect(xScale.id).toBe('x-axis');
13
- expect(yScale.id).toBe('y-axis');
14
- });
15
-
16
- it('should handle missing y scale', () => {
17
- const mockChartInstance = {
18
- scales: {
19
- x: { isHorizontal: () => true, id: 'x-axis' },
20
- },
21
- };
22
- const { xScale, yScale } = getScales(mockChartInstance);
23
- expect(xScale.id).toBe('x-axis');
24
- expect(yScale).toBeUndefined();
25
- });
26
-
27
- it('should handle missing x scale', () => {
28
- const mockChartInstance = {
29
- scales: {
30
- y: { isHorizontal: () => false, id: 'y-axis' },
31
- },
32
- };
33
- const { xScale, yScale } = getScales(mockChartInstance);
34
- expect(xScale).toBeUndefined();
35
- expect(yScale.id).toBe('y-axis');
36
- });
37
-
38
- it('should handle multiple scales of the same orientation (uses the last one found)', () => {
39
- const mockChartInstance = {
40
- scales: {
41
- x1: { isHorizontal: () => true, id: 'x-axis-1' },
42
- x2: { isHorizontal: () => true, id: 'x-axis-2' },
43
- y: { isHorizontal: () => false, id: 'y-axis' },
44
- },
45
- };
46
- const { xScale, yScale } = getScales(mockChartInstance);
47
- expect(['x-axis-1', 'x-axis-2']).toContain(xScale.id);
48
- expect(yScale.id).toBe('y-axis');
49
- });
50
-
51
- it('should handle multiple y-scales and no x-scale (uses the last one found for y)', () => {
52
- const mockChartInstance = {
53
- scales: {
54
- y1: { isHorizontal: () => false, id: 'y-axis-1' },
55
- y2: { isHorizontal: () => false, id: 'y-axis-2' },
56
- },
57
- };
58
- const { xScale, yScale } = getScales(mockChartInstance);
59
- expect(xScale).toBeUndefined();
60
- expect(['y-axis-1', 'y-axis-2']).toContain(yScale.id);
61
- });
62
-
63
-
64
- it('should handle an empty scales object', () => {
65
- const mockChartInstance = {
66
- scales: {},
67
- };
68
- const { xScale, yScale } = getScales(mockChartInstance);
69
- expect(xScale).toBeUndefined();
70
- expect(yScale).toBeUndefined();
71
- });
72
- });
73
-
74
- describe('setLineStyle', () => {
75
- let mockCtx;
76
-
77
- beforeEach(() => {
78
- mockCtx = {
79
- setLineDash: jest.fn(),
80
- };
81
- });
82
-
83
- it('should set dotted line style', () => {
84
- setLineStyle(mockCtx, 'dotted');
85
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([2, 2]);
86
- });
87
-
88
- it('should set dashed line style', () => {
89
- setLineStyle(mockCtx, 'dashed');
90
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([8, 3]);
91
- });
92
-
93
- it('should set dashdot line style', () => {
94
- setLineStyle(mockCtx, 'dashdot');
95
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([8, 3, 2, 3]);
96
- });
97
-
98
- it('should set solid line style', () => {
99
- setLineStyle(mockCtx, 'solid');
100
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
101
- });
102
-
103
- it('should default to solid line style for undefined input', () => {
104
- setLineStyle(mockCtx, undefined);
105
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
106
- });
107
-
108
- it('should default to solid line style for unknown input', () => {
109
- setLineStyle(mockCtx, 'unknownStyle');
110
- expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
111
- });
112
- });
113
-
114
- describe('drawTrendline', () => {
115
- let mockCtx;
116
- let mockGradient;
117
- let consoleWarnSpy;
118
-
119
- beforeEach(() => {
120
- mockGradient = {
121
- addColorStop: jest.fn(),
122
- };
123
- mockCtx = {
124
- beginPath: jest.fn(),
125
- moveTo: jest.fn(),
126
- lineTo: jest.fn(),
127
- stroke: jest.fn(),
128
- closePath: jest.fn(),
129
- createLinearGradient: jest.fn().mockReturnValue(mockGradient),
130
- strokeStyle: '', // Initialize, will be set by the function
131
- };
132
- consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
133
- });
134
-
135
- afterEach(() => {
136
- consoleWarnSpy.mockRestore();
137
- jest.clearAllMocks();
138
- });
139
-
140
- it('should draw a trendline with valid parameters and gradient', () => {
141
- const params = { ctx: mockCtx, x1: 0, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
142
- drawTrendline(params);
143
-
144
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
145
- expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
146
- expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
147
- expect(mockCtx.createLinearGradient).toHaveBeenCalledWith(params.x1, params.y1, params.x2, params.y2);
148
- expect(mockGradient.addColorStop).toHaveBeenCalledTimes(2);
149
- expect(mockGradient.addColorStop).toHaveBeenCalledWith(0, params.colorMin);
150
- expect(mockGradient.addColorStop).toHaveBeenCalledWith(1, params.colorMax);
151
- expect(mockCtx.strokeStyle).toBe(mockGradient);
152
- expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
153
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
154
- expect(consoleWarnSpy).not.toHaveBeenCalled();
155
- });
156
-
157
- it('should warn and not draw if coordinates are non-finite', () => {
158
- const params = { ctx: mockCtx, x1: Infinity, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
159
- drawTrendline(params);
160
-
161
- expect(consoleWarnSpy).toHaveBeenCalledWith(
162
- 'Cannot draw trendline: coordinates contain non-finite values',
163
- { x1: Infinity, y1: 10, x2: 100, y2: 110 }
164
- );
165
- expect(mockCtx.beginPath).not.toHaveBeenCalled();
166
- expect(mockCtx.moveTo).not.toHaveBeenCalled();
167
- expect(mockCtx.lineTo).not.toHaveBeenCalled();
168
- expect(mockCtx.stroke).not.toHaveBeenCalled();
169
- expect(mockCtx.closePath).not.toHaveBeenCalled();
170
- expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
171
- });
172
-
173
- it('should warn and use fallback color if gradient creation fails', () => {
174
- mockCtx.createLinearGradient.mockImplementation(() => {
175
- throw new Error('Test gradient error');
176
- });
177
- const params = { ctx: mockCtx, x1: 0, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
178
- drawTrendline(params);
179
-
180
- expect(consoleWarnSpy).toHaveBeenCalledWith('Gradient creation failed, using solid color:', expect.any(Error));
181
- expect(mockCtx.strokeStyle).toBe(params.colorMin);
182
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
183
- expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
184
- expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
185
- expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
186
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
187
- });
188
-
189
- it('should use solid color for degenerate gradient (identical coordinates)', () => {
190
- const params = { ctx: mockCtx, x1: 50, y1: 50, x2: 50, y2: 50, colorMin: 'red', colorMax: 'blue' };
191
- drawTrendline(params);
192
-
193
- expect(consoleWarnSpy).toHaveBeenCalledWith(
194
- 'Gradient vector too small, using solid color:',
195
- { x1: 50, y1: 50, x2: 50, y2: 50, length: 0 }
196
- );
197
- expect(mockCtx.strokeStyle).toBe(params.colorMin);
198
- expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
199
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
200
- expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
201
- expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
202
- expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
203
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
204
- });
205
-
206
- it('should use solid color for degenerate gradient (very close coordinates)', () => {
207
- const params = { ctx: mockCtx, x1: 50, y1: 50, x2: 50.005, y2: 50.005, colorMin: 'red', colorMax: 'blue' };
208
- drawTrendline(params);
209
-
210
- expect(consoleWarnSpy).toHaveBeenCalledWith(
211
- 'Gradient vector too small, using solid color:',
212
- expect.objectContaining({ x1: 50, y1: 50, x2: 50.005, y2: 50.005 })
213
- );
214
- expect(mockCtx.strokeStyle).toBe(params.colorMin);
215
- expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
216
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
217
- expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
218
- expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
219
- expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
220
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
221
- });
222
-
223
- it('should create gradient successfully for sufficiently separated coordinates', () => {
224
- const params = { ctx: mockCtx, x1: 0, y1: 0, x2: 1, y2: 0, colorMin: 'red', colorMax: 'blue' };
225
- drawTrendline(params);
226
-
227
- expect(consoleWarnSpy).not.toHaveBeenCalled();
228
- expect(mockCtx.createLinearGradient).toHaveBeenCalledWith(params.x1, params.y1, params.x2, params.y2);
229
- expect(mockGradient.addColorStop).toHaveBeenCalledTimes(2);
230
- expect(mockGradient.addColorStop).toHaveBeenCalledWith(0, params.colorMin);
231
- expect(mockGradient.addColorStop).toHaveBeenCalledWith(1, params.colorMax);
232
- expect(mockCtx.strokeStyle).toBe(mockGradient);
233
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
234
- expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
235
- expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
236
- expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
237
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
238
- });
239
- });
240
-
241
- describe('fillBelowTrendline', () => {
242
- let mockCtx;
243
- let consoleWarnSpy;
244
-
245
- beforeEach(() => {
246
- mockCtx = {
247
- beginPath: jest.fn(),
248
- moveTo: jest.fn(),
249
- lineTo: jest.fn(),
250
- closePath: jest.fn(),
251
- fill: jest.fn(),
252
- fillStyle: '', // Initialize, will be set by the function
253
- };
254
- consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
255
- });
256
-
257
- afterEach(() => {
258
- consoleWarnSpy.mockRestore();
259
- jest.clearAllMocks();
260
- });
261
-
262
- it('should fill below trendline with valid parameters', () => {
263
- const x1 = 0, y1 = 10, x2 = 100, y2 = 110, drawBottom = 200, fillColor = 'rgba(0,0,255,0.1)';
264
- fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
265
-
266
- expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
267
- expect(mockCtx.moveTo).toHaveBeenCalledWith(x1, y1);
268
- expect(mockCtx.lineTo).toHaveBeenNthCalledWith(1, x2, y2);
269
- expect(mockCtx.lineTo).toHaveBeenNthCalledWith(2, x2, drawBottom);
270
- expect(mockCtx.lineTo).toHaveBeenNthCalledWith(3, x1, drawBottom);
271
- expect(mockCtx.lineTo).toHaveBeenNthCalledWith(4, x1, y1);
272
- expect(mockCtx.lineTo).toHaveBeenCalledTimes(4);
273
- expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
274
- expect(mockCtx.fillStyle).toBe(fillColor);
275
- expect(mockCtx.fill).toHaveBeenCalledTimes(1);
276
- expect(consoleWarnSpy).not.toHaveBeenCalled();
277
- });
278
-
279
- it('should warn and not fill if coordinates or drawBottom are non-finite', () => {
280
- const x1 = 0, y1 = 10, x2 = 100, y2 = Infinity, drawBottom = 200, fillColor = 'rgba(0,0,255,0.1)';
281
- fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
282
-
283
- expect(consoleWarnSpy).toHaveBeenCalledWith(
284
- 'Cannot fill below trendline: coordinates contain non-finite values',
285
- { x1, y1, x2, y2: Infinity, drawBottom }
286
- );
287
- expect(mockCtx.beginPath).not.toHaveBeenCalled();
288
- expect(mockCtx.moveTo).not.toHaveBeenCalled();
289
- expect(mockCtx.lineTo).not.toHaveBeenCalled();
290
- expect(mockCtx.closePath).not.toHaveBeenCalled();
291
- expect(mockCtx.fill).not.toHaveBeenCalled();
292
- });
293
-
294
- it('should warn and not fill if drawBottom is non-finite', () => {
295
- const x1 = 0, y1 = 10, x2 = 100, y2 = 110, drawBottom = Infinity, fillColor = 'rgba(0,0,255,0.1)';
296
- fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
297
-
298
- expect(consoleWarnSpy).toHaveBeenCalledWith(
299
- 'Cannot fill below trendline: coordinates contain non-finite values',
300
- { x1, y1, x2, y2, drawBottom: Infinity }
301
- );
302
- expect(mockCtx.beginPath).not.toHaveBeenCalled();
303
- expect(mockCtx.moveTo).not.toHaveBeenCalled();
304
- expect(mockCtx.lineTo).not.toHaveBeenCalled();
305
- expect(mockCtx.closePath).not.toHaveBeenCalled();
306
- expect(mockCtx.fill).not.toHaveBeenCalled();
307
- });
308
- });
1
+ import { getScales, setLineStyle, drawTrendline, fillBelowTrendline } from './drawing.js';
2
+
3
+ describe('getScales', () => {
4
+ it('should correctly identify x and y scales', () => {
5
+ const mockChartInstance = {
6
+ scales: {
7
+ x: { isHorizontal: () => true, id: 'x-axis' },
8
+ y: { isHorizontal: () => false, id: 'y-axis' },
9
+ },
10
+ };
11
+ const { xScale, yScale } = getScales(mockChartInstance);
12
+ expect(xScale.id).toBe('x-axis');
13
+ expect(yScale.id).toBe('y-axis');
14
+ });
15
+
16
+ it('should handle missing y scale', () => {
17
+ const mockChartInstance = {
18
+ scales: {
19
+ x: { isHorizontal: () => true, id: 'x-axis' },
20
+ },
21
+ };
22
+ const { xScale, yScale } = getScales(mockChartInstance);
23
+ expect(xScale.id).toBe('x-axis');
24
+ expect(yScale).toBeUndefined();
25
+ });
26
+
27
+ it('should handle missing x scale', () => {
28
+ const mockChartInstance = {
29
+ scales: {
30
+ y: { isHorizontal: () => false, id: 'y-axis' },
31
+ },
32
+ };
33
+ const { xScale, yScale } = getScales(mockChartInstance);
34
+ expect(xScale).toBeUndefined();
35
+ expect(yScale.id).toBe('y-axis');
36
+ });
37
+
38
+ it('should handle multiple scales of the same orientation (uses the last one found)', () => {
39
+ const mockChartInstance = {
40
+ scales: {
41
+ x1: { isHorizontal: () => true, id: 'x-axis-1' },
42
+ x2: { isHorizontal: () => true, id: 'x-axis-2' },
43
+ y: { isHorizontal: () => false, id: 'y-axis' },
44
+ },
45
+ };
46
+ const { xScale, yScale } = getScales(mockChartInstance);
47
+ expect(['x-axis-1', 'x-axis-2']).toContain(xScale.id);
48
+ expect(yScale.id).toBe('y-axis');
49
+ });
50
+
51
+ it('should handle multiple y-scales and no x-scale (uses the last one found for y)', () => {
52
+ const mockChartInstance = {
53
+ scales: {
54
+ y1: { isHorizontal: () => false, id: 'y-axis-1' },
55
+ y2: { isHorizontal: () => false, id: 'y-axis-2' },
56
+ },
57
+ };
58
+ const { xScale, yScale } = getScales(mockChartInstance);
59
+ expect(xScale).toBeUndefined();
60
+ expect(['y-axis-1', 'y-axis-2']).toContain(yScale.id);
61
+ });
62
+
63
+
64
+ it('should handle an empty scales object', () => {
65
+ const mockChartInstance = {
66
+ scales: {},
67
+ };
68
+ const { xScale, yScale } = getScales(mockChartInstance);
69
+ expect(xScale).toBeUndefined();
70
+ expect(yScale).toBeUndefined();
71
+ });
72
+ });
73
+
74
+ describe('setLineStyle', () => {
75
+ let mockCtx;
76
+
77
+ beforeEach(() => {
78
+ mockCtx = {
79
+ setLineDash: jest.fn(),
80
+ };
81
+ });
82
+
83
+ it('should set dotted line style', () => {
84
+ setLineStyle(mockCtx, 'dotted');
85
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([2, 2]);
86
+ });
87
+
88
+ it('should set dashed line style', () => {
89
+ setLineStyle(mockCtx, 'dashed');
90
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([8, 3]);
91
+ });
92
+
93
+ it('should set dashdot line style', () => {
94
+ setLineStyle(mockCtx, 'dashdot');
95
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([8, 3, 2, 3]);
96
+ });
97
+
98
+ it('should set solid line style', () => {
99
+ setLineStyle(mockCtx, 'solid');
100
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
101
+ });
102
+
103
+ it('should default to solid line style for undefined input', () => {
104
+ setLineStyle(mockCtx, undefined);
105
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
106
+ });
107
+
108
+ it('should default to solid line style for unknown input', () => {
109
+ setLineStyle(mockCtx, 'unknownStyle');
110
+ expect(mockCtx.setLineDash).toHaveBeenCalledWith([]);
111
+ });
112
+ });
113
+
114
+ describe('drawTrendline', () => {
115
+ let mockCtx;
116
+ let mockGradient;
117
+ let consoleWarnSpy;
118
+
119
+ beforeEach(() => {
120
+ mockGradient = {
121
+ addColorStop: jest.fn(),
122
+ };
123
+ mockCtx = {
124
+ beginPath: jest.fn(),
125
+ moveTo: jest.fn(),
126
+ lineTo: jest.fn(),
127
+ stroke: jest.fn(),
128
+ closePath: jest.fn(),
129
+ createLinearGradient: jest.fn().mockReturnValue(mockGradient),
130
+ strokeStyle: '', // Initialize, will be set by the function
131
+ };
132
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
133
+ });
134
+
135
+ afterEach(() => {
136
+ consoleWarnSpy.mockRestore();
137
+ jest.clearAllMocks();
138
+ });
139
+
140
+ it('should draw a trendline with valid parameters and gradient', () => {
141
+ const params = { ctx: mockCtx, x1: 0, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
142
+ drawTrendline(params);
143
+
144
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
145
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
146
+ expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
147
+ expect(mockCtx.createLinearGradient).toHaveBeenCalledWith(params.x1, params.y1, params.x2, params.y2);
148
+ expect(mockGradient.addColorStop).toHaveBeenCalledTimes(2);
149
+ expect(mockGradient.addColorStop).toHaveBeenCalledWith(0, params.colorMin);
150
+ expect(mockGradient.addColorStop).toHaveBeenCalledWith(1, params.colorMax);
151
+ expect(mockCtx.strokeStyle).toBe(mockGradient);
152
+ expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
153
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
154
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('should warn and not draw if coordinates are non-finite', () => {
158
+ const params = { ctx: mockCtx, x1: Infinity, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
159
+ drawTrendline(params);
160
+
161
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
162
+ 'Cannot draw trendline: coordinates contain non-finite values',
163
+ { x1: Infinity, y1: 10, x2: 100, y2: 110 }
164
+ );
165
+ expect(mockCtx.beginPath).not.toHaveBeenCalled();
166
+ expect(mockCtx.moveTo).not.toHaveBeenCalled();
167
+ expect(mockCtx.lineTo).not.toHaveBeenCalled();
168
+ expect(mockCtx.stroke).not.toHaveBeenCalled();
169
+ expect(mockCtx.closePath).not.toHaveBeenCalled();
170
+ expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
171
+ });
172
+
173
+ it('should warn and use fallback color if gradient creation fails', () => {
174
+ mockCtx.createLinearGradient.mockImplementation(() => {
175
+ throw new Error('Test gradient error');
176
+ });
177
+ const params = { ctx: mockCtx, x1: 0, y1: 10, x2: 100, y2: 110, colorMin: 'red', colorMax: 'blue' };
178
+ drawTrendline(params);
179
+
180
+ expect(consoleWarnSpy).toHaveBeenCalledWith('Gradient creation failed, using solid color:', expect.any(Error));
181
+ expect(mockCtx.strokeStyle).toBe(params.colorMin);
182
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
183
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
184
+ expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
185
+ expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
186
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it('should use solid color for degenerate gradient (identical coordinates)', () => {
190
+ const params = { ctx: mockCtx, x1: 50, y1: 50, x2: 50, y2: 50, colorMin: 'red', colorMax: 'blue' };
191
+ drawTrendline(params);
192
+
193
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
194
+ 'Gradient vector too small, using solid color:',
195
+ { x1: 50, y1: 50, x2: 50, y2: 50, length: 0 }
196
+ );
197
+ expect(mockCtx.strokeStyle).toBe(params.colorMin);
198
+ expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
199
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
200
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
201
+ expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
202
+ expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
203
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
204
+ });
205
+
206
+ it('should use solid color for degenerate gradient (very close coordinates)', () => {
207
+ const params = { ctx: mockCtx, x1: 50, y1: 50, x2: 50.005, y2: 50.005, colorMin: 'red', colorMax: 'blue' };
208
+ drawTrendline(params);
209
+
210
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
211
+ 'Gradient vector too small, using solid color:',
212
+ expect.objectContaining({ x1: 50, y1: 50, x2: 50.005, y2: 50.005 })
213
+ );
214
+ expect(mockCtx.strokeStyle).toBe(params.colorMin);
215
+ expect(mockCtx.createLinearGradient).not.toHaveBeenCalled();
216
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
217
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
218
+ expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
219
+ expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
220
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
221
+ });
222
+
223
+ it('should create gradient successfully for sufficiently separated coordinates', () => {
224
+ const params = { ctx: mockCtx, x1: 0, y1: 0, x2: 1, y2: 0, colorMin: 'red', colorMax: 'blue' };
225
+ drawTrendline(params);
226
+
227
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
228
+ expect(mockCtx.createLinearGradient).toHaveBeenCalledWith(params.x1, params.y1, params.x2, params.y2);
229
+ expect(mockGradient.addColorStop).toHaveBeenCalledTimes(2);
230
+ expect(mockGradient.addColorStop).toHaveBeenCalledWith(0, params.colorMin);
231
+ expect(mockGradient.addColorStop).toHaveBeenCalledWith(1, params.colorMax);
232
+ expect(mockCtx.strokeStyle).toBe(mockGradient);
233
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
234
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(params.x1, params.y1);
235
+ expect(mockCtx.lineTo).toHaveBeenCalledWith(params.x2, params.y2);
236
+ expect(mockCtx.stroke).toHaveBeenCalledTimes(1);
237
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
238
+ });
239
+ });
240
+
241
+ describe('fillBelowTrendline', () => {
242
+ let mockCtx;
243
+ let consoleWarnSpy;
244
+
245
+ beforeEach(() => {
246
+ mockCtx = {
247
+ beginPath: jest.fn(),
248
+ moveTo: jest.fn(),
249
+ lineTo: jest.fn(),
250
+ closePath: jest.fn(),
251
+ fill: jest.fn(),
252
+ fillStyle: '', // Initialize, will be set by the function
253
+ };
254
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
255
+ });
256
+
257
+ afterEach(() => {
258
+ consoleWarnSpy.mockRestore();
259
+ jest.clearAllMocks();
260
+ });
261
+
262
+ it('should fill below trendline with valid parameters', () => {
263
+ const x1 = 0, y1 = 10, x2 = 100, y2 = 110, drawBottom = 200, fillColor = 'rgba(0,0,255,0.1)';
264
+ fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
265
+
266
+ expect(mockCtx.beginPath).toHaveBeenCalledTimes(1);
267
+ expect(mockCtx.moveTo).toHaveBeenCalledWith(x1, y1);
268
+ expect(mockCtx.lineTo).toHaveBeenNthCalledWith(1, x2, y2);
269
+ expect(mockCtx.lineTo).toHaveBeenNthCalledWith(2, x2, drawBottom);
270
+ expect(mockCtx.lineTo).toHaveBeenNthCalledWith(3, x1, drawBottom);
271
+ expect(mockCtx.lineTo).toHaveBeenNthCalledWith(4, x1, y1);
272
+ expect(mockCtx.lineTo).toHaveBeenCalledTimes(4);
273
+ expect(mockCtx.closePath).toHaveBeenCalledTimes(1);
274
+ expect(mockCtx.fillStyle).toBe(fillColor);
275
+ expect(mockCtx.fill).toHaveBeenCalledTimes(1);
276
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
277
+ });
278
+
279
+ it('should warn and not fill if coordinates or drawBottom are non-finite', () => {
280
+ const x1 = 0, y1 = 10, x2 = 100, y2 = Infinity, drawBottom = 200, fillColor = 'rgba(0,0,255,0.1)';
281
+ fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
282
+
283
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
284
+ 'Cannot fill below trendline: coordinates contain non-finite values',
285
+ { x1, y1, x2, y2: Infinity, drawBottom }
286
+ );
287
+ expect(mockCtx.beginPath).not.toHaveBeenCalled();
288
+ expect(mockCtx.moveTo).not.toHaveBeenCalled();
289
+ expect(mockCtx.lineTo).not.toHaveBeenCalled();
290
+ expect(mockCtx.closePath).not.toHaveBeenCalled();
291
+ expect(mockCtx.fill).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it('should warn and not fill if drawBottom is non-finite', () => {
295
+ const x1 = 0, y1 = 10, x2 = 100, y2 = 110, drawBottom = Infinity, fillColor = 'rgba(0,0,255,0.1)';
296
+ fillBelowTrendline(mockCtx, x1, y1, x2, y2, drawBottom, fillColor);
297
+
298
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
299
+ 'Cannot fill below trendline: coordinates contain non-finite values',
300
+ { x1, y1, x2, y2, drawBottom: Infinity }
301
+ );
302
+ expect(mockCtx.beginPath).not.toHaveBeenCalled();
303
+ expect(mockCtx.moveTo).not.toHaveBeenCalled();
304
+ expect(mockCtx.lineTo).not.toHaveBeenCalled();
305
+ expect(mockCtx.closePath).not.toHaveBeenCalled();
306
+ expect(mockCtx.fill).not.toHaveBeenCalled();
307
+ });
308
+ });