chartjs-plugin-trendline 2.1.4 → 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|dashed|dashdot",
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 s={id:"chartjs-plugin-trendline",afterDatasetsDraw:t=>{let e,s;for(let i in t.scales)if("x"==i[0]?s=t.scales[i]:e=t.scales[i],s&&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);i(r,a,e,s,t.scales[r.yAxisID])}})),a.setLineDash([])}},i=(t,e,s,i,n)=>{let r=s.borderColor||"rgba(169,169,169, .6)",o=s.trendlineLinear.colorMin||r,l=s.trendlineLinear.colorMax||r,h=s.trendlineLinear.width??s.borderWidth??3,d=s.trendlineLinear.lineStyle||"solid",u=s.trendlineLinear.fillColor;const m=t.controller.chart.options,c="object"==typeof m.parsing?m.parsing:void 0,x=s.trendlineLinear.xAxisKey||c?.xAxisKey||"x",f=s.trendlineLinear.yAxisKey||c?.yAxisKey||"y";let X=new a,y=s.data.findIndex((t=>null!=t)),p=s.data.length-1,g=t.data[y][x],w=t.data[p][x],L="object"==typeof s.data[y];s.data.forEach(((t,e)=>{if(null!=t)if(["time","timeseries"].includes(i.options.type)){let s=null!=t[x]?t[x]:t.t;void 0!==s?X.add(new Date(s).getTime(),t[f]):X.add(e,t)}else L?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 Y,b,D=i.getPixelForValue(X.minx),F=n.getPixelForValue(X.f(X.minx));if(s.trendlineLinear.projection&&X.scale()<0){let t=X.fo();t<X.minx&&(t=X.maxx),Y=i.getPixelForValue(t),b=n.getPixelForValue(X.f(t))}else Y=i.getPixelForValue(X.maxx),b=n.getPixelForValue(X.f(X.maxx));(isFinite(g)||isFinite(w))&&(D=g,Y=w);let P=t.controller.chart.chartArea.bottom,C=t.controller.chart.width;if(F>P){let t=F-P,e=F-b;F=P,D+=C*(t/e)}else if(b>P){let t=b-P,e=b-F;b=P,Y=C-(Y-(C-C*(t/e)))}switch(e.lineWidth=h,d){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([])}e.beginPath(),e.moveTo(D,F),e.lineTo(Y,b);let v=e.createLinearGradient(D,F,Y,b);b<F?(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(D,F),e.lineTo(Y,b),e.lineTo(Y,P),e.lineTo(D,P),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(s):window.Chart.plugins.register(s));try{t.exports=s}catch(t){}}},e={};!function s(i){var a=e[i];if(void 0!==a)return a.exports;var n=e[i]={exports:{}};return t[i](n,n.exports,s),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.4
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
  */
@@ -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.4",
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.4
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;
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
+ };
125
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,106 +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
- // line styles
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) => {
149
326
  switch (lineStyle) {
150
327
  case 'dotted':
151
- ctx.setLineDash([2, 2]); // Dotted
328
+ ctx.setLineDash([2, 2]);
152
329
  break;
153
330
  case 'dashed':
154
- ctx.setLineDash([8, 3]); // Dashed
331
+ ctx.setLineDash([8, 3]);
155
332
  break;
156
333
  case 'dashdot':
157
- ctx.setLineDash([8, 3, 2, 3]); // Dash-dot
334
+ ctx.setLineDash([8, 3, 2, 3]);
158
335
  break;
159
336
  case 'solid':
160
337
  default:
161
- ctx.setLineDash([]); // Solid
338
+ ctx.setLineDash([]);
162
339
  break;
163
340
  }
341
+ };
164
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 }) => {
165
355
  ctx.beginPath();
166
356
  ctx.moveTo(x1, y1);
167
357
  ctx.lineTo(x2, y2);
168
358
 
169
359
  let gradient = ctx.createLinearGradient(x1, y1, x2, y2);
170
- if (y2 < y1) {
171
- gradient.addColorStop(0, colorMax);
172
- gradient.addColorStop(1, colorMin);
173
- } else {
174
- gradient.addColorStop(0, colorMin);
175
- gradient.addColorStop(1, colorMax);
176
- }
360
+ gradient.addColorStop(0, colorMin);
361
+ gradient.addColorStop(1, colorMax);
177
362
 
178
363
  ctx.strokeStyle = gradient;
179
-
180
364
  ctx.stroke();
181
365
  ctx.closePath();
366
+ };
182
367
 
183
- if (!!fillColor) {
184
- ctx.fillStyle = fillColor;
185
- ctx.beginPath();
186
- ctx.moveTo(x1, y1);
187
- ctx.lineTo(x2, y2);
188
- ctx.lineTo(x2, drawBottom);
189
- ctx.lineTo(x1, drawBottom);
190
- ctx.closePath();
191
- ctx.fill();
192
- }
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();
193
389
  };
194
390
 
391
+ /**
392
+ * A class that fits a line to a series of points using least squares.
393
+ */
195
394
  class LineFitter {
196
395
  constructor() {
197
396
  this.count = 0;
198
- this.sumX = 0;
199
- this.sumX2 = 0;
200
- this.sumXY = 0;
201
- this.sumY = 0;
202
- this.minx = 1e100;
203
- this.maxx = -1e100;
204
- 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;
205
403
  }
206
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
+ */
207
410
  add(x, y) {
208
- x = parseFloat(x);
209
- y = parseFloat(y);
210
-
211
- this.count++;
212
- this.sumX += x;
213
- this.sumX2 += x * x;
214
- this.sumXY += x * y;
215
- this.sumY += y;
411
+ this.sumx += x;
412
+ this.sumy += y;
413
+ this.sumx2 += x * x;
414
+ this.sumxy += x * y;
216
415
  if (x < this.minx) this.minx = x;
217
416
  if (x > this.maxx) this.maxx = x;
218
- if (y > this.maxy) this.maxy = y;
417
+ this.count++;
219
418
  }
220
419
 
221
- f(x) {
222
- 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
+ }
223
428
 
224
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
225
- let offset = (this.sumX2 * this.sumY - this.sumX * this.sumXY) / det;
226
- let scale = (this.count * this.sumXY - this.sumX * this.sumY) / det;
227
- 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;
228
435
  }
229
436
 
230
- fo() {
231
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
232
- let offset = (this.sumX2 * this.sumY - this.sumX * this.sumXY) / det;
233
- 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
+ }
234
445
 
235
- // Get x when y = 0
236
- let xo = -offset / scale;
237
- 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();
238
452
  }
239
453
 
454
+ /**
455
+ * Returns the scale (variance) of the fitted line.
456
+ * @returns {number} - The scale of the fitted line.
457
+ */
240
458
  scale() {
241
- let det = this.count * this.sumX2 - this.sumX * this.sumX;
242
- let scale = (this.count * this.sumXY - this.sumX * this.sumY) / det;
243
-
244
- return scale;
459
+ return this.slope();
245
460
  }
246
461
  }
247
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>