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,79 +1,78 @@
1
- import { addFitter } from '../components/trendline.js';
2
- import { getScales } from '../utils/drawing.js';
3
-
4
- export const pluginTrendlineLinear = {
5
- id: 'chartjs-plugin-trendline',
6
-
7
- afterDatasetsDraw: (chartInstance) => {
8
- const ctx = chartInstance.ctx;
9
- const { xScale, yScale } = getScales(chartInstance);
10
-
11
- const sortedDatasets = chartInstance.data.datasets
12
- .map((dataset, index) => ({ dataset, index }))
13
- .filter((entry) => entry.dataset.trendlineLinear || entry.dataset.trendlineExponential)
14
- .sort((a, b) => {
15
- const orderA = a.dataset.order ?? 0;
16
- const orderB = b.dataset.order ?? 0;
17
-
18
- // Push 0-order datasets to the end (they draw last / on top)
19
- if (orderA === 0 && orderB !== 0) return 1;
20
- if (orderB === 0 && orderA !== 0) return -1;
21
-
22
- // Otherwise, draw lower order first
23
- return orderA - orderB;
24
- });
25
-
26
- sortedDatasets.forEach(({ dataset, index }) => {
27
- const showTrendline =
28
- dataset.alwaysShowTrendline ||
29
- chartInstance.isDatasetVisible(index);
30
-
31
- if (showTrendline && dataset.data.length > 1) {
32
- const datasetMeta = chartInstance.getDatasetMeta(index);
33
- addFitter(datasetMeta, ctx, dataset, xScale, yScale);
34
- }
35
- });
36
-
37
- // Reset to solid line after drawing trendline
38
- ctx.setLineDash([]);
39
- },
40
-
41
- beforeInit: (chartInstance) => {
42
- const datasets = chartInstance.data.datasets;
43
-
44
- datasets.forEach((dataset) => {
45
- const trendlineConfig = dataset.trendlineLinear || dataset.trendlineExponential;
46
- if (trendlineConfig && trendlineConfig.label) {
47
- const label = trendlineConfig.label;
48
-
49
- // Access chartInstance to update legend labels
50
- const originalGenerateLabels =
51
- chartInstance.legend.options.labels.generateLabels;
52
-
53
- chartInstance.legend.options.labels.generateLabels = function (
54
- chart
55
- ) {
56
- const defaultLabels = originalGenerateLabels(chart);
57
-
58
- const legendConfig = trendlineConfig.legend;
59
-
60
- // Display the legend is it's populated and not set to hidden
61
- if (legendConfig && legendConfig.display !== false) {
62
- defaultLabels.push({
63
- text: legendConfig.text || label + ' (Trendline)',
64
- strokeStyle:
65
- legendConfig.color ||
66
- dataset.borderColor ||
67
- 'rgba(169,169,169, .6)',
68
- fillStyle: legendConfig.fillStyle || 'transparent',
69
- lineCap: legendConfig.lineCap || 'butt',
70
- lineDash: legendConfig.lineDash || [],
71
- lineWidth: legendConfig.width || 1,
72
- });
73
- }
74
- return defaultLabels;
75
- };
76
- }
77
- });
78
- },
79
- };
1
+ import { addFitter } from '../components/trendline.js';
2
+ import { getScales } from '../utils/drawing.js';
3
+
4
+ export const pluginTrendlineLinear = {
5
+ id: 'chartjs-plugin-trendline',
6
+
7
+ afterDatasetsDraw: (chartInstance) => {
8
+ const ctx = chartInstance.ctx;
9
+ const { xScale, yScale } = getScales(chartInstance);
10
+
11
+ const sortedDatasets = chartInstance.data.datasets
12
+ .map((dataset, index) => ({ dataset, index }))
13
+ .filter((entry) => entry.dataset.trendlineLinear || entry.dataset.trendlineExponential)
14
+ .sort((a, b) => {
15
+ const orderA = a.dataset.order ?? 0;
16
+ const orderB = b.dataset.order ?? 0;
17
+
18
+ // Push 0-order datasets to the end (they draw last / on top)
19
+ if (orderA === 0 && orderB !== 0) return 1;
20
+ if (orderB === 0 && orderA !== 0) return -1;
21
+
22
+ // Otherwise, draw lower order first
23
+ return orderA - orderB;
24
+ });
25
+
26
+ sortedDatasets.forEach(({ dataset, index }) => {
27
+ const showTrendline =
28
+ dataset.alwaysShowTrendline ||
29
+ chartInstance.isDatasetVisible(index);
30
+
31
+ if (showTrendline && dataset.data.length > 1) {
32
+ const datasetMeta = chartInstance.getDatasetMeta(index);
33
+ addFitter(datasetMeta, ctx, dataset, xScale, yScale);
34
+ }
35
+ });
36
+
37
+ // Reset to solid line after drawing trendline
38
+ ctx.setLineDash([]);
39
+ },
40
+
41
+ beforeInit: (chartInstance) => {
42
+ const datasets = chartInstance.data.datasets;
43
+
44
+ datasets.forEach((dataset) => {
45
+ const trendlineConfig = dataset.trendlineLinear || dataset.trendlineExponential;
46
+ if (trendlineConfig && (trendlineConfig.label || trendlineConfig.legend)) {
47
+ // Access chartInstance to update legend labels
48
+ const originalGenerateLabels =
49
+ chartInstance.legend.options.labels.generateLabels;
50
+
51
+ chartInstance.legend.options.labels.generateLabels = function (
52
+ chart
53
+ ) {
54
+ const defaultLabels = originalGenerateLabels(chart);
55
+
56
+ const legendConfig = trendlineConfig.legend;
57
+
58
+ // Display the legend if it's populated and not set to hidden
59
+ if (legendConfig && legendConfig.display !== false) {
60
+ defaultLabels.push({
61
+ text: legendConfig.text || dataset.label || 'Trendline',
62
+ strokeStyle:
63
+ legendConfig.strokeStyle ||
64
+ legendConfig.color ||
65
+ dataset.borderColor ||
66
+ 'rgba(169,169,169, .6)',
67
+ fillStyle: legendConfig.fillStyle || 'transparent',
68
+ lineCap: legendConfig.lineCap || 'butt',
69
+ lineDash: legendConfig.lineDash || [],
70
+ lineWidth: legendConfig.lineWidth ?? legendConfig.width ?? 1,
71
+ });
72
+ }
73
+ return defaultLabels;
74
+ };
75
+ }
76
+ });
77
+ },
78
+ };
@@ -0,0 +1,307 @@
1
+ import { pluginTrendlineLinear } from './plugin.js';
2
+
3
+ describe('pluginTrendlineLinear.beforeInit - legend generation', () => {
4
+ let chartInstance;
5
+ let originalGenerateLabels;
6
+
7
+ beforeEach(() => {
8
+ originalGenerateLabels = jest.fn(() => [{ text: 'Dataset 1' }]);
9
+ chartInstance = {
10
+ data: { datasets: [] },
11
+ legend: {
12
+ options: {
13
+ labels: {
14
+ generateLabels: originalGenerateLabels,
15
+ },
16
+ },
17
+ },
18
+ };
19
+ });
20
+
21
+ /**
22
+ * Bug 1: Legend item must appear even when trendlineConfig.label is absent,
23
+ * as long as trendlineConfig.legend is configured.
24
+ */
25
+ it('adds a legend item when only trendlineConfig.legend is present (no label key)', () => {
26
+ chartInstance.data.datasets = [
27
+ {
28
+ label: 'My Dataset',
29
+ borderColor: '#2196F3',
30
+ trendlineLinear: {
31
+ // No `label` key - this was the bug trigger
32
+ legend: {
33
+ text: 'Trend',
34
+ strokeStyle: '#ff6b6b',
35
+ },
36
+ },
37
+ },
38
+ ];
39
+
40
+ pluginTrendlineLinear.beforeInit(chartInstance);
41
+
42
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
43
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
44
+ expect(trendlineLabel).toBeDefined();
45
+ });
46
+
47
+ it('does not add a legend item when neither label nor legend key is present', () => {
48
+ chartInstance.data.datasets = [
49
+ {
50
+ label: 'My Dataset',
51
+ trendlineLinear: {
52
+ colorMin: 'red',
53
+ },
54
+ },
55
+ ];
56
+
57
+ pluginTrendlineLinear.beforeInit(chartInstance);
58
+
59
+ // generateLabels should not have been replaced
60
+ expect(chartInstance.legend.options.labels.generateLabels).toBe(originalGenerateLabels);
61
+ });
62
+
63
+ it('still adds a legend item when only trendlineConfig.label is present (backwards compat)', () => {
64
+ chartInstance.data.datasets = [
65
+ {
66
+ label: 'My Dataset',
67
+ trendlineLinear: {
68
+ label: { display: false },
69
+ legend: { text: 'Trend' },
70
+ },
71
+ },
72
+ ];
73
+
74
+ pluginTrendlineLinear.beforeInit(chartInstance);
75
+
76
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
77
+ expect(labels.some((l) => l.text === 'Trend')).toBe(true);
78
+ });
79
+
80
+ /**
81
+ * Bug 2: legendConfig.strokeStyle should be respected.
82
+ */
83
+ it('uses legendConfig.strokeStyle for the legend item strokeStyle', () => {
84
+ chartInstance.data.datasets = [
85
+ {
86
+ borderColor: '#2196F3',
87
+ trendlineLinear: {
88
+ legend: {
89
+ text: 'Trend',
90
+ strokeStyle: '#ff6b6b',
91
+ },
92
+ },
93
+ },
94
+ ];
95
+
96
+ pluginTrendlineLinear.beforeInit(chartInstance);
97
+
98
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
99
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
100
+ expect(trendlineLabel.strokeStyle).toBe('#ff6b6b');
101
+ });
102
+
103
+ it('falls back to legendConfig.color for strokeStyle when strokeStyle is absent', () => {
104
+ chartInstance.data.datasets = [
105
+ {
106
+ borderColor: '#2196F3',
107
+ trendlineLinear: {
108
+ legend: {
109
+ text: 'Trend',
110
+ color: '#aabbcc',
111
+ },
112
+ },
113
+ },
114
+ ];
115
+
116
+ pluginTrendlineLinear.beforeInit(chartInstance);
117
+
118
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
119
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
120
+ expect(trendlineLabel.strokeStyle).toBe('#aabbcc');
121
+ });
122
+
123
+ it('falls back to dataset.borderColor for strokeStyle when neither strokeStyle nor color is set', () => {
124
+ chartInstance.data.datasets = [
125
+ {
126
+ borderColor: '#2196F3',
127
+ trendlineLinear: {
128
+ legend: { text: 'Trend' },
129
+ },
130
+ },
131
+ ];
132
+
133
+ pluginTrendlineLinear.beforeInit(chartInstance);
134
+
135
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
136
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
137
+ expect(trendlineLabel.strokeStyle).toBe('#2196F3');
138
+ });
139
+
140
+ it('falls back to default gray strokeStyle when no color source is available', () => {
141
+ chartInstance.data.datasets = [
142
+ {
143
+ trendlineLinear: {
144
+ legend: { text: 'Trend' },
145
+ },
146
+ },
147
+ ];
148
+
149
+ pluginTrendlineLinear.beforeInit(chartInstance);
150
+
151
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
152
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
153
+ expect(trendlineLabel.strokeStyle).toBe('rgba(169,169,169, .6)');
154
+ });
155
+
156
+ /**
157
+ * Bug 3: legendConfig.lineWidth should control the legend item line width.
158
+ */
159
+ it('uses legendConfig.lineWidth for the legend item lineWidth', () => {
160
+ chartInstance.data.datasets = [
161
+ {
162
+ trendlineLinear: {
163
+ legend: {
164
+ text: 'Trend',
165
+ lineWidth: 3,
166
+ },
167
+ },
168
+ },
169
+ ];
170
+
171
+ pluginTrendlineLinear.beforeInit(chartInstance);
172
+
173
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
174
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
175
+ expect(trendlineLabel.lineWidth).toBe(3);
176
+ });
177
+
178
+ it('allows legendConfig.lineWidth of 0 to hide the legend border', () => {
179
+ chartInstance.data.datasets = [
180
+ {
181
+ trendlineLinear: {
182
+ legend: {
183
+ text: 'Trend',
184
+ lineWidth: 0,
185
+ },
186
+ },
187
+ },
188
+ ];
189
+
190
+ pluginTrendlineLinear.beforeInit(chartInstance);
191
+
192
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
193
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
194
+ expect(trendlineLabel.lineWidth).toBe(0);
195
+ });
196
+
197
+ it('falls back to legendConfig.width for lineWidth when lineWidth is absent', () => {
198
+ chartInstance.data.datasets = [
199
+ {
200
+ trendlineLinear: {
201
+ legend: {
202
+ text: 'Trend',
203
+ width: 2,
204
+ },
205
+ },
206
+ },
207
+ ];
208
+
209
+ pluginTrendlineLinear.beforeInit(chartInstance);
210
+
211
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
212
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
213
+ expect(trendlineLabel.lineWidth).toBe(2);
214
+ });
215
+
216
+ it('defaults lineWidth to 1 when neither lineWidth nor width is set', () => {
217
+ chartInstance.data.datasets = [
218
+ {
219
+ trendlineLinear: {
220
+ legend: { text: 'Trend' },
221
+ },
222
+ },
223
+ ];
224
+
225
+ pluginTrendlineLinear.beforeInit(chartInstance);
226
+
227
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
228
+ const trendlineLabel = labels.find((l) => l.text === 'Trend');
229
+ expect(trendlineLabel.lineWidth).toBe(1);
230
+ });
231
+
232
+ /**
233
+ * Legend text fallback behaviour.
234
+ */
235
+ it('uses dataset.label as text fallback when legendConfig.text is absent', () => {
236
+ chartInstance.data.datasets = [
237
+ {
238
+ label: 'Balance',
239
+ trendlineLinear: {
240
+ legend: { strokeStyle: '#ff0000' },
241
+ },
242
+ },
243
+ ];
244
+
245
+ pluginTrendlineLinear.beforeInit(chartInstance);
246
+
247
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
248
+ const trendlineLabel = labels.find((l) => l.text === 'Balance');
249
+ expect(trendlineLabel).toBeDefined();
250
+ });
251
+
252
+ it('falls back to "Trendline" text when neither legendConfig.text nor dataset.label is set', () => {
253
+ chartInstance.data.datasets = [
254
+ {
255
+ trendlineLinear: {
256
+ legend: { strokeStyle: '#ff0000' },
257
+ },
258
+ },
259
+ ];
260
+
261
+ pluginTrendlineLinear.beforeInit(chartInstance);
262
+
263
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
264
+ const trendlineLabel = labels.find((l) => l.text === 'Trendline');
265
+ expect(trendlineLabel).toBeDefined();
266
+ });
267
+
268
+ it('does not add legend item when legendConfig.display is false', () => {
269
+ chartInstance.data.datasets = [
270
+ {
271
+ label: 'My Dataset',
272
+ trendlineLinear: {
273
+ legend: { text: 'Trend', display: false },
274
+ },
275
+ },
276
+ ];
277
+
278
+ pluginTrendlineLinear.beforeInit(chartInstance);
279
+
280
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
281
+ expect(labels.some((l) => l.text === 'Trend')).toBe(false);
282
+ });
283
+
284
+ it('works with trendlineExponential config', () => {
285
+ chartInstance.data.datasets = [
286
+ {
287
+ label: 'Exp Dataset',
288
+ borderColor: '#00ff00',
289
+ trendlineExponential: {
290
+ legend: {
291
+ text: 'Exp Trend',
292
+ strokeStyle: '#ff0000',
293
+ lineWidth: 2,
294
+ },
295
+ },
296
+ },
297
+ ];
298
+
299
+ pluginTrendlineLinear.beforeInit(chartInstance);
300
+
301
+ const labels = chartInstance.legend.options.labels.generateLabels(chartInstance);
302
+ const trendlineLabel = labels.find((l) => l.text === 'Exp Trend');
303
+ expect(trendlineLabel).toBeDefined();
304
+ expect(trendlineLabel.strokeStyle).toBe('#ff0000');
305
+ expect(trendlineLabel.lineWidth).toBe(2);
306
+ });
307
+ });
package/src/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { pluginTrendlineLinear } from './core/plugin.js';
2
-
3
- // If we're in the browser and have access to the global Chart obj, register plugin automatically
4
- if (typeof window !== 'undefined' && window.Chart) {
5
- if (window.Chart.hasOwnProperty('register')) {
6
- window.Chart.register(pluginTrendlineLinear);
7
- } else {
8
- window.Chart.plugins.register(pluginTrendlineLinear);
9
- }
10
- }
11
-
12
- // Export the plugin
1
+ import { pluginTrendlineLinear } from './core/plugin.js';
2
+
3
+ // If we're in the browser and have access to the global Chart obj, register plugin automatically
4
+ if (typeof window !== 'undefined' && window.Chart) {
5
+ if (window.Chart.hasOwnProperty('register')) {
6
+ window.Chart.register(pluginTrendlineLinear);
7
+ } else {
8
+ window.Chart.plugins.register(pluginTrendlineLinear);
9
+ }
10
+ }
11
+
12
+ // Export the plugin
13
13
  export default pluginTrendlineLinear;