chartjs-plugin-trendline 2.1.3 → 2.1.5

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/README.md CHANGED
@@ -34,13 +34,34 @@ 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: "red",
38
- colorMax: "green",
39
- lineStyle: "dotted|solid",
40
- width: 2,
41
- xAxisKey: "time" (optional),
42
- yAxisKey: "usage" (optional),
43
- projection: true|false (optional)
37
+ colorMin: Color
38
+ colorMax: Color,
39
+ lineStyle: string, // "dotted" | "solid" | "dashed" | "dashdot"
40
+ width: number,
41
+ xAxisKey: string, // optional
42
+ yAxisKey: string, // optional
43
+ projection: boolean, // optional
44
+ // optional
45
+ label: {
46
+ color: Color,
47
+ text: string,
48
+ display: boolean,
49
+ displayValue: boolean,
50
+ offset: number,
51
+ font: {
52
+ family: string,
53
+ size: number,
54
+ }
55
+ },
56
+ // optional
57
+ legend: {
58
+ text: string,
59
+ strokeStyle: Color,
60
+ fillStyle: Color,
61
+ lineCap: string,
62
+ lineDash: number[],
63
+ lineWidth: number,
64
+ }
44
65
  }
45
66
  }
46
67
  ```
@@ -49,6 +70,7 @@ To configure the trendline plugin you simply add a new config options to your da
49
70
 
50
71
  - bar
51
72
  - line
73
+ - scatter
52
74
 
53
75
  ## Contributing
54
76
 
@@ -1,2 +1,2 @@
1
1
  /*! For license information please see chartjs-plugin-trendline.min.js.LICENSE.txt */
2
- (()=>{var t={339:(t,e)=>{const i={id:"chartjs-plugin-trendline",afterDatasetsDraw:t=>{let e,i;for(let s in t.scales)if("x"==s[0]?i=t.scales[s]:e=t.scales[s],i&&e)break;const a=t.ctx;t.data.datasets.forEach(((e,n)=>{const r=e.alwaysShowTrendline||t.isDatasetVisible(n);if(e.trendlineLinear&&r&&e.data.length>1){const r=t.getDatasetMeta(n);s(r,a,e,i,t.scales[r.yAxisID])}})),a.setLineDash([])}},s=(t,e,i,s,n)=>{let r=i.borderColor||"rgba(169,169,169, .6)",o=i.trendlineLinear.colorMin||r,l=i.trendlineLinear.colorMax||r,h=i.trendlineLinear.width??i.borderWidth??3,d=i.trendlineLinear.lineStyle||"solid",u=i.trendlineLinear.fillColor;const m=t.controller.chart.options,x="object"==typeof m.parsing?m.parsing:void 0,c=i.trendlineLinear.xAxisKey||x?.xAxisKey||"x",f=i.trendlineLinear.yAxisKey||x?.yAxisKey||"y";let X=new a,y=i.data.findIndex((t=>null!=t)),p=i.data.length-1,g=t.data[y][c],w=t.data[p][c],Y="object"==typeof i.data[y];i.data.forEach(((t,e)=>{if(null!=t)if(["time","timeseries"].includes(s.options.type)){let i=null!=t[c]?t[c]:t.t;void 0!==i?X.add(new Date(i).getTime(),t[f]):X.add(e,t)}else Y?isNaN(t.x)||isNaN(t.y)?isNaN(t.x)?isNaN(t.y)||X.add(e,t.y):X.add(e,t.x):X.add(t.x,t.y):X.add(e,t)}));let L,F,P=s.getPixelForValue(X.minx),b=n.getPixelForValue(X.f(X.minx));if(i.trendlineLinear.projection&&X.scale()<0){let t=X.fo();t<X.minx&&(t=X.maxx),L=s.getPixelForValue(t),F=n.getPixelForValue(X.f(t))}else L=s.getPixelForValue(X.maxx),F=n.getPixelForValue(X.f(X.maxx));(isFinite(g)||isFinite(w))&&(P=g,L=w);let C=t.controller.chart.chartArea.bottom,D=t.controller.chart.width;if(b>C){let t=b-C,e=b-F;b=C,P+=D*(t/e)}else if(F>C){let t=F-C,e=F-b;F=C,L=D-(L-(D-D*(t/e)))}e.lineWidth=h,"dotted"===d?e.setLineDash([2,3]):e.setLineDash([]),e.beginPath(),e.moveTo(P,b),e.lineTo(L,F);let v=e.createLinearGradient(P,b,L,F);F<b?(v.addColorStop(0,l),v.addColorStop(1,o)):(v.addColorStop(0,o),v.addColorStop(1,l)),e.strokeStyle=v,e.stroke(),e.closePath(),u&&(e.fillStyle=u,e.beginPath(),e.moveTo(P,b),e.lineTo(L,F),e.lineTo(L,C),e.lineTo(P,C),e.closePath(),e.fill())};class a{constructor(){this.count=0,this.sumX=0,this.sumX2=0,this.sumXY=0,this.sumY=0,this.minx=1e100,this.maxx=-1e100,this.maxy=-1e100}add(t,e){t=parseFloat(t),e=parseFloat(e),this.count++,this.sumX+=t,this.sumX2+=t*t,this.sumXY+=t*e,this.sumY+=e,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t),e>this.maxy&&(this.maxy=e)}f(t){t=parseFloat(t);let e=this.count*this.sumX2-this.sumX*this.sumX;return(this.sumX2*this.sumY-this.sumX*this.sumXY)/e+t*((this.count*this.sumXY-this.sumX*this.sumY)/e)}fo(){let t=this.count*this.sumX2-this.sumX*this.sumX;return-(this.sumX2*this.sumY-this.sumX*this.sumXY)/t/((this.count*this.sumXY-this.sumX*this.sumY)/t)}scale(){let t=this.count*this.sumX2-this.sumX*this.sumX;return(this.count*this.sumXY-this.sumX*this.sumY)/t}}"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(i):window.Chart.plugins.register(i));try{t.exports=i}catch(t){}}},e={};!function i(s){var a=e[s];if(void 0!==a)return a.exports;var n=e[s]={exports:{}};return t[s](n,n.exports,i),n.exports}(339)})();
2
+ (()=>{var e={339:(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}=i.trendlineLinear.label||{},{family:S="'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,T=i.trendlineLinear?.xAxisKey||N?.xAxisKey||"x",C=i.trendlineLinear?.yAxisKey||N?.yAxisKey||"y";let P=new h,M=i.data.findIndex((e=>null!=e)),V=i.data.length-1,A=e.data[M]?.[T],F=e.data[V]?.[T],k="object"==typeof i.data[M];i.data.forEach(((e,t)=>{if(null!=e)if(["time","timeseries"].includes(a.options.type)){let i=null!=e[T]?e[T]:e.t;void 0!==i?P.add(new Date(i).getTime(),e[C]):P.add(t,e)}else k?isNaN(e.x)||isNaN(e.y)?isNaN(e.x)?isNaN(e.y)||P.add(t,e.y):P.add(t,e.x):P.add(e.x,e.y):P.add(t,e)}));let j,E,W=a.getPixelForValue(P.minx),K=s.getPixelForValue(P.f(P.minx));if(i.trendlineLinear.projection&&P.scale()<0){let e=P.fo();e<P.minx&&(e=P.maxx),j=a.getPixelForValue(e),E=s.getPixelForValue(P.f(e))}else j=a.getPixelForValue(P.maxx),E=s.getPixelForValue(P.f(P.maxx));(isFinite(A)||isFinite(F))&&(W=A,j=F);const $=e.controller.chart.chartArea.bottom,H=e.controller.chart.width;l({x1:W,y1:K,x2:j,y2:E,drawBottom:$,chartWidth:H}),t.lineWidth=y,r(t,f),o({ctx:t,x1:W,y1:K,x2:j,y2:E,colorMin:c,colorMax:u}),m&&d(t,W,K,j,E,$,m);const I=Math.atan2(E-K,j-W),z=(K-E)/(j-W);if(i.trendlineLinear.label&&!1!==g){const e=w?`${b} (Slope: ${z.toFixed(2)})`:b;n(t,e,W,K,j,E,I,p,S,v,L)}},n=(e,t,i,a,s,n,l,r,o,d,h)=>{e.font=`${d}px ${o}`,e.fillStyle=r;const x=(i+s)/2,c=(a+n)/2;e.save(),e.translate(x,c),e.rotate(l),e.fillText(t,0,-h),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}(339)})();
@@ -1,10 +1,11 @@
1
1
  /*!
2
2
  * chartjs-plugin-trendline.js
3
- * Version: 2.1.3
3
+ * Version: 2.1.5
4
4
  *
5
5
  * Copyright 2024 Marcus Alsterfjord
6
6
  * Released under the MIT license
7
7
  * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md
8
8
  *
9
- * Mod by: vesal: accept also xy-data so works with scatter
9
+ * Modified by @vesal: accept xy-data from scatter,
10
+ * Modified by @Megaemce: add label and basic legend to trendline, add JSDoc,
10
11
  */
@@ -0,0 +1,50 @@
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
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <title>BarChart Example</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.js"></script>
9
+ <script src="./../src/chartjs-plugin-trendline.js"></script>
10
+ <script>
11
+ document.addEventListener("DOMContentLoaded", function(event) {
12
+ // Bar chart
13
+ new Chart(document.getElementById("bar-chart"), {
14
+ type: 'bar',
15
+ data: {
16
+ labels: ["Africa", "Asia", "Europe", "Latin America", "North America"],
17
+ datasets: [
18
+ {
19
+ label: "Population (millions)",
20
+ backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"],
21
+ data: [2478,5267,734,784,433],
22
+ trendlineLinear: {
23
+ colorMin: "rgba(255,105,180, .8)",
24
+ lineStyle: "dotted",
25
+ width: 2
26
+ }
27
+ },
28
+ ]
29
+ },
30
+ options: {
31
+ legend: { display: false },
32
+ title: {
33
+ display: true,
34
+ text: 'Predicted world population (millions) in 2050'
35
+ }
36
+ }
37
+ });
38
+ });
39
+ </script>
40
+ </head>
41
+ <body>
42
+ <h1>Bar Chart</h1>
43
+
44
+ <div style="width: 800px;">
45
+ <canvas id="bar-chart"></canvas>
46
+ </div>
47
+
48
+ <p>Using example code from <a href="http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/" target="_blank">tobiasahlin.com.</a></p>
49
+ </body>
50
+ </html>
@@ -1,73 +1,108 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
- <title>LineChart Example</title>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.js"></script>
9
- <script src="./../src/chartjs-plugin-trendline.js"></script>
10
- <script>
11
- document.addEventListener("DOMContentLoaded", function(event) {
12
- new Chart(document.getElementById("line-chart"), {
13
- type: 'line',
14
- data: {
15
- labels: [1500,1600,1700,1750,1800,1850,1900,1950,1999,2050],
16
- datasets: [{
17
- data: [86,114,106,106,107,111,133,221,783,2478],
18
- label: "Africa",
19
- borderColor: "#3e95cd",
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge" />
7
+ <title>LineChart Example with Labeled Trendlines</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.js"></script>
9
+ <script src="./../src/chartjs-plugin-trendline.js"></script>
10
+ <script>
11
+ document.addEventListener('DOMContentLoaded', function (event) {
12
+ new Chart(document.getElementById('line-chart'), {
13
+ type: 'line',
14
+ data: {
15
+ labels: [
16
+ 1500, 1600, 1700, 1750, 1800, 1850, 1900, 1950,
17
+ 1999, 2050,
18
+ ],
19
+ datasets: [
20
+ {
21
+ data: [
22
+ 86, 114, 106, 106, 107, 111, 133, 221, 783,
23
+ 2478,
24
+ ],
25
+ label: 'Africa',
26
+ borderColor: '#3e95cd',
20
27
  fill: false,
21
28
  trendlineLinear: {
22
- colorMin: "#3e95cd",
23
- lineStyle: "line",
24
- width: 1
25
- }
26
- }, {
27
- data: [282,350,411,502,635,809,947,1402,3700,5267],
28
- label: "Asia",
29
- borderColor: "#8e5ea2",
29
+ colorMin: '#3e95cd',
30
+ width: 1,
31
+ lineStyle: 'solid',
32
+ },
33
+ },
34
+ {
35
+ data: [
36
+ 282, 350, 411, 502, 635, 809, 947, 1402,
37
+ 3700, 5267,
38
+ ],
39
+ label: 'Asia',
40
+ borderColor: '#8e5ea2',
30
41
  fill: false,
31
42
  trendlineLinear: {
32
- colorMin: "red",
33
- colorMax: "green",
34
- lineStyle: "line",
35
- width: 1
36
- }
37
- }, {
38
- data: [168,170,178,190,203,276,408,547,675,734],
39
- label: "Europe",
40
- borderColor: "#3cba9f",
41
- fill: false
42
- }, {
43
- data: [40,20,10,16,24,38,74,167,508,784],
44
- label: "Latin America",
45
- borderColor: "#e8c3b9",
46
- fill: false
47
- }, {
48
- data: [6,3,2,2,7,26,82,172,312,433],
49
- label: "North America",
50
- borderColor: "#c45850",
51
- fill: false
52
- }
53
- ]
54
- },
55
- options: {
43
+ colorMin: 'red',
44
+ colorMax: 'green',
45
+ lineStyle: 'dashed',
46
+ width: 1,
47
+ label: {
48
+ font: {
49
+ size: 12,
50
+ },
51
+ color: 'red',
52
+ text: 'Asia',
53
+ displayValue: false,
54
+ offset: 10,
55
+ },
56
+ legend: {
57
+ color: 'red',
58
+ text: 'Asian trendline',
59
+ display: true,
60
+ lineDash: [8, 3],
61
+ },
62
+ },
63
+ },
64
+ {
65
+ data: [
66
+ 168, 170, 178, 190, 203, 276, 408, 547, 675,
67
+ 734,
68
+ ],
69
+ label: 'Europe',
70
+ borderColor: '#3cba9f',
71
+ fill: false,
72
+ },
73
+ {
74
+ data: [
75
+ 40, 20, 10, 16, 24, 38, 74, 167, 508, 784,
76
+ ],
77
+ label: 'Latin America',
78
+ borderColor: '#e8c3b9',
79
+ fill: false,
80
+ },
81
+ {
82
+ data: [6, 3, 2, 2, 7, 26, 82, 172, 312, 433],
83
+ label: 'North America',
84
+ borderColor: '#c45850',
85
+ fill: false,
86
+ },
87
+ ],
88
+ },
89
+ options: {
90
+ plugins: {
56
91
  title: {
57
- display: true,
58
- text: 'World population per region (in millions)'
59
- }
60
- }
61
- });
92
+ display: true,
93
+ text: 'World population per region (in millions)',
94
+ },
95
+ },
96
+ },
62
97
  });
98
+ });
63
99
  </script>
64
- </head>
65
- <body>
66
- <h1>Line Chart</h1>
67
-
68
- <div style="width: 800px;">
69
- <canvas id="line-chart"></canvas>
70
- </div>
100
+ </head>
101
+ <body>
102
+ <h1>Line Chart with Labeled Trendlines</h1>
71
103
 
72
- </body>
73
- </html>
104
+ <div style="width: 800px">
105
+ <canvas id="line-chart"></canvas>
106
+ </div>
107
+ </body>
108
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-plugin-trendline",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "Trendline for Chart.js",
5
5
  "main": "src/chartjs-plugin-trendline.js",
6
6
  "scripts": {
@@ -1,24 +1,29 @@
1
1
  /*!
2
2
  * chartjs-plugin-trendline.js
3
- * Version: 2.1.3
3
+ * Version: 2.1.5
4
4
  *
5
5
  * Copyright 2024 Marcus Alsterfjord
6
6
  * Released under the MIT license
7
7
  * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md
8
8
  *
9
- * Mod by: vesal: accept also xy-data so works with scatter
9
+ * Modified by @vesal: accept xy-data from scatter,
10
+ * Modified by @Megaemce: add label and basic legend to trendline, add JSDoc,
11
+ */
12
+
13
+ /**
14
+ * Chart.js plugin to draw linear trendlines on datasets.
10
15
  */
11
16
  const pluginTrendlineLinear = {
12
17
  id: 'chartjs-plugin-trendline',
18
+
19
+ /**
20
+ * Hook that is called after datasets are drawn.
21
+ * Adds trendlines to the datasets that have `trendlineLinear` configured.
22
+ * @param {Chart} chartInstance - The chart instance where datasets are drawn.
23
+ */
13
24
  afterDatasetsDraw: (chartInstance) => {
14
- let yScale;
15
- let xScale;
16
- for (let axis in chartInstance.scales) {
17
- if (axis[0] == 'x') xScale = chartInstance.scales[axis];
18
- else yScale = chartInstance.scales[axis];
19
- if (xScale && yScale) break;
20
- }
21
25
  const ctx = chartInstance.ctx;
26
+ const { xScale, yScale } = getScales(chartInstance);
22
27
 
23
28
  chartInstance.data.datasets.forEach((dataset, index) => {
24
29
  const showTrendline =
@@ -31,27 +36,98 @@ const pluginTrendlineLinear = {
31
36
  dataset.data.length > 1
32
37
  ) {
33
38
  const datasetMeta = chartInstance.getDatasetMeta(index);
34
- addFitter(
35
- datasetMeta,
36
- ctx,
37
- dataset,
38
- xScale,
39
- chartInstance.scales[datasetMeta.yAxisID]
40
- );
39
+ addFitter(datasetMeta, ctx, dataset, xScale, yScale);
41
40
  }
42
41
  });
43
42
 
43
+ // Reset to solid line after drawing trendline
44
44
  ctx.setLineDash([]);
45
45
  },
46
+
47
+ beforeInit: (chartInstance) => {
48
+ const datasets = chartInstance.data.datasets;
49
+
50
+ datasets.forEach((dataset) => {
51
+ if (dataset.trendlineLinear && dataset.trendlineLinear.label) {
52
+ const label = dataset.trendlineLinear.label;
53
+
54
+ // Access chartInstance to update legend labels
55
+ const originalGenerateLabels =
56
+ chartInstance.legend.options.labels.generateLabels;
57
+
58
+ chartInstance.legend.options.labels.generateLabels = function (
59
+ chart
60
+ ) {
61
+ const defaultLabels = originalGenerateLabels(chart);
62
+
63
+ const legendConfig = dataset.trendlineLinear.legend;
64
+
65
+ // Display the legend is it's populated and not set to hidden
66
+ if (legendConfig && legendConfig.display !== false) {
67
+ defaultLabels.push({
68
+ text: legendConfig.text || label + ' (Trendline)',
69
+ strokeStyle:
70
+ legendConfig.color ||
71
+ dataset.borderColor ||
72
+ 'rgba(169,169,169, .6)',
73
+ fillStyle: legendConfig.fillStyle || 'transparent',
74
+ lineCap: legendConfig.lineCap || 'butt',
75
+ lineDash: legendConfig.lineDash || [],
76
+ lineWidth: legendConfig.width || 1,
77
+ });
78
+ }
79
+ return defaultLabels;
80
+ };
81
+ }
82
+ });
83
+ },
46
84
  };
47
85
 
86
+ /**
87
+ * Retrieves the x and y scales from the chart instance.
88
+ * @param {Chart} chartInstance - The chart instance.
89
+ * @returns {Object} - The xScale and yScale of the chart.
90
+ */
91
+ const getScales = (chartInstance) => {
92
+ let xScale, yScale;
93
+ for (const scale of Object.values(chartInstance.scales)) {
94
+ if (scale.isHorizontal()) xScale = scale;
95
+ else yScale = scale;
96
+ if (xScale && yScale) break;
97
+ }
98
+ return { xScale, yScale };
99
+ };
100
+
101
+ /**
102
+ * Adds a trendline (fitter) to the dataset on the chart and optionally labels it with trend value.
103
+ * @param {Object} datasetMeta - Metadata about the dataset.
104
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
105
+ * @param {Object} dataset - The dataset configuration from the chart.
106
+ * @param {Scale} xScale - The x-axis scale object.
107
+ * @param {Scale} yScale - The y-axis scale object.
108
+ */
48
109
  const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
49
- let defaultColor = dataset.borderColor || 'rgba(169,169,169, .6)';
50
- let colorMin = dataset.trendlineLinear.colorMin || defaultColor;
51
- let colorMax = dataset.trendlineLinear.colorMax || defaultColor;
52
- let lineWidth = dataset.trendlineLinear.width ?? dataset.borderWidth ?? 3;
53
- let lineStyle = dataset.trendlineLinear.lineStyle || 'solid';
54
- let fillColor = dataset.trendlineLinear.fillColor;
110
+ const defaultColor = dataset.borderColor || 'rgba(169,169,169, .6)';
111
+ const {
112
+ colorMin = defaultColor,
113
+ colorMax = defaultColor,
114
+ width: lineWidth = dataset.borderWidth || 3,
115
+ lineStyle = 'solid',
116
+ fillColor = false,
117
+ } = dataset.trendlineLinear || {};
118
+
119
+ const {
120
+ color = defaultColor,
121
+ text = 'Trendline',
122
+ display = true,
123
+ displayValue = true,
124
+ offset = 10,
125
+ } = dataset.trendlineLinear.label || {};
126
+
127
+ const {
128
+ family = "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
129
+ size = 12,
130
+ } = dataset.trendlineLinear.label?.font || {};
55
131
 
56
132
  const chartOptions = datasetMeta.controller.chart.options;
57
133
  const parsingOptions =
@@ -59,17 +135,17 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
59
135
  ? chartOptions.parsing
60
136
  : undefined;
61
137
  const xAxisKey =
62
- dataset.trendlineLinear.xAxisKey || parsingOptions?.xAxisKey || 'x';
138
+ dataset.trendlineLinear?.xAxisKey || parsingOptions?.xAxisKey || 'x';
63
139
  const yAxisKey =
64
- dataset.trendlineLinear.yAxisKey || parsingOptions?.yAxisKey || 'y';
140
+ dataset.trendlineLinear?.yAxisKey || parsingOptions?.yAxisKey || 'y';
65
141
 
66
142
  let fitter = new LineFitter();
67
- let firstIndex = dataset.data.findIndex((d) => {
68
- return d !== undefined && d !== null;
69
- });
143
+ let firstIndex = dataset.data.findIndex(
144
+ (d) => d !== undefined && d !== null
145
+ );
70
146
  let lastIndex = dataset.data.length - 1;
71
- let startPos = datasetMeta.data[firstIndex][xAxisKey];
72
- let endPos = datasetMeta.data[lastIndex][xAxisKey];
147
+ let startPos = datasetMeta.data[firstIndex]?.[xAxisKey];
148
+ let endPos = datasetMeta.data[lastIndex]?.[xAxisKey];
73
149
  let xy = typeof dataset.data[firstIndex] === 'object';
74
150
 
75
151
  dataset.data.forEach((data, index) => {
@@ -98,33 +174,131 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
98
174
  let x1 = xScale.getPixelForValue(fitter.minx);
99
175
  let y1 = yScale.getPixelForValue(fitter.f(fitter.minx));
100
176
 
101
- let x2;
102
- let y2;
177
+ let x2, y2;
103
178
 
104
- // Project only on x axes, do not project if trendline will never hit x axes
179
+ // Projection logic for trendline
105
180
  if (dataset.trendlineLinear.projection && fitter.scale() < 0) {
106
- // X
107
181
  let x2value = fitter.fo();
108
182
  if (x2value < fitter.minx) x2value = fitter.maxx;
109
183
  x2 = xScale.getPixelForValue(x2value);
110
-
111
- // Y
112
184
  y2 = yScale.getPixelForValue(fitter.f(x2value));
113
185
  } else {
114
186
  x2 = xScale.getPixelForValue(fitter.maxx);
115
187
  y2 = yScale.getPixelForValue(fitter.f(fitter.maxx));
116
188
  }
117
189
 
118
- if(isFinite(startPos) || isFinite(endPos)) {
190
+ if (isFinite(startPos) || isFinite(endPos)) {
119
191
  x1 = startPos;
120
192
  x2 = endPos;
121
193
  }
122
194
 
123
- let drawBottom = datasetMeta.controller.chart.chartArea.bottom;
124
- let chartWidth = datasetMeta.controller.chart.width;
195
+ const drawBottom = datasetMeta.controller.chart.chartArea.bottom;
196
+ const chartWidth = datasetMeta.controller.chart.width;
125
197
 
198
+ adjustLineForOverflow({ x1, y1, x2, y2, drawBottom, chartWidth });
199
+
200
+ // Set line width and styles
201
+ ctx.lineWidth = lineWidth;
202
+ setLineStyle(ctx, lineStyle);
203
+
204
+ // Draw the trendline
205
+ drawTrendline({ ctx, x1, y1, x2, y2, colorMin, colorMax });
206
+
207
+ // Optionally fill below the trendline
208
+ if (fillColor) {
209
+ fillBelowTrendline(ctx, x1, y1, x2, y2, drawBottom, fillColor);
210
+ }
211
+
212
+ // Calculate the angle of the trendline
213
+ const angle = Math.atan2(y2 - y1, x2 - x1);
214
+
215
+ // Calculate the slope of the trendline (value of trend)
216
+ const slope = (y1 - y2) / (x2 - x1);
217
+
218
+ // Add the label to the trendline if it's populated and not set to hidden
219
+ if (dataset.trendlineLinear.label && display !== false) {
220
+ const trendText = displayValue
221
+ ? `${text} (Slope: ${slope.toFixed(2)})`
222
+ : text;
223
+ addTrendlineLabel(
224
+ ctx,
225
+ trendText,
226
+ x1,
227
+ y1,
228
+ x2,
229
+ y2,
230
+ angle,
231
+ color,
232
+ family,
233
+ size,
234
+ offset
235
+ );
236
+ }
237
+ };
238
+
239
+ /**
240
+ * Adds a label to the trendline at the calculated angle.
241
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
242
+ * @param {string} label - The label text to add.
243
+ * @param {number} x1 - The starting x-coordinate of the trendline.
244
+ * @param {number} y1 - The starting y-coordinate of the trendline.
245
+ * @param {number} x2 - The ending x-coordinate of the trendline.
246
+ * @param {number} y2 - The ending y-coordinate of the trendline.
247
+ * @param {number} angle - The angle (in radians) of the trendline.
248
+ * @param {string} labelColor - The color of the label text.
249
+ * @param {string} family - The font family for the label text.
250
+ * @param {number} size - The font size for the label text.
251
+ * @param {number} offset - The offset of the label from the trendline
252
+ */
253
+ const addTrendlineLabel = (
254
+ ctx,
255
+ label,
256
+ x1,
257
+ y1,
258
+ x2,
259
+ y2,
260
+ angle,
261
+ labelColor,
262
+ family,
263
+ size,
264
+ offset
265
+ ) => {
266
+ // Set the label font and color
267
+ ctx.font = `${size}px ${family}`;
268
+ ctx.fillStyle = labelColor;
269
+
270
+ // Calculate label position (middle of the trendline)
271
+ const labelX = (x1 + x2) / 2;
272
+ const labelY = (y1 + y2) / 2;
273
+
274
+ // Save the current state of the canvas
275
+ ctx.save();
276
+
277
+ // Translate to the label position
278
+ ctx.translate(labelX, labelY);
279
+
280
+ // Rotate the context to align with the trendline
281
+ ctx.rotate(angle);
282
+
283
+ // Draw the label
284
+ ctx.fillText(label, 0, -offset);
285
+
286
+ // Restore the canvas state
287
+ ctx.restore();
288
+ };
289
+
290
+ /**
291
+ * Adjusts the line if it overflows below the chart bottom.
292
+ * @param {Object} params - The line parameters.
293
+ * @param {number} params.x1 - Starting x-coordinate of the trendline.
294
+ * @param {number} params.y1 - Starting y-coordinate of the trendline.
295
+ * @param {number} params.x2 - Ending x-coordinate of the trendline.
296
+ * @param {number} params.y2 - Ending y-coordinate of the trendline.
297
+ * @param {number} params.drawBottom - Bottom boundary of the chart.
298
+ * @param {number} params.chartWidth - Width of the chart.
299
+ */
300
+ const adjustLineForOverflow = ({ x1, y1, x2, y2, drawBottom, chartWidth }) => {
126
301
  if (y1 > drawBottom) {
127
- // Left side is below zero
128
302
  let diff = y1 - drawBottom;
129
303
  let lineHeight = y1 - y2;
130
304
  let overlapPercentage = diff / lineHeight;
@@ -133,7 +307,6 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
133
307
  y1 = drawBottom;
134
308
  x1 = x1 + addition;
135
309
  } else if (y2 > drawBottom) {
136
- // right side is below zero
137
310
  let diff = y2 - drawBottom;
138
311
  let lineHeight = y2 - y1;
139
312
  let overlapPercentage = diff / lineHeight;
@@ -142,95 +315,148 @@ const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
142
315
  y2 = drawBottom;
143
316
  x2 = chartWidth - (x2 - subtraction);
144
317
  }
318
+ };
145
319
 
146
- ctx.lineWidth = lineWidth;
147
-
148
- if (lineStyle === 'dotted') {
149
- ctx.setLineDash([2, 3]); // Dotted
150
- } else {
151
- ctx.setLineDash([]); // Solid
320
+ /**
321
+ * Sets the line style (dashed, dotted, solid) for the canvas context.
322
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
323
+ * @param {string} lineStyle - The style of the line ('dotted', 'dashed', 'solid', etc.).
324
+ */
325
+ const setLineStyle = (ctx, lineStyle) => {
326
+ switch (lineStyle) {
327
+ case 'dotted':
328
+ ctx.setLineDash([2, 2]);
329
+ break;
330
+ case 'dashed':
331
+ ctx.setLineDash([8, 3]);
332
+ break;
333
+ case 'dashdot':
334
+ ctx.setLineDash([8, 3, 2, 3]);
335
+ break;
336
+ case 'solid':
337
+ default:
338
+ ctx.setLineDash([]);
339
+ break;
152
340
  }
341
+ };
153
342
 
343
+ /**
344
+ * Draws the trendline on the canvas context.
345
+ * @param {Object} params - The trendline parameters.
346
+ * @param {CanvasRenderingContext2D} params.ctx - The canvas rendering context.
347
+ * @param {number} params.x1 - Starting x-coordinate of the trendline.
348
+ * @param {number} params.y1 - Starting y-coordinate of the trendline.
349
+ * @param {number} params.x2 - Ending x-coordinate of the trendline.
350
+ * @param {number} params.y2 - Ending y-coordinate of the trendline.
351
+ * @param {string} params.colorMin - The starting color of the trendline gradient.
352
+ * @param {string} params.colorMax - The ending color of the trendline gradient.
353
+ */
354
+ const drawTrendline = ({ ctx, x1, y1, x2, y2, colorMin, colorMax }) => {
154
355
  ctx.beginPath();
155
356
  ctx.moveTo(x1, y1);
156
357
  ctx.lineTo(x2, y2);
157
358
 
158
359
  let gradient = ctx.createLinearGradient(x1, y1, x2, y2);
159
- if (y2 < y1) {
160
- gradient.addColorStop(0, colorMax);
161
- gradient.addColorStop(1, colorMin);
162
- } else {
163
- gradient.addColorStop(0, colorMin);
164
- gradient.addColorStop(1, colorMax);
165
- }
360
+ gradient.addColorStop(0, colorMin);
361
+ gradient.addColorStop(1, colorMax);
166
362
 
167
363
  ctx.strokeStyle = gradient;
168
-
169
364
  ctx.stroke();
170
365
  ctx.closePath();
366
+ };
171
367
 
172
- if (!!fillColor) {
173
- ctx.fillStyle = fillColor;
174
- ctx.beginPath();
175
- ctx.moveTo(x1, y1);
176
- ctx.lineTo(x2, y2);
177
- ctx.lineTo(x2, drawBottom);
178
- ctx.lineTo(x1, drawBottom);
179
- ctx.closePath();
180
- ctx.fill();
181
- }
368
+ /**
369
+ * Fills the area below the trendline with the specified color.
370
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
371
+ * @param {number} x1 - Starting x-coordinate of the trendline.
372
+ * @param {number} y1 - Starting y-coordinate of the trendline.
373
+ * @param {number} x2 - Ending x-coordinate of the trendline.
374
+ * @param {number} y2 - Ending y-coordinate of the trendline.
375
+ * @param {number} drawBottom - The bottom boundary of the chart.
376
+ * @param {string} fillColor - The color to fill below the trendline.
377
+ */
378
+ const fillBelowTrendline = (ctx, x1, y1, x2, y2, drawBottom, fillColor) => {
379
+ ctx.beginPath();
380
+ ctx.moveTo(x1, y1);
381
+ ctx.lineTo(x2, y2);
382
+ ctx.lineTo(x2, drawBottom);
383
+ ctx.lineTo(x1, drawBottom);
384
+ ctx.lineTo(x1, y1);
385
+ ctx.closePath();
386
+
387
+ ctx.fillStyle = fillColor;
388
+ ctx.fill();
182
389
  };
183
390
 
391
+ /**
392
+ * A class that fits a line to a series of points using least squares.
393
+ */
184
394
  class LineFitter {
185
395
  constructor() {
186
396
  this.count = 0;
187
- this.sumX = 0;
188
- this.sumX2 = 0;
189
- this.sumXY = 0;
190
- this.sumY = 0;
191
- this.minx = 1e100;
192
- this.maxx = -1e100;
193
- this.maxy = -1e100;
397
+ this.sumx = 0;
398
+ this.sumy = 0;
399
+ this.sumx2 = 0;
400
+ this.sumxy = 0;
401
+ this.minx = Number.MAX_VALUE;
402
+ this.maxx = Number.MIN_VALUE;
194
403
  }
195
404
 
405
+ /**
406
+ * Adds a point to the line fitter.
407
+ * @param {number} x - The x-coordinate of the point.
408
+ * @param {number} y - The y-coordinate of the point.
409
+ */
196
410
  add(x, y) {
197
- x = parseFloat(x);
198
- y = parseFloat(y);
199
-
200
- this.count++;
201
- this.sumX += x;
202
- this.sumX2 += x * x;
203
- this.sumXY += x * y;
204
- this.sumY += y;
411
+ this.sumx += x;
412
+ this.sumy += y;
413
+ this.sumx2 += x * x;
414
+ this.sumxy += x * y;
205
415
  if (x < this.minx) this.minx = x;
206
416
  if (x > this.maxx) this.maxx = x;
207
- if (y > this.maxy) this.maxy = y;
417
+ this.count++;
208
418
  }
209
419
 
210
- f(x) {
211
- x = parseFloat(x);
420
+ /**
421
+ * Calculates the slope of the fitted line.
422
+ * @returns {number} - The slope of the line.
423
+ */
424
+ slope() {
425
+ const denominator = this.count * this.sumx2 - this.sumx * this.sumx;
426
+ return (this.count * this.sumxy - this.sumx * this.sumy) / denominator;
427
+ }
212
428
 
213
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
214
- let offset = (this.sumX2 * this.sumY - this.sumX * this.sumXY) / det;
215
- let scale = (this.count * this.sumXY - this.sumX * this.sumY) / det;
216
- return offset + x * scale;
429
+ /**
430
+ * Calculates the y-intercept of the fitted line.
431
+ * @returns {number} - The y-intercept of the line.
432
+ */
433
+ intercept() {
434
+ return (this.sumy - this.slope() * this.sumx) / this.count;
217
435
  }
218
436
 
219
- fo() {
220
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
221
- let offset = (this.sumX2 * this.sumY - this.sumX * this.sumXY) / det;
222
- let scale = (this.count * this.sumXY - this.sumX * this.sumY) / det;
437
+ /**
438
+ * Returns the fitted value (y) for a given x.
439
+ * @param {number} x - The x-coordinate.
440
+ * @returns {number} - The corresponding y-coordinate on the fitted line.
441
+ */
442
+ f(x) {
443
+ return this.slope() * x + this.intercept();
444
+ }
223
445
 
224
- // Get x when y = 0
225
- let xo = -offset / scale;
226
- return xo;
446
+ /**
447
+ * Calculates the projection of the line for the future value.
448
+ * @returns {number} - The future value based on the fitted line.
449
+ */
450
+ fo() {
451
+ return -this.intercept() / this.slope();
227
452
  }
228
453
 
454
+ /**
455
+ * Returns the scale (variance) of the fitted line.
456
+ * @returns {number} - The scale of the fitted line.
457
+ */
229
458
  scale() {
230
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
231
- let scale = (this.count * this.sumXY - this.sumX * this.sumY) / det;
232
-
233
- return scale;
459
+ return this.slope();
234
460
  }
235
461
  }
236
462
 
@@ -1,79 +0,0 @@
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
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
- <title>BarChart Example</title>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.js"></script>
9
- <script src="./../src/chartjs-plugin-trendline.js"></script>
10
- <script>
11
-
12
- const datasets = [{
13
- data: [
14
- { x: '202309', y: 144214.8 },
15
- { x: '202310', y: 144514.8 },
16
- { x: '202311', y: 150626 },
17
- { x: '202312', y: 204523 },
18
- { x: '202401', y: 125402 },
19
- { x: '202402', y: 225232 },
20
- { x: '202403', y: 2786435 },
21
- { x: '202404', y: 584833 },
22
- ],
23
- label: 'Usage Not Working!',
24
- yAxisID: 'y',
25
- lineTension: 0,
26
- borderWidth: 2,
27
- trendlineLinear: {
28
- colorMin: '#ff0000',
29
- colorMax: '#00ff00',
30
- lineStyle: 'dotted',
31
- width: 2,
32
- yAxisKey: 'y',
33
- xAxisKey: 'x',
34
- projection: true,
35
- },
36
- }, ]
37
- const chartConfig = {
38
- type: 'line',
39
- data: {
40
- datasets: datasets
41
- },
42
- options: {
43
- scales: {
44
- x: {
45
- ticks: {
46
- padding: 8
47
- },
48
- },
49
- y: {
50
- type: 'linear',
51
- display: true,
52
- position: 'left',
53
- title: {
54
- display: true,
55
- text: 'Usage',
56
- padding: 8,
57
- },
58
- },
59
- },
60
- },
61
- plugins: [pluginTrendlineLinear],
62
- };
63
-
64
- document.addEventListener("DOMContentLoaded", function(event) {
65
- // Bar chart
66
- new Chart(document.getElementById("bar-chart"), chartConfig);
67
- });
68
- </script>
69
- </head>
70
- <body>
71
- <h1>Bar Chart</h1>
72
-
73
- <div style="width: 800px;">
74
- <canvas id="bar-chart"></canvas>
75
- </div>
76
-
77
- <p>Using example code from <a href="http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/" target="_blank">tobiasahlin.com.</a></p>
78
- </body>
79
- </html>
@@ -1,72 +0,0 @@
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
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
- <title>BarChart Example</title>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.js"></script>
9
- <script src="./../src/chartjs-plugin-trendline.js"></script>
10
- <script>
11
-
12
- const datasets = [{
13
- data: [144214.8, 144514.8, 150626, 204523, 125402, 225232, 2786435, 584833],
14
- label: 'Usage',
15
- yAxisID: 'y',
16
- lineTension: 0,
17
- borderWidth: 2,
18
- trendlineLinear: {
19
- colorMin: '#ff0000',
20
- colorMax: '#00ff00',
21
- lineStyle: 'dotted',
22
- width: 2,
23
- yAxisKey: 'y',
24
- xAxisKey: 'x',
25
- projection: true,
26
- },
27
- }, ]
28
-
29
- const chartConfig = {
30
- type: 'line',
31
- data: {
32
- datasets: datasets,
33
- labels: ['202309', '202310', '202311', '202401', '202402', '202403', '202404', '20240'],
34
- },
35
- options: {
36
- scales: {
37
- x: {
38
- ticks: {
39
- padding: 8
40
- },
41
- },
42
- y: {
43
- type: 'linear',
44
- display: true,
45
- position: 'left',
46
- title: {
47
- display: true,
48
- text: 'Usage',
49
- padding: 8,
50
- },
51
- },
52
- },
53
- },
54
- plugins: [pluginTrendlineLinear],
55
- };
56
-
57
- document.addEventListener("DOMContentLoaded", function(event) {
58
- // Bar chart
59
- new Chart(document.getElementById("bar-chart"), chartConfig);
60
- });
61
- </script>
62
- </head>
63
- <body>
64
- <h1>Bar Chart</h1>
65
-
66
- <div style="width: 800px;">
67
- <canvas id="bar-chart"></canvas>
68
- </div>
69
-
70
- <p>Using example code from <a href="http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/" target="_blank">tobiasahlin.com.</a></p>
71
- </body>
72
- </html>