chartjs-plugin-trendline 3.1.0 → 3.1.1

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.
@@ -1 +1 @@
1
- (()=>{"use strict";class t{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(t,e){this.sumx+=t,this.sumy+=e,this.sumx2+=t*t,this.sumxy+=t*e,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t),this.count++}slope(){const t=this.count*this.sumx2-this.sumx*this.sumx;return(this.count*this.sumxy-this.sumx*this.sumy)/t}intercept(){return(this.sumy-this.slope()*this.sumx)/this.count}f(t){return this.slope()*t+this.intercept()}fo(){return-this.intercept()/this.slope()}scale(){return this.slope()}}class e{constructor(){this.count=0,this.sumx=0,this.sumlny=0,this.sumx2=0,this.sumxlny=0,this.minx=Number.MAX_VALUE,this.maxx=Number.MIN_VALUE,this.hasValidData=!0,this.dataPoints=[]}add(t,e){if(e<=0)return void(this.hasValidData=!1);const i=Math.log(e);isFinite(i)?(this.sumx+=t,this.sumlny+=i,this.sumx2+=t*t,this.sumxlny+=t*i,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t),this.dataPoints.push({x:t,y:e,lny:i}),this.count++):this.hasValidData=!1}growthRate(){if(!this.hasValidData||this.count<2)return 0;const t=this.count*this.sumx2-this.sumx*this.sumx;return Math.abs(t)<1e-10?0:(this.count*this.sumxlny-this.sumx*this.sumlny)/t}coefficient(){if(!this.hasValidData||this.count<2)return 1;const t=(this.sumlny-this.growthRate()*this.sumx)/this.count;return Math.exp(t)}f(t){if(!this.hasValidData||this.count<2)return 0;const e=this.coefficient(),i=this.growthRate();if(Math.abs(i*t)>500)return 0;const s=e*Math.exp(i*t);return isFinite(s)?s:0}correlation(){if(!this.hasValidData||this.count<2)return 0;const t=this.sumlny/this.count,e=Math.log(this.coefficient()),i=this.growthRate();let s=0,n=0;for(const a of this.dataPoints){const l=e+i*a.x;s+=Math.pow(a.lny-t,2),n+=Math.pow(a.lny-l,2)}return 0===s?1:Math.max(0,1-n/s)}scale(){return this.growthRate()}}const i={id:"chartjs-plugin-trendline",afterDatasetsDraw:i=>{const s=i.ctx,{xScale:n,yScale:a}=(t=>{let e,i;for(const s of Object.values(t.scales))if(s.isHorizontal()?e=s:i=s,e&&i)break;return{xScale:e,yScale:i}})(i);i.data.datasets.map(((t,e)=>({dataset:t,index:e}))).filter((t=>t.dataset.trendlineLinear||t.dataset.trendlineExponential)).sort(((t,e)=>{const i=t.dataset.order??0,s=e.dataset.order??0;return 0===i&&0!==s?1:0===s&&0!==i?-1:i-s})).forEach((({dataset:l,index:o})=>{if((l.alwaysShowTrendline||i.isDatasetVisible(o))&&l.data.length>1){((i,s,n,a,l)=>{const o=n.yAxisID||"y",r=i.controller.chart.scales[o]||l,h=!!n.trendlineExponential,c=n.trendlineExponential||n.trendlineLinear||{},u=n.borderColor||"rgba(169,169,169, .6)",{colorMin:x=u,colorMax:d=u,width:f=n.borderWidth||3,lineStyle:y="solid",fillColor:m=!1}=c;let g=c.trendoffset||0;const{color:p=u,text:b=(h?"Exponential Trendline":"Trendline"),display:F=!0,displayValue:w=!0,offset:V=10,percentage:M=!1}=c&&c.label||{},{family:P="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:N=12}=c&&c.label&&c.label.font||{},D=i.controller.chart.options,S="object"==typeof D.parsing?D.parsing:void 0,L=c?.xAxisKey||S?.xAxisKey||"x",T=c?.yAxisKey||S?.yAxisKey||"y";let A=h?new e:new t;Math.abs(g)>=n.data.length&&(g=0);let C=0;if(g>0){const t=n.data.slice(g).findIndex((t=>null!=t));C=-1!==t?g+t:n.data.length}else{const t=n.data.findIndex((t=>null!=t));C=-1!==t?t:n.data.length}let v,E,k,I,$=C<n.data.length&&"object"==typeof n.data[C];if(n.data.forEach(((t,e)=>{if(null!=t&&!(g>0&&e<C||g<0&&e>=n.data.length+g))if(["time","timeseries"].includes(a.options.type)&&$){let e=null!=t[L]?t[L]:t.t;const i=t[T];null==e||void 0===e||null==i||isNaN(i)||A.add(new Date(e).getTime(),i)}else if($){const e=t[L],i=t[T],s=null!=e&&!isNaN(e),n=null!=i&&!isNaN(i);s&&n&&A.add(e,i)}else if(["time","timeseries"].includes(a.options.type)&&!$){const s=i.controller.chart.data.labels;if(s&&s[e]&&null!=t&&!isNaN(t)){const i=new Date(s[e]).getTime();isNaN(i)||A.add(i,t)}}else null==t||isNaN(t)||A.add(e,t)})),A.count<2)return;const R=i.controller.chart.chartArea;if(c.projection){let t=[];if(h){const e=a.getValueForPixel(R.left),i=A.f(e);t.push({x:e,y:i});const s=a.getValueForPixel(R.right),n=A.f(s);t.push({x:s,y:n})}else{const e=A.slope(),i=A.intercept();if(Math.abs(e)>1e-6){const s=r.getValueForPixel(R.top),n=(s-i)/e;t.push({x:n,y:s});const a=r.getValueForPixel(R.bottom),l=(a-i)/e;t.push({x:l,y:a})}else t.push({x:a.getValueForPixel(R.left),y:i}),t.push({x:a.getValueForPixel(R.right),y:i});const s=a.getValueForPixel(R.left),n=A.f(s);t.push({x:s,y:n});const l=a.getValueForPixel(R.right),o=A.f(l);t.push({x:l,y:o})}const e=a.getValueForPixel(R.left),i=a.getValueForPixel(R.right),s=[r.getValueForPixel(R.top),r.getValueForPixel(R.bottom)].filter((t=>isFinite(t))),n=s.length>0?Math.min(...s):-1/0,l=s.length>0?Math.max(...s):1/0;let o=t.filter((t=>isFinite(t.x)&&isFinite(t.y)&&t.x>=e&&t.x<=i&&t.y>=n&&t.y<=l));o=o.filter(((t,e,i)=>e===i.findIndex((e=>Math.abs(e.x-t.x)<1e-4&&Math.abs(e.y-t.y)<1e-4)))),o.length>=2?(o.sort(((t,e)=>t.x-e.x||t.y-e.y)),v=a.getPixelForValue(o[0].x),E=r.getPixelForValue(o[0].y),k=a.getPixelForValue(o[o.length-1].x),I=r.getPixelForValue(o[o.length-1].y)):(v=NaN,E=NaN,k=NaN,I=NaN)}else{const t=A.f(A.minx),e=A.f(A.maxx);v=a.getPixelForValue(A.minx),E=r.getPixelForValue(t),k=a.getPixelForValue(A.maxx),I=r.getPixelForValue(e)}let j=null;if(isFinite(v)&&isFinite(E)&&isFinite(k)&&isFinite(I)&&(j=function(t,e,i,s,n){let a=i-t,l=s-e,o=0,r=1;const h=[-a,a,-l,l],c=[t-n.left,n.right-t,e-n.top,n.bottom-e];for(let t=0;t<4;t++)if(0===h[t]){if(c[t]<0)return null}else{const e=c[t]/h[t];if(h[t]<0){if(e>r)return null;o=Math.max(o,e)}else{if(e<o)return null;r=Math.min(r,e)}}return o>r?null:{x1:t+o*a,y1:e+o*l,x2:t+r*a,y2:e+r*l}}(v,E,k,I,R)),j)if(v=j.x1,E=j.y1,k=j.x2,I=j.y2,Math.abs(v-k)<.5&&Math.abs(E-I)<.5);else{s.lineWidth=f,((t,e)=>{switch(e){case"dotted":t.setLineDash([2,2]);break;case"dashed":t.setLineDash([8,3]);break;case"dashdot":t.setLineDash([8,3,2,3]);break;default:t.setLineDash([])}})(s,y),(({ctx:t,x1:e,y1:i,x2:s,y2:n,colorMin:a,colorMax:l})=>{if(isFinite(e)&&isFinite(i)&&isFinite(s)&&isFinite(n)){t.beginPath(),t.moveTo(e,i),t.lineTo(s,n);try{const o=s-e,r=n-i,h=Math.sqrt(o*o+r*r);if(h<.01)console.warn("Gradient vector too small, using solid color:",{x1:e,y1:i,x2:s,y2:n,length:h}),t.strokeStyle=a;else{let o=t.createLinearGradient(e,i,s,n);o.addColorStop(0,a),o.addColorStop(1,l),t.strokeStyle=o}}catch(e){console.warn("Gradient creation failed, using solid color:",e),t.strokeStyle=a}t.stroke(),t.closePath()}else console.warn("Cannot draw trendline: coordinates contain non-finite values",{x1:e,y1:i,x2:s,y2:n})})({ctx:s,x1:v,y1:E,x2:k,y2:I,colorMin:x,colorMax:d}),m&&((t,e,i,s,n,a,l)=>{isFinite(e)&&isFinite(i)&&isFinite(s)&&isFinite(n)&&isFinite(a)?(t.beginPath(),t.moveTo(e,i),t.lineTo(s,n),t.lineTo(s,a),t.lineTo(e,a),t.lineTo(e,i),t.closePath(),t.fillStyle=l,t.fill()):console.warn("Cannot fill below trendline: coordinates contain non-finite values",{x1:e,y1:i,x2:s,y2:n,drawBottom:a})})(s,v,E,k,I,R.bottom,m);const t=Math.atan2(I-E,k-v);if(c.label&&!1!==F){let e=b;if(w)if(h){const t=A.coefficient(),i=A.growthRate();e=`${b} (a=${t.toFixed(2)}, b=${i.toFixed(2)})`}else{const t=A.slope();e=`${b} (Slope: ${M?(100*t).toFixed(2)+"%":t.toFixed(2)})`}((t,e,i,s,n,a,l,o,r,h,c)=>{t.font=`${h}px ${r}`,t.fillStyle=o;const u=t.measureText(e).width,x=(i+n)/2,d=(s+a)/2;t.save(),t.translate(x,d),t.rotate(l);const f=-u/2,y=c;t.fillText(e,f,y),t.restore()})(s,e,v,E,k,I,t,p,P,N,V)}}})(i.getDatasetMeta(o),s,l,n,a)}})),s.setLineDash([])},beforeInit:t=>{t.data.datasets.forEach((e=>{const i=e.trendlineLinear||e.trendlineExponential;if(i&&i.label){const s=i.label,n=t.legend.options.labels.generateLabels;t.legend.options.labels.generateLabels=function(t){const a=n(t),l=i.legend;return l&&!1!==l.display&&a.push({text:l.text||s+" (Trendline)",strokeStyle:l.color||e.borderColor||"rgba(169,169,169, .6)",fillStyle:l.fillStyle||"transparent",lineCap:l.lineCap||"butt",lineDash:l.lineDash||[],lineWidth:l.width||1}),a}}}))}};"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(i):window.Chart.plugins.register(i))})();
1
+ (()=>{"use strict";class t{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,this._cachedSlope=null,this._cachedIntercept=null,this._cacheValid=!1}add(t,e){this.sumx+=t,this.sumy+=e,this.sumx2+=t*t,this.sumxy+=t*e,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t),this.count++,this._cacheValid=!1}slope(){return this._cacheValid||this._computeCoefficients(),this._cachedSlope}intercept(){return this._cacheValid||this._computeCoefficients(),this._cachedIntercept}f(t){return this.slope()*t+this.intercept()}fo(){return-this.intercept()/this.slope()}scale(){return this.slope()}_computeCoefficients(){const t=this.count*this.sumx2-this.sumx*this.sumx;this._cachedSlope=(this.count*this.sumxy-this.sumx*this.sumy)/t,this._cachedIntercept=(this.sumy-this._cachedSlope*this.sumx)/this.count,this._cacheValid=!0}}class e{constructor(){this.count=0,this.sumx=0,this.sumlny=0,this.sumx2=0,this.sumxlny=0,this.minx=Number.MAX_VALUE,this.maxx=Number.MIN_VALUE,this.hasValidData=!0,this.dataPoints=[],this._cachedGrowthRate=null,this._cachedCoefficient=null,this._cachedCorrelation=null,this._cacheValid=!1}add(t,e){if(e<=0)return void(this.hasValidData=!1);const i=Math.log(e);isFinite(i)?(this.sumx+=t,this.sumlny+=i,this.sumx2+=t*t,this.sumxlny+=t*i,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t),this.dataPoints.push({x:t,y:e,lny:i}),this.count++,this._cacheValid=!1):this.hasValidData=!1}growthRate(){return!this.hasValidData||this.count<2?0:(this._cacheValid||this._computeCoefficients(),this._cachedGrowthRate)}coefficient(){return!this.hasValidData||this.count<2?1:(this._cacheValid||this._computeCoefficients(),this._cachedCoefficient)}f(t){if(!this.hasValidData||this.count<2)return 0;if(this._cacheValid||this._computeCoefficients(),Math.abs(this._cachedGrowthRate*t)>500)return 0;const e=this._cachedCoefficient*Math.exp(this._cachedGrowthRate*t);return isFinite(e)?e:0}correlation(){return!this.hasValidData||this.count<2?0:(this._cacheValid||this._computeCoefficients(),this._cachedCorrelation)}scale(){return this.growthRate()}_computeCoefficients(){if(!this.hasValidData||this.count<2)return this._cachedGrowthRate=0,this._cachedCoefficient=1,this._cachedCorrelation=0,void(this._cacheValid=!0);const t=this.count*this.sumx2-this.sumx*this.sumx;if(Math.abs(t)<1e-10)return this._cachedGrowthRate=0,this._cachedCoefficient=1,this._cachedCorrelation=0,void(this._cacheValid=!0);this._cachedGrowthRate=(this.count*this.sumxlny-this.sumx*this.sumlny)/t;const e=(this.sumlny-this._cachedGrowthRate*this.sumx)/this.count;this._cachedCoefficient=Math.exp(e);const i=this.sumlny/this.count;let s=0,a=0;for(const t of this.dataPoints){const n=e+this._cachedGrowthRate*t.x;s+=Math.pow(t.lny-i,2),a+=Math.pow(t.lny-n,2)}this._cachedCorrelation=0===s?1:Math.max(0,1-a/s),this._cacheValid=!0}}const i={id:"chartjs-plugin-trendline",afterDatasetsDraw:i=>{const s=i.ctx,{xScale:a,yScale:n}=(t=>{let e,i;for(const s of Object.values(t.scales))if(s.isHorizontal()?e=s:i=s,e&&i)break;return{xScale:e,yScale:i}})(i);i.data.datasets.map(((t,e)=>({dataset:t,index:e}))).filter((t=>t.dataset.trendlineLinear||t.dataset.trendlineExponential)).sort(((t,e)=>{const i=t.dataset.order??0,s=e.dataset.order??0;return 0===i&&0!==s?1:0===s&&0!==i?-1:i-s})).forEach((({dataset:l,index:o})=>{if((l.alwaysShowTrendline||i.isDatasetVisible(o))&&l.data.length>1){((i,s,a,n,l)=>{const o=a.yAxisID||"y",h=i.controller.chart.scales[o]||l,c=!!a.trendlineExponential,r=a.trendlineExponential||a.trendlineLinear||{},d=a.borderColor||"rgba(169,169,169, .6)",{colorMin:u=d,colorMax:x=d,width:f=a.borderWidth||3,lineStyle:m="solid",fillColor:y=!1}=r;let p=r.trendoffset||0;const{color:g=d,text:_=(c?"Exponential Trendline":"Trendline"),display:V=!0,displayValue:b=!0,offset:w=10,percentage:F=!1}=r&&r.label||{},{family:C="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:M=12}=r&&r.label&&r.label.font||{},P=i.controller.chart.options,N="object"==typeof P.parsing?P.parsing:void 0,D=r?.xAxisKey||N?.xAxisKey||"x",S=r?.yAxisKey||N?.yAxisKey||"y";let L=c?new e:new t;Math.abs(p)>=a.data.length&&(p=0);let T=0;if(p>0){const t=a.data.slice(p).findIndex((t=>null!=t));T=-1!==t?p+t:a.data.length}else{const t=a.data.findIndex((t=>null!=t));T=-1!==t?t:a.data.length}let v,A,E,G,R=T<a.data.length&&"object"==typeof a.data[T];if(a.data.forEach(((t,e)=>{if(null!=t&&!(p>0&&e<T||p<0&&e>=a.data.length+p))if(["time","timeseries"].includes(n.options.type)&&R){let e=null!=t[D]?t[D]:t.t;const i=t[S];null==e||void 0===e||null==i||isNaN(i)||L.add(new Date(e).getTime(),i)}else if(R){const e=t[D],i=t[S],s=null!=e&&!isNaN(e),a=null!=i&&!isNaN(i);s&&a&&L.add(e,i)}else if(["time","timeseries"].includes(n.options.type)&&!R){const s=i.controller.chart.data.labels;if(s&&s[e]&&null!=t&&!isNaN(t)){const i=new Date(s[e]).getTime();isNaN(i)||L.add(i,t)}}else null==t||isNaN(t)||L.add(e,t)})),L.count<2)return;const I=i.controller.chart.chartArea;if(r.projection){let t=[];if(c){const e=n.getValueForPixel(I.left),i=L.f(e);t.push({x:e,y:i});const s=n.getValueForPixel(I.right),a=L.f(s);t.push({x:s,y:a})}else{const e=L.slope(),i=L.intercept();if(Math.abs(e)>1e-6){const s=h.getValueForPixel(I.top),a=(s-i)/e;t.push({x:a,y:s});const n=h.getValueForPixel(I.bottom),l=(n-i)/e;t.push({x:l,y:n})}else t.push({x:n.getValueForPixel(I.left),y:i}),t.push({x:n.getValueForPixel(I.right),y:i});const s=n.getValueForPixel(I.left),a=L.f(s);t.push({x:s,y:a});const l=n.getValueForPixel(I.right),o=L.f(l);t.push({x:l,y:o})}const e=n.getValueForPixel(I.left),i=n.getValueForPixel(I.right),s=[h.getValueForPixel(I.top),h.getValueForPixel(I.bottom)].filter((t=>isFinite(t))),a=s.length>0?Math.min(...s):-1/0,l=s.length>0?Math.max(...s):1/0;let o=t.filter((t=>isFinite(t.x)&&isFinite(t.y)&&t.x>=e&&t.x<=i&&t.y>=a&&t.y<=l));o=o.filter(((t,e,i)=>e===i.findIndex((e=>Math.abs(e.x-t.x)<1e-4&&Math.abs(e.y-t.y)<1e-4)))),o.length>=2?(o.sort(((t,e)=>t.x-e.x||t.y-e.y)),v=n.getPixelForValue(o[0].x),A=h.getPixelForValue(o[0].y),E=n.getPixelForValue(o[o.length-1].x),G=h.getPixelForValue(o[o.length-1].y)):(v=NaN,A=NaN,E=NaN,G=NaN)}else{const t=L.f(L.minx),e=L.f(L.maxx);v=n.getPixelForValue(L.minx),A=h.getPixelForValue(t),E=n.getPixelForValue(L.maxx),G=h.getPixelForValue(e)}let k=null;if(isFinite(v)&&isFinite(A)&&isFinite(E)&&isFinite(G)&&(k=function(t,e,i,s,a){let n=i-t,l=s-e,o=0,h=1;const c=[-n,n,-l,l],r=[t-a.left,a.right-t,e-a.top,a.bottom-e];for(let t=0;t<4;t++)if(0===c[t]){if(r[t]<0)return null}else{const e=r[t]/c[t];if(c[t]<0){if(e>h)return null;o=Math.max(o,e)}else{if(e<o)return null;h=Math.min(h,e)}}return o>h?null:{x1:t+o*n,y1:e+o*l,x2:t+h*n,y2:e+h*l}}(v,A,E,G,I)),k)if(v=k.x1,A=k.y1,E=k.x2,G=k.y2,Math.abs(v-E)<.5&&Math.abs(A-G)<.5);else{s.lineWidth=f,((t,e)=>{switch(e){case"dotted":t.setLineDash([2,2]);break;case"dashed":t.setLineDash([8,3]);break;case"dashdot":t.setLineDash([8,3,2,3]);break;default:t.setLineDash([])}})(s,m),(({ctx:t,x1:e,y1:i,x2:s,y2:a,colorMin:n,colorMax:l})=>{if(isFinite(e)&&isFinite(i)&&isFinite(s)&&isFinite(a)){t.beginPath(),t.moveTo(e,i),t.lineTo(s,a);try{const o=s-e,h=a-i,c=Math.sqrt(o*o+h*h);if(c<.01)console.warn("Gradient vector too small, using solid color:",{x1:e,y1:i,x2:s,y2:a,length:c}),t.strokeStyle=n;else{let o=t.createLinearGradient(e,i,s,a);o.addColorStop(0,n),o.addColorStop(1,l),t.strokeStyle=o}}catch(e){console.warn("Gradient creation failed, using solid color:",e),t.strokeStyle=n}t.stroke(),t.closePath()}else console.warn("Cannot draw trendline: coordinates contain non-finite values",{x1:e,y1:i,x2:s,y2:a})})({ctx:s,x1:v,y1:A,x2:E,y2:G,colorMin:u,colorMax:x}),y&&((t,e,i,s,a,n,l)=>{isFinite(e)&&isFinite(i)&&isFinite(s)&&isFinite(a)&&isFinite(n)?(t.beginPath(),t.moveTo(e,i),t.lineTo(s,a),t.lineTo(s,n),t.lineTo(e,n),t.lineTo(e,i),t.closePath(),t.fillStyle=l,t.fill()):console.warn("Cannot fill below trendline: coordinates contain non-finite values",{x1:e,y1:i,x2:s,y2:a,drawBottom:n})})(s,v,A,E,G,I.bottom,y);const t=Math.atan2(G-A,E-v);if(r.label&&!1!==V){let e=_;if(b)if(c){const t=L.coefficient(),i=L.growthRate();e=`${_} (a=${t.toFixed(2)}, b=${i.toFixed(2)})`}else{const t=L.slope();e=`${_} (Slope: ${F?(100*t).toFixed(2)+"%":t.toFixed(2)})`}((t,e,i,s,a,n,l,o,h,c,r)=>{t.font=`${c}px ${h}`,t.fillStyle=o;const d=t.measureText(e).width,u=(i+a)/2,x=(s+n)/2;t.save(),t.translate(u,x),t.rotate(l);const f=-d/2,m=r;t.fillText(e,f,m),t.restore()})(s,e,v,A,E,G,t,g,C,M,w)}}})(i.getDatasetMeta(o),s,l,a,n)}})),s.setLineDash([])},beforeInit:t=>{t.data.datasets.forEach((e=>{const i=e.trendlineLinear||e.trendlineExponential;if(i&&i.label){const s=i.label,a=t.legend.options.labels.generateLabels;t.legend.options.labels.generateLabels=function(t){const n=a(t),l=i.legend;return l&&!1!==l.display&&n.push({text:l.text||s+" (Trendline)",strokeStyle:l.color||e.borderColor||"rgba(169,169,169, .6)",fillStyle:l.fillStyle||"transparent",lineCap:l.lineCap||"butt",lineDash:l.lineDash||[],lineWidth:l.width||1}),n}}}))}};"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(i):window.Chart.plugins.register(i))})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-plugin-trendline",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Trendline for Chart.js",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -13,6 +13,10 @@ export class ExponentialFitter {
13
13
  this.maxx = Number.MIN_VALUE;
14
14
  this.hasValidData = true;
15
15
  this.dataPoints = []; // Store data points for correlation calculation
16
+ this._cachedGrowthRate = null;
17
+ this._cachedCoefficient = null;
18
+ this._cachedCorrelation = null;
19
+ this._cacheValid = false;
16
20
  }
17
21
 
18
22
  /**
@@ -40,6 +44,7 @@ export class ExponentialFitter {
40
44
  if (x > this.maxx) this.maxx = x;
41
45
  this.dataPoints.push({x, y, lny}); // Store actual data points
42
46
  this.count++;
47
+ this._cacheValid = false;
43
48
  }
44
49
 
45
50
  /**
@@ -48,9 +53,10 @@ export class ExponentialFitter {
48
53
  */
49
54
  growthRate() {
50
55
  if (!this.hasValidData || this.count < 2) return 0;
51
- const denominator = this.count * this.sumx2 - this.sumx * this.sumx;
52
- if (Math.abs(denominator) < 1e-10) return 0;
53
- return (this.count * this.sumxlny - this.sumx * this.sumlny) / denominator;
56
+ if (!this._cacheValid) {
57
+ this._computeCoefficients();
58
+ }
59
+ return this._cachedGrowthRate;
54
60
  }
55
61
 
56
62
  /**
@@ -59,8 +65,10 @@ export class ExponentialFitter {
59
65
  */
60
66
  coefficient() {
61
67
  if (!this.hasValidData || this.count < 2) return 1;
62
- const lnA = (this.sumlny - this.growthRate() * this.sumx) / this.count;
63
- return Math.exp(lnA);
68
+ if (!this._cacheValid) {
69
+ this._computeCoefficients();
70
+ }
71
+ return this._cachedCoefficient;
64
72
  }
65
73
 
66
74
  /**
@@ -70,13 +78,14 @@ export class ExponentialFitter {
70
78
  */
71
79
  f(x) {
72
80
  if (!this.hasValidData || this.count < 2) return 0;
73
- const a = this.coefficient();
74
- const b = this.growthRate();
81
+ if (!this._cacheValid) {
82
+ this._computeCoefficients();
83
+ }
75
84
 
76
85
  // Check for potential overflow before calculation
77
- if (Math.abs(b * x) > 500) return 0; // Safer limit to prevent overflow
86
+ if (Math.abs(this._cachedGrowthRate * x) > 500) return 0; // Safer limit to prevent overflow
78
87
 
79
- const result = a * Math.exp(b * x);
88
+ const result = this._cachedCoefficient * Math.exp(this._cachedGrowthRate * x);
80
89
  return isFinite(result) ? result : 0;
81
90
  }
82
91
 
@@ -86,22 +95,10 @@ export class ExponentialFitter {
86
95
  */
87
96
  correlation() {
88
97
  if (!this.hasValidData || this.count < 2) return 0;
89
-
90
- const meanLnY = this.sumlny / this.count;
91
- const lnA = Math.log(this.coefficient());
92
- const b = this.growthRate();
93
-
94
- let ssTotal = 0;
95
- let ssRes = 0;
96
-
97
- for (const point of this.dataPoints) {
98
- const predictedLnY = lnA + b * point.x;
99
- ssTotal += Math.pow(point.lny - meanLnY, 2);
100
- ssRes += Math.pow(point.lny - predictedLnY, 2);
98
+ if (!this._cacheValid) {
99
+ this._computeCoefficients();
101
100
  }
102
-
103
- if (ssTotal === 0) return 1;
104
- return Math.max(0, 1 - (ssRes / ssTotal));
101
+ return this._cachedCorrelation;
105
102
  }
106
103
 
107
104
  /**
@@ -111,4 +108,40 @@ export class ExponentialFitter {
111
108
  scale() {
112
109
  return this.growthRate();
113
110
  }
111
+
112
+ _computeCoefficients() {
113
+ if (!this.hasValidData || this.count < 2) {
114
+ this._cachedGrowthRate = 0;
115
+ this._cachedCoefficient = 1;
116
+ this._cachedCorrelation = 0;
117
+ this._cacheValid = true;
118
+ return;
119
+ }
120
+
121
+ const denominator = this.count * this.sumx2 - this.sumx * this.sumx;
122
+ if (Math.abs(denominator) < 1e-10) {
123
+ this._cachedGrowthRate = 0;
124
+ this._cachedCoefficient = 1;
125
+ this._cachedCorrelation = 0;
126
+ this._cacheValid = true;
127
+ return;
128
+ }
129
+
130
+ this._cachedGrowthRate = (this.count * this.sumxlny - this.sumx * this.sumlny) / denominator;
131
+ const lnA = (this.sumlny - this._cachedGrowthRate * this.sumx) / this.count;
132
+ this._cachedCoefficient = Math.exp(lnA);
133
+
134
+ const meanLnY = this.sumlny / this.count;
135
+ let ssTotal = 0;
136
+ let ssRes = 0;
137
+
138
+ for (const point of this.dataPoints) {
139
+ const predictedLnY = lnA + this._cachedGrowthRate * point.x;
140
+ ssTotal += Math.pow(point.lny - meanLnY, 2);
141
+ ssRes += Math.pow(point.lny - predictedLnY, 2);
142
+ }
143
+
144
+ this._cachedCorrelation = ssTotal === 0 ? 1 : Math.max(0, 1 - (ssRes / ssTotal));
145
+ this._cacheValid = true;
146
+ }
114
147
  }
@@ -248,4 +248,116 @@ describe('ExponentialFitter', () => {
248
248
  expect(smallerResult).toBeLessThan(Infinity);
249
249
  });
250
250
  });
251
+
252
+ describe('caching behavior', () => {
253
+ test('should cache coefficient, growth rate, and correlation calculations', () => {
254
+ const fitter = new ExponentialFitter();
255
+ fitter.add(0, 1);
256
+ fitter.add(1, 2);
257
+ fitter.add(2, 4);
258
+
259
+ // First calls should compute and cache
260
+ const growthRate1 = fitter.growthRate();
261
+ const coefficient1 = fitter.coefficient();
262
+ const correlation1 = fitter.correlation();
263
+
264
+ // Subsequent calls should return cached values
265
+ const growthRate2 = fitter.growthRate();
266
+ const coefficient2 = fitter.coefficient();
267
+ const correlation2 = fitter.correlation();
268
+
269
+ expect(growthRate1).toBe(growthRate2);
270
+ expect(coefficient1).toBe(coefficient2);
271
+ expect(correlation1).toBe(correlation2);
272
+
273
+ // Check that cache is populated
274
+ expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
275
+ expect(fitter._cachedCoefficient).toBeCloseTo(1, 3);
276
+ expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
277
+ expect(fitter._cacheValid).toBe(true);
278
+ });
279
+
280
+ test('should invalidate cache when new data is added', () => {
281
+ const fitter = new ExponentialFitter();
282
+ fitter.add(0, 1);
283
+ fitter.add(1, 2);
284
+
285
+ // Cache should be valid after first calculation
286
+ fitter.growthRate();
287
+ expect(fitter._cacheValid).toBe(true);
288
+
289
+ // Adding new data should invalidate cache
290
+ fitter.add(2, 8); // This changes the exponential curve
291
+ expect(fitter._cacheValid).toBe(false);
292
+
293
+ // New calculation should work with updated data
294
+ const newGrowthRate = fitter.growthRate();
295
+ expect(newGrowthRate).toBeGreaterThan(Math.log(2)); // Should be higher with [0,1], [1,2], [2,8]
296
+ expect(fitter._cacheValid).toBe(true);
297
+ });
298
+
299
+ test('should handle multiple method calls efficiently with caching', () => {
300
+ const fitter = new ExponentialFitter();
301
+ fitter.add(0, 2);
302
+ fitter.add(1, 4);
303
+ fitter.add(2, 8); // y = 2 * 2^x
304
+
305
+ // Simulate multiple calls like in trendline.js
306
+ const coefficient1 = fitter.coefficient();
307
+ const growthRate1 = fitter.growthRate();
308
+ const f1 = fitter.f(3); // This calls both coefficient() and growthRate()
309
+ const coefficient2 = fitter.coefficient(); // Another direct call
310
+ const correlation1 = fitter.correlation();
311
+
312
+ expect(coefficient1).toBeCloseTo(2, 3);
313
+ expect(coefficient2).toBeCloseTo(2, 3);
314
+ expect(growthRate1).toBeCloseTo(Math.log(2), 3);
315
+ expect(f1).toBeCloseTo(16, 3); // 2 * 2^3 = 16
316
+ expect(correlation1).toBeGreaterThan(0.99);
317
+
318
+ // All should use cached values
319
+ expect(fitter._cachedCoefficient).toBeCloseTo(2, 3);
320
+ expect(fitter._cachedGrowthRate).toBeCloseTo(Math.log(2), 3);
321
+ expect(fitter._cachedCorrelation).toBeGreaterThan(0.99);
322
+ expect(fitter._cacheValid).toBe(true);
323
+ });
324
+
325
+ test('should initialize cache properties correctly', () => {
326
+ const fitter = new ExponentialFitter();
327
+ expect(fitter._cachedGrowthRate).toBe(null);
328
+ expect(fitter._cachedCoefficient).toBe(null);
329
+ expect(fitter._cachedCorrelation).toBe(null);
330
+ expect(fitter._cacheValid).toBe(false);
331
+ });
332
+
333
+ test('should handle caching with invalid data correctly', () => {
334
+ const fitter = new ExponentialFitter();
335
+ fitter.add(0, -1); // Invalid data
336
+
337
+ // Should return default values without caching (early return)
338
+ const growthRate = fitter.growthRate();
339
+ const coefficient = fitter.coefficient();
340
+ const correlation = fitter.correlation();
341
+
342
+ expect(growthRate).toBe(0);
343
+ expect(coefficient).toBe(1);
344
+ expect(correlation).toBe(0);
345
+ expect(fitter._cacheValid).toBe(false); // Cache not set for invalid data
346
+ });
347
+
348
+ test('should handle caching with insufficient data correctly', () => {
349
+ const fitter = new ExponentialFitter();
350
+ fitter.add(0, 1); // Only one point
351
+
352
+ // Should return default values without caching (early return)
353
+ const growthRate = fitter.growthRate();
354
+ const coefficient = fitter.coefficient();
355
+ const correlation = fitter.correlation();
356
+
357
+ expect(growthRate).toBe(0);
358
+ expect(coefficient).toBe(1);
359
+ expect(correlation).toBe(0);
360
+ expect(fitter._cacheValid).toBe(false); // Cache not set for insufficient data
361
+ });
362
+ });
251
363
  });
@@ -10,6 +10,9 @@ export class LineFitter {
10
10
  this.sumxy = 0;
11
11
  this.minx = Number.MAX_VALUE;
12
12
  this.maxx = Number.MIN_VALUE;
13
+ this._cachedSlope = null;
14
+ this._cachedIntercept = null;
15
+ this._cacheValid = false;
13
16
  }
14
17
 
15
18
  /**
@@ -25,6 +28,7 @@ export class LineFitter {
25
28
  if (x < this.minx) this.minx = x;
26
29
  if (x > this.maxx) this.maxx = x;
27
30
  this.count++;
31
+ this._cacheValid = false;
28
32
  }
29
33
 
30
34
  /**
@@ -32,8 +36,10 @@ export class LineFitter {
32
36
  * @returns {number} - The slope of the line.
33
37
  */
34
38
  slope() {
35
- const denominator = this.count * this.sumx2 - this.sumx * this.sumx;
36
- return (this.count * this.sumxy - this.sumx * this.sumy) / denominator;
39
+ if (!this._cacheValid) {
40
+ this._computeCoefficients();
41
+ }
42
+ return this._cachedSlope;
37
43
  }
38
44
 
39
45
  /**
@@ -41,7 +47,10 @@ export class LineFitter {
41
47
  * @returns {number} - The y-intercept of the line.
42
48
  */
43
49
  intercept() {
44
- return (this.sumy - this.slope() * this.sumx) / this.count;
50
+ if (!this._cacheValid) {
51
+ this._computeCoefficients();
52
+ }
53
+ return this._cachedIntercept;
45
54
  }
46
55
 
47
56
  /**
@@ -68,4 +77,11 @@ export class LineFitter {
68
77
  scale() {
69
78
  return this.slope();
70
79
  }
80
+
81
+ _computeCoefficients() {
82
+ const denominator = this.count * this.sumx2 - this.sumx * this.sumx;
83
+ this._cachedSlope = (this.count * this.sumxy - this.sumx * this.sumy) / denominator;
84
+ this._cachedIntercept = (this.sumy - this._cachedSlope * this.sumx) / this.count;
85
+ this._cacheValid = true;
86
+ }
71
87
  }
@@ -262,4 +262,79 @@ describe('LineFitter', () => {
262
262
  expect(fitter3.scale()).toBeNaN();
263
263
  });
264
264
  });
265
+
266
+ describe('caching behavior', () => {
267
+ test('should cache slope and intercept calculations', () => {
268
+ const fitter = new LineFitter();
269
+ fitter.add(1, 1);
270
+ fitter.add(2, 2);
271
+
272
+ // First call should compute and cache
273
+ const slope1 = fitter.slope();
274
+ const intercept1 = fitter.intercept();
275
+
276
+ // Subsequent calls should return cached values
277
+ const slope2 = fitter.slope();
278
+ const intercept2 = fitter.intercept();
279
+
280
+ expect(slope1).toBe(slope2);
281
+ expect(intercept1).toBe(intercept2);
282
+ expect(slope1).toBe(1);
283
+ expect(intercept1).toBe(0);
284
+
285
+ // Check that cache is populated
286
+ expect(fitter._cachedSlope).toBe(1);
287
+ expect(fitter._cachedIntercept).toBe(0);
288
+ expect(fitter._cacheValid).toBe(true);
289
+ });
290
+
291
+ test('should invalidate cache when new data is added', () => {
292
+ const fitter = new LineFitter();
293
+ fitter.add(1, 1);
294
+ fitter.add(2, 2);
295
+
296
+ // Cache should be valid after first calculation
297
+ fitter.slope();
298
+ expect(fitter._cacheValid).toBe(true);
299
+
300
+ // Adding new data should invalidate cache
301
+ fitter.add(3, 4);
302
+ expect(fitter._cacheValid).toBe(false);
303
+
304
+ // New calculation should work with updated data
305
+ const newSlope = fitter.slope();
306
+ expect(newSlope).toBeCloseTo(1.5, 5); // New slope with data [1,1], [2,2], [3,4]
307
+ expect(fitter._cacheValid).toBe(true);
308
+ });
309
+
310
+ test('should handle multiple method calls efficiently with caching', () => {
311
+ const fitter = new LineFitter();
312
+ fitter.add(0, 1);
313
+ fitter.add(1, 3);
314
+ fitter.add(2, 5); // y = 2x + 1
315
+
316
+ // Simulate multiple calls like in trendline.js
317
+ const slope1 = fitter.slope();
318
+ const intercept1 = fitter.intercept(); // This used to call slope() again
319
+ const f1 = fitter.f(3); // This calls both slope() and intercept()
320
+ const slope2 = fitter.slope(); // Another direct call
321
+
322
+ expect(slope1).toBe(2);
323
+ expect(slope2).toBe(2);
324
+ expect(intercept1).toBe(1);
325
+ expect(f1).toBe(7); // 2*3 + 1 = 7
326
+
327
+ // All should use cached values
328
+ expect(fitter._cachedSlope).toBe(2);
329
+ expect(fitter._cachedIntercept).toBe(1);
330
+ expect(fitter._cacheValid).toBe(true);
331
+ });
332
+
333
+ test('should initialize cache properties correctly', () => {
334
+ const fitter = new LineFitter();
335
+ expect(fitter._cachedSlope).toBe(null);
336
+ expect(fitter._cachedIntercept).toBe(null);
337
+ expect(fitter._cacheValid).toBe(false);
338
+ });
339
+ });
265
340
  });