chartjs-plugin-trendline 2.1.6 → 2.1.8

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Marcus Alsterfjord
3
+ Copyright (c) 2025 Marcus Alsterfjord
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -34,7 +34,7 @@ To configure the trendline plugin you simply add a new config options to your da
34
34
  ```javascript
35
35
  {
36
36
  trendlineLinear: {
37
- colorMin: Color
37
+ colorMin: Color,
38
38
  colorMax: Color,
39
39
  lineStyle: string, // "dotted" | "solid" | "dashed" | "dashdot"
40
40
  width: number,
@@ -81,3 +81,7 @@ For bugs and feature requests, [please create an issue](https://github.com/Makan
81
81
  ## License
82
82
 
83
83
  chartjs-plugin-trendline.js is available under the [MIT license](http://opensource.org/licenses/MIT).
84
+
85
+ ## Star History
86
+
87
+ [![Star History Chart](https://api.star-history.com/svg?repos=Makanz/chartjs-plugin-trendline&type=Date)](https://star-history.com/#Makanz/chartjs-plugin-trendline&Date)
@@ -1,2 +1,2 @@
1
- /*! For license information please see chartjs-plugin-trendline.min.js.LICENSE.txt */
2
- (()=>{var e={460:(e,t)=>{const i={id:"chartjs-plugin-trendline",afterDatasetsDraw:e=>{const t=e.ctx,{xScale:i,yScale:n}=a(e);e.data.datasets.forEach(((a,l)=>{const r=a.alwaysShowTrendline||e.isDatasetVisible(l);if(a.trendlineLinear&&r&&a.data.length>1){const r=e.getDatasetMeta(l);s(r,t,a,i,n)}})),t.setLineDash([])},beforeInit:e=>{e.data.datasets.forEach((t=>{if(t.trendlineLinear&&t.trendlineLinear.label){const i=t.trendlineLinear.label,a=e.legend.options.labels.generateLabels;e.legend.options.labels.generateLabels=function(e){const s=a(e),n=t.trendlineLinear.legend;return n&&!1!==n.display&&s.push({text:n.text||i+" (Trendline)",strokeStyle:n.color||t.borderColor||"rgba(169,169,169, .6)",fillStyle:n.fillStyle||"transparent",lineCap:n.lineCap||"butt",lineDash:n.lineDash||[],lineWidth:n.width||1}),s}}}))}},a=e=>{let t,i;for(const a of Object.values(e.scales))if(a.isHorizontal()?t=a:i=a,t&&i)break;return{xScale:t,yScale:i}},s=(e,t,i,a,s)=>{const x=i.borderColor||"rgba(169,169,169, .6)",{colorMin:c=x,colorMax:u=x,width:y=i.borderWidth||3,lineStyle:f="solid",fillColor:m=!1}=i.trendlineLinear||{},{color:p=x,text:b="Trendline",display:g=!0,displayValue:w=!0,offset:L=10,percentage:S=!1}=i.trendlineLinear.label||{},{family:T="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:v=12}=i.trendlineLinear.label?.font||{},D=e.controller.chart.options,N="object"==typeof D.parsing?D.parsing:void 0,C=i.trendlineLinear?.xAxisKey||N?.xAxisKey||"x",P=i.trendlineLinear?.yAxisKey||N?.yAxisKey||"y";let F=new h,M=i.data.findIndex((e=>null!=e)),V=i.data.length-1,A=e.data[M]?.[C],k=e.data[V]?.[C],j="object"==typeof i.data[M];i.data.forEach(((e,t)=>{if(null!=e)if(["time","timeseries"].includes(a.options.type)){let i=null!=e[C]?e[C]:e.t;void 0!==i?F.add(new Date(i).getTime(),e[P]):F.add(t,e)}else j?isNaN(e.x)||isNaN(e.y)?isNaN(e.x)?isNaN(e.y)||F.add(t,e.y):F.add(t,e.x):F.add(e.x,e.y):F.add(t,e)}));let E,W,K=a.getPixelForValue(F.minx),$=s.getPixelForValue(F.f(F.minx));if(i.trendlineLinear.projection&&F.scale()<0){let e=F.fo();e<F.minx&&(e=F.maxx),E=a.getPixelForValue(e),W=s.getPixelForValue(F.f(e))}else E=a.getPixelForValue(F.maxx),W=s.getPixelForValue(F.f(F.maxx));(isFinite(A)||isFinite(k))&&(K=A,E=k);const H=e.controller.chart.chartArea.bottom,I=e.controller.chart.width;l({x1:K,y1:$,x2:E,y2:W,drawBottom:H,chartWidth:I}),t.lineWidth=y,r(t,f),o({ctx:t,x1:K,y1:$,x2:E,y2:W,colorMin:c,colorMax:u}),m&&d(t,K,$,E,W,H,m);const z=Math.atan2(W-$,E-K),B=($-W)/(E-K);if(i.trendlineLinear.label&&!1!==g){const e=w?`${b} (Slope: ${S?(100*B).toFixed(2)+"%":B.toFixed(2)})`:b;n(t,e,K,$,E,W,z,p,T,v,L)}},n=(e,t,i,a,s,n,l,r,o,d,h)=>{e.font=`${d}px ${o}`,e.fillStyle=r;const x=e.measureText(t).width,c=(i+s)/2,u=(a+n)/2;e.save(),e.translate(c,u),e.rotate(l);const y=-x/2,f=h;e.fillText(t,y,f),e.restore()},l=({x1:e,y1:t,x2:i,y2:a,drawBottom:s,chartWidth:n})=>{if(t>s){t=s}else if(a>s){let e=a-s,l=a-t;a=s,i=n-(i-(n-n*(e/l)))}},r=(e,t)=>{switch(t){case"dotted":e.setLineDash([2,2]);break;case"dashed":e.setLineDash([8,3]);break;case"dashdot":e.setLineDash([8,3,2,3]);break;default:e.setLineDash([])}},o=({ctx:e,x1:t,y1:i,x2:a,y2:s,colorMin:n,colorMax:l})=>{e.beginPath(),e.moveTo(t,i),e.lineTo(a,s);let r=e.createLinearGradient(t,i,a,s);r.addColorStop(0,n),r.addColorStop(1,l),e.strokeStyle=r,e.stroke(),e.closePath()},d=(e,t,i,a,s,n,l)=>{e.beginPath(),e.moveTo(t,i),e.lineTo(a,s),e.lineTo(a,n),e.lineTo(t,n),e.lineTo(t,i),e.closePath(),e.fillStyle=l,e.fill()};class h{constructor(){this.count=0,this.sumx=0,this.sumy=0,this.sumx2=0,this.sumxy=0,this.minx=Number.MAX_VALUE,this.maxx=Number.MIN_VALUE}add(e,t){this.sumx+=e,this.sumy+=t,this.sumx2+=e*e,this.sumxy+=e*t,e<this.minx&&(this.minx=e),e>this.maxx&&(this.maxx=e),this.count++}slope(){const e=this.count*this.sumx2-this.sumx*this.sumx;return(this.count*this.sumxy-this.sumx*this.sumy)/e}intercept(){return(this.sumy-this.slope()*this.sumx)/this.count}f(e){return this.slope()*e+this.intercept()}fo(){return-this.intercept()/this.slope()}scale(){return this.slope()}}"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(i):window.Chart.plugins.register(i));try{e.exports=i}catch(e){}}},t={};!function i(a){var s=t[a];if(void 0!==s)return s.exports;var n=t[a]={exports:{}};return e[a](n,n.exports,i),n.exports}(460)})();
1
+ /*! For license information please see chartjs-plugin-trendline.min.js.LICENSE.txt */
2
+ (()=>{var e={339:(e,t)=>{const i={id:"chartjs-plugin-trendline",afterDatasetsDraw:e=>{const t=e.ctx,{xScale:i,yScale:a}=n(e);e.data.datasets.forEach(((n,l)=>{const o=n.alwaysShowTrendline||e.isDatasetVisible(l);if(n.trendlineLinear&&o&&n.data.length>1){const o=e.getDatasetMeta(l);s(o,t,n,i,a)}})),t.setLineDash([])},beforeInit:e=>{e.data.datasets.forEach((t=>{if(t.trendlineLinear&&t.trendlineLinear.label){const i=t.trendlineLinear.label,n=e.legend.options.labels.generateLabels;e.legend.options.labels.generateLabels=function(e){const s=n(e),a=t.trendlineLinear.legend;return a&&!1!==a.display&&s.push({text:a.text||i+" (Trendline)",strokeStyle:a.color||t.borderColor||"rgba(169,169,169, .6)",fillStyle:a.fillStyle||"transparent",lineCap:a.lineCap||"butt",lineDash:a.lineDash||[],lineWidth:a.width||1}),s}}}))}},n=e=>{let t,i;for(const n of Object.values(e.scales))if(n.isHorizontal()?t=n:i=n,t&&i)break;return{xScale:t,yScale:i}},s=(e,t,i,n,s)=>{const x=i.yAxisID||"y",h=e.controller.chart.scales[x]||s,u=i.borderColor||"rgba(169,169,169, .6)",{colorMin:y=u,colorMax:f=u,width:m=i.borderWidth||3,lineStyle:p="solid",fillColor:b=!1}=i.trendlineLinear||{},{color:w=u,text:g="Trendline",display:F=!0,displayValue:L=!0,offset:S=10,percentage:v=!1}=i.trendlineLinear.label||{},{family:C="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:D=12}=i.trendlineLinear.label?.font||{},T=e.controller.chart.options,N="object"==typeof T.parsing?T.parsing:void 0,P=i.trendlineLinear?.xAxisKey||N?.xAxisKey||"x",A=i.trendlineLinear?.yAxisKey||N?.yAxisKey||"y";let M=new c,V=i.data.findIndex((e=>null!=e)),k=i.data.length-1,j=e.data[V]?.[P],E=e.data[k]?.[P],W="object"==typeof i.data[V];i.data.forEach(((e,t)=>{if(null!=e)if(["time","timeseries"].includes(n.options.type)){let i=null!=e[P]?e[P]:e.t;void 0!==i?M.add(new Date(i).getTime(),e[A]):M.add(t,e)}else W?isNaN(e.x)||isNaN(e.y)?isNaN(e.x)?isNaN(e.y)||M.add(t,e.y):M.add(t,e.x):M.add(e.x,e.y):M.add(t,e)}));let I,K,$=isFinite(j)?j:n.getPixelForValue(M.minx),B=h.getPixelForValue(M.f(M.minx));if(i.trendlineLinear.projection&&M.scale()<0){let e=M.fo();e<M.minx&&(e=M.maxx),I=n.getPixelForValue(e),K=h.getPixelForValue(M.f(e))}else I=isFinite(E)?E:n.getPixelForValue(M.maxx),K=h.getPixelForValue(M.f(M.maxx));const H=e.controller.chart.chartArea.bottom,z=e.controller.chart.width;if(isFinite($)&&isFinite(B)&&isFinite(I)&&isFinite(K)){l({x1:$,y1:B,x2:I,y2:K,drawBottom:H,chartWidth:z}),t.lineWidth=m,o(t,p),r({ctx:t,x1:$,y1:B,x2:I,y2:K,colorMin:y,colorMax:f}),b&&d(t,$,B,I,K,H,b);const e=Math.atan2(K-B,I-$),n=(B-K)/(I-$);if(i.trendlineLinear.label&&!1!==F){const i=L?`${g} (Slope: ${v?(100*n).toFixed(2)+"%":n.toFixed(2)})`:g;a(t,i,$,B,I,K,e,w,C,D,S)}}},a=(e,t,i,n,s,a,l,o,r,d,c)=>{e.font=`${d}px ${r}`,e.fillStyle=o;const x=e.measureText(t).width,h=(i+s)/2,u=(n+a)/2;e.save(),e.translate(h,u),e.rotate(l);const y=-x/2,f=c;e.fillText(t,y,f),e.restore()},l=({x1:e,y1:t,x2:i,y2:n,drawBottom:s,chartWidth:a})=>{if(t>s){t=s}else if(n>s){let e=n-s,l=n-t;n=s,i=a-(i-(a-a*(e/l)))}},o=(e,t)=>{switch(t){case"dotted":e.setLineDash([2,2]);break;case"dashed":e.setLineDash([8,3]);break;case"dashdot":e.setLineDash([8,3,2,3]);break;default:e.setLineDash([])}},r=({ctx:e,x1:t,y1:i,x2:n,y2:s,colorMin:a,colorMax:l})=>{if(isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(s)){e.beginPath(),e.moveTo(t,i),e.lineTo(n,s);try{let o=e.createLinearGradient(t,i,n,s);o.addColorStop(0,a),o.addColorStop(1,l),e.strokeStyle=o}catch(t){console.warn("Gradient creation failed, using solid color:",t),e.strokeStyle=a}e.stroke(),e.closePath()}else console.warn("Cannot draw trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:s})},d=(e,t,i,n,s,a,l)=>{isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(s)&&isFinite(a)?(e.beginPath(),e.moveTo(t,i),e.lineTo(n,s),e.lineTo(n,a),e.lineTo(t,a),e.lineTo(t,i),e.closePath(),e.fillStyle=l,e.fill()):console.warn("Cannot fill below trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:s,drawBottom:a})};class c{constructor(){this.count=0,this.sumx=0,this.sumy=0,this.sumx2=0,this.sumxy=0,this.minx=Number.MAX_VALUE,this.maxx=Number.MIN_VALUE}add(e,t){this.sumx+=e,this.sumy+=t,this.sumx2+=e*e,this.sumxy+=e*t,e<this.minx&&(this.minx=e),e>this.maxx&&(this.maxx=e),this.count++}slope(){const e=this.count*this.sumx2-this.sumx*this.sumx;return(this.count*this.sumxy-this.sumx*this.sumy)/e}intercept(){return(this.sumy-this.slope()*this.sumx)/this.count}f(e){return this.slope()*e+this.intercept()}fo(){return-this.intercept()/this.slope()}scale(){return this.slope()}}"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(i):window.Chart.plugins.register(i));try{e.exports=i}catch(e){}}},t={};!function i(n){var s=t[n];if(void 0!==s)return s.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,i),a.exports}(339)})();
@@ -1,11 +1,11 @@
1
- /*!
2
- * chartjs-plugin-trendline.js
3
- * Version: 2.1.6
4
- *
5
- * Copyright 2024 Marcus Alsterfjord
6
- * Released under the MIT license
7
- * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md
8
- *
9
- * Modified by @vesal: accept xy-data from scatter,
10
- * Modified by @Megaemce: add label and basic legend to trendline, add JSDoc,
11
- */
1
+ /*!
2
+ * chartjs-plugin-trendline.js
3
+ * Version: 2.1.8
4
+ *
5
+ * Copyright 2025 Marcus Alsterfjord
6
+ * Released under the MIT license
7
+ * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md
8
+ *
9
+ * Modified by @vesal: accept xy-data from scatter,
10
+ * Modified by @Megaemce: add label and basic legend to trendline, add JSDoc,
11
+ */
@@ -0,0 +1,102 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Chart.js Example</title>
7
+ <!-- Include Chart.js and the Trendline plugin -->
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
10
+ <script src="../src/chartjs-plugin-trendline.js"></script>
11
+ </head>
12
+ <body>
13
+
14
+ <!-- Canvas element for rendering the chart -->
15
+ <canvas id="chartjs" width="400" height="200"></canvas>
16
+
17
+ <script type="text/javascript">
18
+ // Define colors
19
+ const barColor1 = '#258cda'; // Blue color for incidents
20
+ const borderColor1 = '#258cda'; // Same as backgroundColor for incidents
21
+ const barColor2 = '#cccccc'; // Gray color for outage
22
+ const borderColor2 = '#808080'; // Darker gray for borderColor of outage
23
+
24
+ // Get the context of the canvas element we want to select
25
+ const ctx = document.getElementById("chartjs").getContext('2d');
26
+
27
+ const chartDynamics = [
28
+ {
29
+ datepost: "2025-03-01T00:00:00",
30
+ quantity: 75,
31
+ },
32
+ {
33
+ datepost: "2025-03-02T00:00:00",
34
+ quantity: 64,
35
+ },
36
+ {
37
+ datepost: "2025-03-03T00:00:00",
38
+ quantity: 52,
39
+ },
40
+ {
41
+ datepost: "2025-03-04T00:00:00",
42
+ quantity: 23,
43
+ },
44
+ {
45
+ datepost: "2025-03-05T00:00:00",
46
+ quantity: 44,
47
+ },
48
+ {
49
+ datepost: "2025-03-06T00:00:00",
50
+ quantity: 38,
51
+ },
52
+ {
53
+ datepost: "2025-03-07T00:00:00",
54
+ quantity: 44,
55
+ },
56
+ {
57
+ datepost: "2025-03-08T00:00:00",
58
+ quantity: 41,
59
+ },
60
+ {
61
+ datepost: "2025-03-09T00:00:00",
62
+ quantity: 78,
63
+ },
64
+ {
65
+ datepost: "2025-03-10T00:00:00",
66
+ quantity: 31,
67
+ },
68
+ ];
69
+
70
+ // Create a new Chart instance
71
+ const myChart1 = new Chart(ctx, {
72
+ type: "line",
73
+ data: {
74
+ labels: chartDynamics.map((o) => o.datepost),
75
+ datasets: [
76
+ {
77
+ label: "total",
78
+ data: chartDynamics.map((o) => o.quantity),
79
+ borderWidth: 1,
80
+ pointStyle: false,
81
+ trendlineLinear: { lineStyle: "dotted", width: 2 },
82
+ },
83
+ ],
84
+ },
85
+ options: {
86
+ maintainAspectRatio: false,
87
+ scales: {
88
+ x: { type: "time", time: { tooltipFormat: "d", minUnit: "day" } },
89
+ },
90
+ interaction: { mode: "nearest", intersect: false },
91
+ plugins: {
92
+ legend: { display: false },
93
+ autocolors: { enabled: false },
94
+ datalabels: { display: false },
95
+ },
96
+ },
97
+ });
98
+
99
+ </script>
100
+
101
+ </body>
102
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-plugin-trendline",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "Trendline for Chart.js",
5
5
  "main": "src/chartjs-plugin-trendline.js",
6
6
  "scripts": {
@@ -1,8 +1,8 @@
1
1
  /*!
2
2
  * chartjs-plugin-trendline.js
3
- * Version: 2.1.6
3
+ * Version: 2.1.8
4
4
  *
5
- * Copyright 2024 Marcus Alsterfjord
5
+ * Copyright 2025 Marcus Alsterfjord
6
6
  * Released under the MIT license
7
7
  * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md
8
8
  *
@@ -107,6 +107,9 @@ const getScales = (chartInstance) => {
107
107
  * @param {Scale} yScale - The y-axis scale object.
108
108
  */
109
109
  const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
110
+ const yAxisID = dataset.yAxisID || 'y'; // Default to 'y' if no yAxisID is specified
111
+ const yScaleToUse = datasetMeta.controller.chart.scales[yAxisID] || yScale;
112
+
110
113
  const defaultColor = dataset.borderColor || 'rgba(169,169,169, .6)';
111
114
  const {
112
115
  colorMin = defaultColor,
@@ -149,6 +152,7 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
149
152
  let endPos = datasetMeta.data[lastIndex]?.[xAxisKey];
150
153
  let xy = typeof dataset.data[firstIndex] === 'object';
151
154
 
155
+ // Collect data points for the fitter
152
156
  dataset.data.forEach((data, index) => {
153
157
  if (data == null) return;
154
158
 
@@ -172,9 +176,11 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
172
176
  }
173
177
  });
174
178
 
175
- let x1 = xScale.getPixelForValue(fitter.minx);
176
- let y1 = yScale.getPixelForValue(fitter.f(fitter.minx));
177
-
179
+ // Calculate the pixel coordinates for the trendline
180
+ let x1 = isFinite(startPos)
181
+ ? startPos
182
+ : xScale.getPixelForValue(fitter.minx);
183
+ let y1 = yScaleToUse.getPixelForValue(fitter.f(fitter.minx));
178
184
  let x2, y2;
179
185
 
180
186
  // Projection logic for trendline
@@ -182,60 +188,60 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
182
188
  let x2value = fitter.fo();
183
189
  if (x2value < fitter.minx) x2value = fitter.maxx;
184
190
  x2 = xScale.getPixelForValue(x2value);
185
- y2 = yScale.getPixelForValue(fitter.f(x2value));
191
+ y2 = yScaleToUse.getPixelForValue(fitter.f(x2value));
186
192
  } else {
187
- x2 = xScale.getPixelForValue(fitter.maxx);
188
- y2 = yScale.getPixelForValue(fitter.f(fitter.maxx));
189
- }
190
-
191
- if (isFinite(startPos) || isFinite(endPos)) {
192
- x1 = startPos;
193
- x2 = endPos;
193
+ x2 = isFinite(endPos) ? endPos : xScale.getPixelForValue(fitter.maxx);
194
+ y2 = yScaleToUse.getPixelForValue(fitter.f(fitter.maxx));
194
195
  }
195
196
 
196
197
  const drawBottom = datasetMeta.controller.chart.chartArea.bottom;
197
198
  const chartWidth = datasetMeta.controller.chart.width;
198
199
 
199
- adjustLineForOverflow({ x1, y1, x2, y2, drawBottom, chartWidth });
200
+ // Only adjust line for overflow if coordinates are valid
201
+ if (isFinite(x1) && isFinite(y1) && isFinite(x2) && isFinite(y2)) {
202
+ adjustLineForOverflow({ x1, y1, x2, y2, drawBottom, chartWidth });
200
203
 
201
- // Set line width and styles
202
- ctx.lineWidth = lineWidth;
203
- setLineStyle(ctx, lineStyle);
204
+ // Set line width and styles
205
+ ctx.lineWidth = lineWidth;
206
+ setLineStyle(ctx, lineStyle);
204
207
 
205
- // Draw the trendline
206
- drawTrendline({ ctx, x1, y1, x2, y2, colorMin, colorMax });
208
+ // Draw the trendline
209
+ drawTrendline({ ctx, x1, y1, x2, y2, colorMin, colorMax });
207
210
 
208
- // Optionally fill below the trendline
209
- if (fillColor) {
210
- fillBelowTrendline(ctx, x1, y1, x2, y2, drawBottom, fillColor);
211
- }
211
+ // Optionally fill below the trendline
212
+ if (fillColor) {
213
+ fillBelowTrendline(ctx, x1, y1, x2, y2, drawBottom, fillColor);
214
+ }
212
215
 
213
- // Calculate the angle of the trendline
214
- const angle = Math.atan2(y2 - y1, x2 - x1);
215
-
216
- // Calculate the slope of the trendline (value of trend)
217
- const slope = (y1 - y2) / (x2 - x1);
218
-
219
- // Add the label to the trendline if it's populated and not set to hidden
220
- if (dataset.trendlineLinear.label && display !== false) {
221
- const trendText = displayValue
222
- ? `${text} (Slope: ${
223
- percentage ? (slope * 100).toFixed(2) + '%' : slope.toFixed(2)
224
- })`
225
- : text;
226
- addTrendlineLabel(
227
- ctx,
228
- trendText,
229
- x1,
230
- y1,
231
- x2,
232
- y2,
233
- angle,
234
- color,
235
- family,
236
- size,
237
- offset
238
- );
216
+ // Calculate the angle of the trendline
217
+ const angle = Math.atan2(y2 - y1, x2 - x1);
218
+
219
+ // Calculate the slope of the trendline (value of trend)
220
+ const slope = (y1 - y2) / (x2 - x1);
221
+
222
+ // Add the label to the trendline if it's populated and not set to hidden
223
+ if (dataset.trendlineLinear.label && display !== false) {
224
+ const trendText = displayValue
225
+ ? `${text} (Slope: ${
226
+ percentage
227
+ ? (slope * 100).toFixed(2) + '%'
228
+ : slope.toFixed(2)
229
+ })`
230
+ : text;
231
+ addTrendlineLabel(
232
+ ctx,
233
+ trendText,
234
+ x1,
235
+ y1,
236
+ x2,
237
+ y2,
238
+ angle,
239
+ color,
240
+ family,
241
+ size,
242
+ offset
243
+ );
244
+ }
239
245
  }
240
246
  };
241
247
 
@@ -362,15 +368,30 @@ const setLineStyle = (ctx, lineStyle) => {
362
368
  * @param {string} params.colorMax - The ending color of the trendline gradient.
363
369
  */
364
370
  const drawTrendline = ({ ctx, x1, y1, x2, y2, colorMin, colorMax }) => {
371
+ // Ensure all values are finite numbers
372
+ if (!isFinite(x1) || !isFinite(y1) || !isFinite(x2) || !isFinite(y2)) {
373
+ console.warn(
374
+ 'Cannot draw trendline: coordinates contain non-finite values',
375
+ { x1, y1, x2, y2 }
376
+ );
377
+ return;
378
+ }
379
+
365
380
  ctx.beginPath();
366
381
  ctx.moveTo(x1, y1);
367
382
  ctx.lineTo(x2, y2);
368
383
 
369
- let gradient = ctx.createLinearGradient(x1, y1, x2, y2);
370
- gradient.addColorStop(0, colorMin);
371
- gradient.addColorStop(1, colorMax);
384
+ try {
385
+ let gradient = ctx.createLinearGradient(x1, y1, x2, y2);
386
+ gradient.addColorStop(0, colorMin);
387
+ gradient.addColorStop(1, colorMax);
388
+ ctx.strokeStyle = gradient;
389
+ } catch (e) {
390
+ // Fallback to solid color if gradient creation fails
391
+ console.warn('Gradient creation failed, using solid color:', e);
392
+ ctx.strokeStyle = colorMin;
393
+ }
372
394
 
373
- ctx.strokeStyle = gradient;
374
395
  ctx.stroke();
375
396
  ctx.closePath();
376
397
  };
@@ -386,6 +407,21 @@ const drawTrendline = ({ ctx, x1, y1, x2, y2, colorMin, colorMax }) => {
386
407
  * @param {string} fillColor - The color to fill below the trendline.
387
408
  */
388
409
  const fillBelowTrendline = (ctx, x1, y1, x2, y2, drawBottom, fillColor) => {
410
+ // Ensure all values are finite numbers
411
+ if (
412
+ !isFinite(x1) ||
413
+ !isFinite(y1) ||
414
+ !isFinite(x2) ||
415
+ !isFinite(y2) ||
416
+ !isFinite(drawBottom)
417
+ ) {
418
+ console.warn(
419
+ 'Cannot fill below trendline: coordinates contain non-finite values',
420
+ { x1, y1, x2, y2, drawBottom }
421
+ );
422
+ return;
423
+ }
424
+
389
425
  ctx.beginPath();
390
426
  ctx.moveTo(x1, y1);
391
427
  ctx.lineTo(x2, y2);