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
package/src/components/label.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adds a label to the trendline at the calculated angle.
|
|
3
|
-
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
|
|
4
|
-
* @param {string} label - The label text to add.
|
|
5
|
-
* @param {number} x1 - The starting x-coordinate of the trendline.
|
|
6
|
-
* @param {number} y1 - The starting y-coordinate of the trendline.
|
|
7
|
-
* @param {number} x2 - The ending x-coordinate of the trendline.
|
|
8
|
-
* @param {number} y2 - The ending y-coordinate of the trendline.
|
|
9
|
-
* @param {number} angle - The angle (in radians) of the trendline.
|
|
10
|
-
* @param {string} labelColor - The color of the label text.
|
|
11
|
-
* @param {string} family - The font family for the label text.
|
|
12
|
-
* @param {number} size - The font size for the label text.
|
|
13
|
-
* @param {number} offset - The offset of the label from the trendline
|
|
14
|
-
*/
|
|
15
|
-
export const addTrendlineLabel = (
|
|
16
|
-
ctx,
|
|
17
|
-
label,
|
|
18
|
-
x1,
|
|
19
|
-
y1,
|
|
20
|
-
x2,
|
|
21
|
-
y2,
|
|
22
|
-
angle,
|
|
23
|
-
labelColor,
|
|
24
|
-
family,
|
|
25
|
-
size,
|
|
26
|
-
offset
|
|
27
|
-
) => {
|
|
28
|
-
// Set the label font and color
|
|
29
|
-
ctx.font = `${size}px ${family}`;
|
|
30
|
-
ctx.fillStyle = labelColor;
|
|
31
|
-
|
|
32
|
-
// Label width
|
|
33
|
-
const labelWidth = ctx.measureText(label).width;
|
|
34
|
-
|
|
35
|
-
// Calculate the center of the trendline
|
|
36
|
-
const labelX = (x1 + x2) / 2;
|
|
37
|
-
const labelY = (y1 + y2) / 2;
|
|
38
|
-
|
|
39
|
-
// Save the current state of the canvas
|
|
40
|
-
ctx.save();
|
|
41
|
-
|
|
42
|
-
// Translate to the label position
|
|
43
|
-
ctx.translate(labelX, labelY);
|
|
44
|
-
|
|
45
|
-
// Rotate the context to align with the trendline
|
|
46
|
-
ctx.rotate(angle);
|
|
47
|
-
|
|
48
|
-
// Adjust for the length of the label and rotation
|
|
49
|
-
const adjustedX = -labelWidth / 2; // Center the label horizontally
|
|
50
|
-
const adjustedY = offset; // Adjust Y to compensate for the height
|
|
51
|
-
|
|
52
|
-
// Draw the label
|
|
53
|
-
ctx.fillText(label, adjustedX, adjustedY);
|
|
54
|
-
|
|
55
|
-
// Restore the canvas state
|
|
56
|
-
ctx.restore();
|
|
1
|
+
/**
|
|
2
|
+
* Adds a label to the trendline at the calculated angle.
|
|
3
|
+
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
|
|
4
|
+
* @param {string} label - The label text to add.
|
|
5
|
+
* @param {number} x1 - The starting x-coordinate of the trendline.
|
|
6
|
+
* @param {number} y1 - The starting y-coordinate of the trendline.
|
|
7
|
+
* @param {number} x2 - The ending x-coordinate of the trendline.
|
|
8
|
+
* @param {number} y2 - The ending y-coordinate of the trendline.
|
|
9
|
+
* @param {number} angle - The angle (in radians) of the trendline.
|
|
10
|
+
* @param {string} labelColor - The color of the label text.
|
|
11
|
+
* @param {string} family - The font family for the label text.
|
|
12
|
+
* @param {number} size - The font size for the label text.
|
|
13
|
+
* @param {number} offset - The offset of the label from the trendline
|
|
14
|
+
*/
|
|
15
|
+
export const addTrendlineLabel = (
|
|
16
|
+
ctx,
|
|
17
|
+
label,
|
|
18
|
+
x1,
|
|
19
|
+
y1,
|
|
20
|
+
x2,
|
|
21
|
+
y2,
|
|
22
|
+
angle,
|
|
23
|
+
labelColor,
|
|
24
|
+
family,
|
|
25
|
+
size,
|
|
26
|
+
offset
|
|
27
|
+
) => {
|
|
28
|
+
// Set the label font and color
|
|
29
|
+
ctx.font = `${size}px ${family}`;
|
|
30
|
+
ctx.fillStyle = labelColor;
|
|
31
|
+
|
|
32
|
+
// Label width
|
|
33
|
+
const labelWidth = ctx.measureText(label).width;
|
|
34
|
+
|
|
35
|
+
// Calculate the center of the trendline
|
|
36
|
+
const labelX = (x1 + x2) / 2;
|
|
37
|
+
const labelY = (y1 + y2) / 2;
|
|
38
|
+
|
|
39
|
+
// Save the current state of the canvas
|
|
40
|
+
ctx.save();
|
|
41
|
+
|
|
42
|
+
// Translate to the label position
|
|
43
|
+
ctx.translate(labelX, labelY);
|
|
44
|
+
|
|
45
|
+
// Rotate the context to align with the trendline
|
|
46
|
+
ctx.rotate(angle);
|
|
47
|
+
|
|
48
|
+
// Adjust for the length of the label and rotation
|
|
49
|
+
const adjustedX = -labelWidth / 2; // Center the label horizontally
|
|
50
|
+
const adjustedY = offset; // Adjust Y to compensate for the height
|
|
51
|
+
|
|
52
|
+
// Draw the label
|
|
53
|
+
ctx.fillText(label, adjustedX, adjustedY);
|
|
54
|
+
|
|
55
|
+
// Restore the canvas state
|
|
56
|
+
ctx.restore();
|
|
57
57
|
};
|
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
import { addTrendlineLabel } from './label.js';
|
|
2
|
-
import 'jest-canvas-mock'; // Ensures canvas context is mocked
|
|
3
|
-
|
|
4
|
-
describe('addTrendlineLabel', () => {
|
|
5
|
-
let mockCtx;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
// Reset the mock before each test
|
|
9
|
-
mockCtx = {
|
|
10
|
-
save: jest.fn(),
|
|
11
|
-
translate: jest.fn(),
|
|
12
|
-
rotate: jest.fn(),
|
|
13
|
-
fillText: jest.fn(),
|
|
14
|
-
measureText: jest.fn(() => ({ width: 100 })), // Mock measureText to return a fixed width
|
|
15
|
-
font: '',
|
|
16
|
-
fillStyle: '',
|
|
17
|
-
restore: jest.fn(),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Define mock data directly as it's used in the function signature
|
|
22
|
-
const mockLabel = 'Test Label';
|
|
23
|
-
const mockX1 = 10;
|
|
24
|
-
const mockY1 = 20;
|
|
25
|
-
const mockX2 = 110;
|
|
26
|
-
const mockY2 = 120;
|
|
27
|
-
const mockAngle = Math.PI / 4; // 45 degrees
|
|
28
|
-
const mockLabelColor = 'red';
|
|
29
|
-
const mockFamily = 'Arial';
|
|
30
|
-
const mockSize = 12;
|
|
31
|
-
const mockOffset = 5;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
test('should correctly set label text and style', () => {
|
|
35
|
-
addTrendlineLabel(
|
|
36
|
-
mockCtx,
|
|
37
|
-
mockLabel,
|
|
38
|
-
mockX1, mockY1, mockX2, mockY2,
|
|
39
|
-
mockAngle,
|
|
40
|
-
mockLabelColor,
|
|
41
|
-
mockFamily,
|
|
42
|
-
mockSize,
|
|
43
|
-
mockOffset
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, expect.any(Number), expect.any(Number));
|
|
47
|
-
expect(mockCtx.measureText).toHaveBeenCalledWith(mockLabel);
|
|
48
|
-
expect(mockCtx.font).toBe(`${mockSize}px ${mockFamily}`);
|
|
49
|
-
expect(mockCtx.fillStyle).toBe(mockLabelColor);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('should correctly apply transformations and positioning', () => {
|
|
53
|
-
addTrendlineLabel(
|
|
54
|
-
mockCtx,
|
|
55
|
-
mockLabel,
|
|
56
|
-
mockX1, mockY1, mockX2, mockY2,
|
|
57
|
-
mockAngle,
|
|
58
|
-
mockLabelColor,
|
|
59
|
-
mockFamily,
|
|
60
|
-
mockSize,
|
|
61
|
-
mockOffset
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const midX = (mockX1 + mockX2) / 2;
|
|
65
|
-
const midY = (mockY1 + mockY2) / 2;
|
|
66
|
-
expect(mockCtx.translate).toHaveBeenCalledWith(midX, midY);
|
|
67
|
-
expect(mockCtx.rotate).toHaveBeenCalledWith(mockAngle);
|
|
68
|
-
|
|
69
|
-
const labelWidth = mockCtx.measureText(mockLabel).width; // From mockCtx setup
|
|
70
|
-
const adjustedX = -labelWidth / 2;
|
|
71
|
-
// The original implementation uses offset directly, not negated
|
|
72
|
-
const adjustedY = mockOffset;
|
|
73
|
-
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, adjustedX, adjustedY);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('should save and restore canvas state', () => {
|
|
77
|
-
addTrendlineLabel(
|
|
78
|
-
mockCtx,
|
|
79
|
-
mockLabel,
|
|
80
|
-
mockX1, mockY1, mockX2, mockY2,
|
|
81
|
-
mockAngle,
|
|
82
|
-
mockLabelColor,
|
|
83
|
-
mockFamily,
|
|
84
|
-
mockSize,
|
|
85
|
-
mockOffset
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(mockCtx.save).toHaveBeenCalledTimes(1);
|
|
89
|
-
expect(mockCtx.restore).toHaveBeenCalledTimes(1);
|
|
90
|
-
// We can infer call order by checking if save was called and restore was called.
|
|
91
|
-
// For more specific ordering, a more complex mock or spy setup would be needed,
|
|
92
|
-
// or checking the order of calls on the mock object if the testing library supports it.
|
|
93
|
-
// For now, checking they were called is sufficient given the function's structure.
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('should handle zero offset correctly', () => {
|
|
97
|
-
const currentOffset = 0;
|
|
98
|
-
addTrendlineLabel(
|
|
99
|
-
mockCtx,
|
|
100
|
-
mockLabel,
|
|
101
|
-
mockX1, mockY1, mockX2, mockY2,
|
|
102
|
-
mockAngle,
|
|
103
|
-
mockLabelColor,
|
|
104
|
-
mockFamily,
|
|
105
|
-
mockSize,
|
|
106
|
-
currentOffset // Using zero offset
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const labelWidth = mockCtx.measureText(mockLabel).width;
|
|
110
|
-
const adjustedX = -labelWidth / 2;
|
|
111
|
-
const adjustedY = currentOffset; // y is the offset itself
|
|
112
|
-
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, adjustedX, adjustedY);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('should handle different angle correctly', () => {
|
|
116
|
-
const currentAngle = Math.PI / 2; // 90 degrees
|
|
117
|
-
addTrendlineLabel(
|
|
118
|
-
mockCtx,
|
|
119
|
-
mockLabel,
|
|
120
|
-
mockX1, mockY1, mockX2, mockY2,
|
|
121
|
-
currentAngle, // Using different angle
|
|
122
|
-
mockLabelColor,
|
|
123
|
-
mockFamily,
|
|
124
|
-
mockSize,
|
|
125
|
-
mockOffset
|
|
126
|
-
);
|
|
127
|
-
expect(mockCtx.rotate).toHaveBeenCalledWith(currentAngle);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
1
|
+
import { addTrendlineLabel } from './label.js';
|
|
2
|
+
import 'jest-canvas-mock'; // Ensures canvas context is mocked
|
|
3
|
+
|
|
4
|
+
describe('addTrendlineLabel', () => {
|
|
5
|
+
let mockCtx;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Reset the mock before each test
|
|
9
|
+
mockCtx = {
|
|
10
|
+
save: jest.fn(),
|
|
11
|
+
translate: jest.fn(),
|
|
12
|
+
rotate: jest.fn(),
|
|
13
|
+
fillText: jest.fn(),
|
|
14
|
+
measureText: jest.fn(() => ({ width: 100 })), // Mock measureText to return a fixed width
|
|
15
|
+
font: '',
|
|
16
|
+
fillStyle: '',
|
|
17
|
+
restore: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Define mock data directly as it's used in the function signature
|
|
22
|
+
const mockLabel = 'Test Label';
|
|
23
|
+
const mockX1 = 10;
|
|
24
|
+
const mockY1 = 20;
|
|
25
|
+
const mockX2 = 110;
|
|
26
|
+
const mockY2 = 120;
|
|
27
|
+
const mockAngle = Math.PI / 4; // 45 degrees
|
|
28
|
+
const mockLabelColor = 'red';
|
|
29
|
+
const mockFamily = 'Arial';
|
|
30
|
+
const mockSize = 12;
|
|
31
|
+
const mockOffset = 5;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
test('should correctly set label text and style', () => {
|
|
35
|
+
addTrendlineLabel(
|
|
36
|
+
mockCtx,
|
|
37
|
+
mockLabel,
|
|
38
|
+
mockX1, mockY1, mockX2, mockY2,
|
|
39
|
+
mockAngle,
|
|
40
|
+
mockLabelColor,
|
|
41
|
+
mockFamily,
|
|
42
|
+
mockSize,
|
|
43
|
+
mockOffset
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, expect.any(Number), expect.any(Number));
|
|
47
|
+
expect(mockCtx.measureText).toHaveBeenCalledWith(mockLabel);
|
|
48
|
+
expect(mockCtx.font).toBe(`${mockSize}px ${mockFamily}`);
|
|
49
|
+
expect(mockCtx.fillStyle).toBe(mockLabelColor);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should correctly apply transformations and positioning', () => {
|
|
53
|
+
addTrendlineLabel(
|
|
54
|
+
mockCtx,
|
|
55
|
+
mockLabel,
|
|
56
|
+
mockX1, mockY1, mockX2, mockY2,
|
|
57
|
+
mockAngle,
|
|
58
|
+
mockLabelColor,
|
|
59
|
+
mockFamily,
|
|
60
|
+
mockSize,
|
|
61
|
+
mockOffset
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const midX = (mockX1 + mockX2) / 2;
|
|
65
|
+
const midY = (mockY1 + mockY2) / 2;
|
|
66
|
+
expect(mockCtx.translate).toHaveBeenCalledWith(midX, midY);
|
|
67
|
+
expect(mockCtx.rotate).toHaveBeenCalledWith(mockAngle);
|
|
68
|
+
|
|
69
|
+
const labelWidth = mockCtx.measureText(mockLabel).width; // From mockCtx setup
|
|
70
|
+
const adjustedX = -labelWidth / 2;
|
|
71
|
+
// The original implementation uses offset directly, not negated
|
|
72
|
+
const adjustedY = mockOffset;
|
|
73
|
+
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, adjustedX, adjustedY);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should save and restore canvas state', () => {
|
|
77
|
+
addTrendlineLabel(
|
|
78
|
+
mockCtx,
|
|
79
|
+
mockLabel,
|
|
80
|
+
mockX1, mockY1, mockX2, mockY2,
|
|
81
|
+
mockAngle,
|
|
82
|
+
mockLabelColor,
|
|
83
|
+
mockFamily,
|
|
84
|
+
mockSize,
|
|
85
|
+
mockOffset
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(mockCtx.save).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(mockCtx.restore).toHaveBeenCalledTimes(1);
|
|
90
|
+
// We can infer call order by checking if save was called and restore was called.
|
|
91
|
+
// For more specific ordering, a more complex mock or spy setup would be needed,
|
|
92
|
+
// or checking the order of calls on the mock object if the testing library supports it.
|
|
93
|
+
// For now, checking they were called is sufficient given the function's structure.
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should handle zero offset correctly', () => {
|
|
97
|
+
const currentOffset = 0;
|
|
98
|
+
addTrendlineLabel(
|
|
99
|
+
mockCtx,
|
|
100
|
+
mockLabel,
|
|
101
|
+
mockX1, mockY1, mockX2, mockY2,
|
|
102
|
+
mockAngle,
|
|
103
|
+
mockLabelColor,
|
|
104
|
+
mockFamily,
|
|
105
|
+
mockSize,
|
|
106
|
+
currentOffset // Using zero offset
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const labelWidth = mockCtx.measureText(mockLabel).width;
|
|
110
|
+
const adjustedX = -labelWidth / 2;
|
|
111
|
+
const adjustedY = currentOffset; // y is the offset itself
|
|
112
|
+
expect(mockCtx.fillText).toHaveBeenCalledWith(mockLabel, adjustedX, adjustedY);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should handle different angle correctly', () => {
|
|
116
|
+
const currentAngle = Math.PI / 2; // 90 degrees
|
|
117
|
+
addTrendlineLabel(
|
|
118
|
+
mockCtx,
|
|
119
|
+
mockLabel,
|
|
120
|
+
mockX1, mockY1, mockX2, mockY2,
|
|
121
|
+
currentAngle, // Using different angle
|
|
122
|
+
mockLabelColor,
|
|
123
|
+
mockFamily,
|
|
124
|
+
mockSize,
|
|
125
|
+
mockOffset
|
|
126
|
+
);
|
|
127
|
+
expect(mockCtx.rotate).toHaveBeenCalledWith(currentAngle);
|
|
128
|
+
});
|
|
129
|
+
});
|