chartjs-plugin-trendline 3.0.0 → 3.0.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 e{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()}}const t={id:"chartjs-plugin-trendline",afterDatasetsDraw:t=>{const i=t.ctx,{xScale:n,yScale:l}=(e=>{let t,i;for(const n of Object.values(e.scales))if(n.isHorizontal()?t=n:i=n,t&&i)break;return{xScale:t,yScale:i}})(t);t.data.datasets.map(((e,t)=>({dataset:e,index:t}))).filter((e=>e.dataset.trendlineLinear)).sort(((e,t)=>{const i=e.dataset.order??0,n=t.dataset.order??0;return 0===i&&0!==n?1:0===n&&0!==i?-1:i-n})).forEach((({dataset:s,index:a})=>{if((s.alwaysShowTrendline||t.isDatasetVisible(a))&&s.data.length>1){((t,i,n,l,s)=>{const a=n.yAxisID||"y",o=t.controller.chart.scales[a]||s,r=n.borderColor||"rgba(169,169,169, .6)",{colorMin:d=r,colorMax:x=r,width:h=n.borderWidth||3,lineStyle:c="solid",fillColor:u=!1}=n.trendlineLinear||{};let f=(n.trendlineLinear||{}).trendoffset||0;const{color:y=r,text:g="Trendline",display:p=!0,displayValue:m=!0,offset:b=10,percentage:F=!1}=n.trendlineLinear.label||{},{family:w="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:L=12}=n.trendlineLinear.label?.font||{},P=t.controller.chart.options,V="object"==typeof P.parsing?P.parsing:void 0,M=n.trendlineLinear?.xAxisKey||V?.xAxisKey||"x",N=n.trendlineLinear?.yAxisKey||V?.yAxisKey||"y";let S=new e;Math.abs(f)>=n.data.length&&(f=0);let C=0;if(f>0){const e=n.data.slice(f).findIndex((e=>null!=e));C=-1!==e?f+e:n.data.length}else{const e=n.data.findIndex((e=>null!=e));C=-1!==e?e:n.data.length}let D,T,v,A,k=C<n.data.length&&"object"==typeof n.data[C];if(n.data.forEach(((e,t)=>{if(null!=e&&!(f>0&&t<C||f<0&&t>=n.data.length+f))if(["time","timeseries"].includes(l.options.type)){let t=null!=e[M]?e[M]:e.t;const i=e[N];null==t||void 0===t||null==i||isNaN(i)||S.add(new Date(t).getTime(),i)}else if(k){const t=e[M],i=e[N],n=null!=t&&!isNaN(t),l=null!=i&&!isNaN(i);n&&l&&S.add(t,i)}else null==e||isNaN(e)||S.add(t,e)})),S.count<2)return;const I=t.controller.chart.chartArea;if(n.trendlineLinear.projection){const e=S.slope(),t=S.intercept();let i=[];if(Math.abs(e)>1e-6){const n=o.getValueForPixel(I.top),l=(n-t)/e;i.push({x:l,y:n});const s=o.getValueForPixel(I.bottom),a=(s-t)/e;i.push({x:a,y:s})}else i.push({x:l.getValueForPixel(I.left),y:t}),i.push({x:l.getValueForPixel(I.right),y:t});const n=l.getValueForPixel(I.left),s=S.f(n);i.push({x:n,y:s});const a=l.getValueForPixel(I.right),r=S.f(a);i.push({x:a,y:r});const d=l.getValueForPixel(I.left),x=l.getValueForPixel(I.right),h=[o.getValueForPixel(I.top),o.getValueForPixel(I.bottom)].filter((e=>isFinite(e))),c=h.length>0?Math.min(...h):-1/0,u=h.length>0?Math.max(...h):1/0;let f=i.filter((e=>isFinite(e.x)&&isFinite(e.y)&&e.x>=d&&e.x<=x&&e.y>=c&&e.y<=u));f=f.filter(((e,t,i)=>t===i.findIndex((t=>Math.abs(t.x-e.x)<1e-4&&Math.abs(t.y-e.y)<1e-4)))),f.length>=2?(f.sort(((e,t)=>e.x-t.x||e.y-t.y)),D=l.getPixelForValue(f[0].x),T=o.getPixelForValue(f[0].y),v=l.getPixelForValue(f[f.length-1].x),A=o.getPixelForValue(f[f.length-1].y)):(D=NaN,T=NaN,v=NaN,A=NaN)}else{const e=S.f(S.minx),t=S.f(S.maxx);D=l.getPixelForValue(S.minx),T=o.getPixelForValue(e),v=l.getPixelForValue(S.maxx),A=o.getPixelForValue(t)}let j=null;if(isFinite(D)&&isFinite(T)&&isFinite(v)&&isFinite(A)&&(j=function(e,t,i,n,l){let s=i-e,a=n-t,o=0,r=1;const d=[-s,s,-a,a],x=[e-l.left,l.right-e,t-l.top,l.bottom-t];for(let e=0;e<4;e++)if(0===d[e]){if(x[e]<0)return null}else{const t=x[e]/d[e];if(d[e]<0){if(t>r)return null;o=Math.max(o,t)}else{if(t<o)return null;r=Math.min(r,t)}}return o>r?null:{x1:e+o*s,y1:t+o*a,x2:e+r*s,y2:t+r*a}}(D,T,v,A,I)),j)if(D=j.x1,T=j.y1,v=j.x2,A=j.y2,Math.abs(D-v)<.5&&Math.abs(T-A)<.5);else{i.lineWidth=h,((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([])}})(i,c),(({ctx:e,x1:t,y1:i,x2:n,y2:l,colorMin:s,colorMax:a})=>{if(isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(l)){e.beginPath(),e.moveTo(t,i),e.lineTo(n,l);try{let o=e.createLinearGradient(t,i,n,l);o.addColorStop(0,s),o.addColorStop(1,a),e.strokeStyle=o}catch(t){console.warn("Gradient creation failed, using solid color:",t),e.strokeStyle=s}e.stroke(),e.closePath()}else console.warn("Cannot draw trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:l})})({ctx:i,x1:D,y1:T,x2:v,y2:A,colorMin:d,colorMax:x}),u&&((e,t,i,n,l,s,a)=>{isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(l)&&isFinite(s)?(e.beginPath(),e.moveTo(t,i),e.lineTo(n,l),e.lineTo(n,s),e.lineTo(t,s),e.lineTo(t,i),e.closePath(),e.fillStyle=a,e.fill()):console.warn("Cannot fill below trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:l,drawBottom:s})})(i,D,T,v,A,I.bottom,u);const e=Math.atan2(A-T,v-D),t=S.slope();n.trendlineLinear.label&&!1!==p&&((e,t,i,n,l,s,a,o,r,d,x)=>{e.font=`${d}px ${r}`,e.fillStyle=o;const h=e.measureText(t).width,c=(i+l)/2,u=(n+s)/2;e.save(),e.translate(c,u),e.rotate(a);const f=-h/2,y=x;e.fillText(t,f,y),e.restore()})(i,m?`${g} (Slope: ${F?(100*t).toFixed(2)+"%":t.toFixed(2)})`:g,D,T,v,A,e,y,w,L,b)}})(t.getDatasetMeta(a),i,s,n,l)}})),i.setLineDash([])},beforeInit:e=>{e.data.datasets.forEach((t=>{if(t.trendlineLinear&&t.trendlineLinear.label){const i=t.trendlineLinear.label,n=e.legend.options.labels.generateLabels;e.legend.options.labels.generateLabels=function(e){const l=n(e),s=t.trendlineLinear.legend;return s&&!1!==s.display&&l.push({text:s.text||i+" (Trendline)",strokeStyle:s.color||t.borderColor||"rgba(169,169,169, .6)",fillStyle:s.fillStyle||"transparent",lineCap:s.lineCap||"butt",lineDash:s.lineDash||[],lineWidth:s.width||1}),l}}}))}};"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(t):window.Chart.plugins.register(t))})();
1
+ (()=>{"use strict";class e{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()}}const t={id:"chartjs-plugin-trendline",afterDatasetsDraw:t=>{const i=t.ctx,{xScale:n,yScale:l}=(e=>{let t,i;for(const n of Object.values(e.scales))if(n.isHorizontal()?t=n:i=n,t&&i)break;return{xScale:t,yScale:i}})(t);t.data.datasets.map(((e,t)=>({dataset:e,index:t}))).filter((e=>e.dataset.trendlineLinear)).sort(((e,t)=>{const i=e.dataset.order??0,n=t.dataset.order??0;return 0===i&&0!==n?1:0===n&&0!==i?-1:i-n})).forEach((({dataset:s,index:a})=>{if((s.alwaysShowTrendline||t.isDatasetVisible(a))&&s.data.length>1){((t,i,n,l,s)=>{const a=n.yAxisID||"y",o=t.controller.chart.scales[a]||s,r=n.borderColor||"rgba(169,169,169, .6)",{colorMin:d=r,colorMax:c=r,width:x=n.borderWidth||3,lineStyle:h="solid",fillColor:u=!1}=n.trendlineLinear||{};let f=(n.trendlineLinear||{}).trendoffset||0;const{color:y=r,text:g="Trendline",display:p=!0,displayValue:m=!0,offset:b=10,percentage:F=!1}=n.trendlineLinear&&n.trendlineLinear.label||{},{family:w="'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:L=12}=n.trendlineLinear&&n.trendlineLinear.label&&n.trendlineLinear.label.font||{},N=t.controller.chart.options,P="object"==typeof N.parsing?N.parsing:void 0,V=n.trendlineLinear?.xAxisKey||P?.xAxisKey||"x",M=n.trendlineLinear?.yAxisKey||P?.yAxisKey||"y";let S=new e;Math.abs(f)>=n.data.length&&(f=0);let D=0;if(f>0){const e=n.data.slice(f).findIndex((e=>null!=e));D=-1!==e?f+e:n.data.length}else{const e=n.data.findIndex((e=>null!=e));D=-1!==e?e:n.data.length}let T,C,v,A,k=D<n.data.length&&"object"==typeof n.data[D];if(n.data.forEach(((e,i)=>{if(null!=e&&!(f>0&&i<D||f<0&&i>=n.data.length+f))if(["time","timeseries"].includes(l.options.type)&&k){let t=null!=e[V]?e[V]:e.t;const i=e[M];null==t||void 0===t||null==i||isNaN(i)||S.add(new Date(t).getTime(),i)}else if(k){const t=e[V],i=e[M],n=null!=t&&!isNaN(t),l=null!=i&&!isNaN(i);n&&l&&S.add(t,i)}else if(["time","timeseries"].includes(l.options.type)&&!k){const n=t.controller.chart.data.labels;if(n&&n[i]&&null!=e&&!isNaN(e)){const t=new Date(n[i]).getTime();isNaN(t)||S.add(t,e)}}else null==e||isNaN(e)||S.add(i,e)})),S.count<2)return;const I=t.controller.chart.chartArea;if(n.trendlineLinear.projection){const e=S.slope(),t=S.intercept();let i=[];if(Math.abs(e)>1e-6){const n=o.getValueForPixel(I.top),l=(n-t)/e;i.push({x:l,y:n});const s=o.getValueForPixel(I.bottom),a=(s-t)/e;i.push({x:a,y:s})}else i.push({x:l.getValueForPixel(I.left),y:t}),i.push({x:l.getValueForPixel(I.right),y:t});const n=l.getValueForPixel(I.left),s=S.f(n);i.push({x:n,y:s});const a=l.getValueForPixel(I.right),r=S.f(a);i.push({x:a,y:r});const d=l.getValueForPixel(I.left),c=l.getValueForPixel(I.right),x=[o.getValueForPixel(I.top),o.getValueForPixel(I.bottom)].filter((e=>isFinite(e))),h=x.length>0?Math.min(...x):-1/0,u=x.length>0?Math.max(...x):1/0;let f=i.filter((e=>isFinite(e.x)&&isFinite(e.y)&&e.x>=d&&e.x<=c&&e.y>=h&&e.y<=u));f=f.filter(((e,t,i)=>t===i.findIndex((t=>Math.abs(t.x-e.x)<1e-4&&Math.abs(t.y-e.y)<1e-4)))),f.length>=2?(f.sort(((e,t)=>e.x-t.x||e.y-t.y)),T=l.getPixelForValue(f[0].x),C=o.getPixelForValue(f[0].y),v=l.getPixelForValue(f[f.length-1].x),A=o.getPixelForValue(f[f.length-1].y)):(T=NaN,C=NaN,v=NaN,A=NaN)}else{const e=S.f(S.minx),t=S.f(S.maxx);T=l.getPixelForValue(S.minx),C=o.getPixelForValue(e),v=l.getPixelForValue(S.maxx),A=o.getPixelForValue(t)}let j=null;if(isFinite(T)&&isFinite(C)&&isFinite(v)&&isFinite(A)&&(j=function(e,t,i,n,l){let s=i-e,a=n-t,o=0,r=1;const d=[-s,s,-a,a],c=[e-l.left,l.right-e,t-l.top,l.bottom-t];for(let e=0;e<4;e++)if(0===d[e]){if(c[e]<0)return null}else{const t=c[e]/d[e];if(d[e]<0){if(t>r)return null;o=Math.max(o,t)}else{if(t<o)return null;r=Math.min(r,t)}}return o>r?null:{x1:e+o*s,y1:t+o*a,x2:e+r*s,y2:t+r*a}}(T,C,v,A,I)),j)if(T=j.x1,C=j.y1,v=j.x2,A=j.y2,Math.abs(T-v)<.5&&Math.abs(C-A)<.5);else{i.lineWidth=x,((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([])}})(i,h),(({ctx:e,x1:t,y1:i,x2:n,y2:l,colorMin:s,colorMax:a})=>{if(isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(l)){e.beginPath(),e.moveTo(t,i),e.lineTo(n,l);try{let o=e.createLinearGradient(t,i,n,l);o.addColorStop(0,s),o.addColorStop(1,a),e.strokeStyle=o}catch(t){console.warn("Gradient creation failed, using solid color:",t),e.strokeStyle=s}e.stroke(),e.closePath()}else console.warn("Cannot draw trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:l})})({ctx:i,x1:T,y1:C,x2:v,y2:A,colorMin:d,colorMax:c}),u&&((e,t,i,n,l,s,a)=>{isFinite(t)&&isFinite(i)&&isFinite(n)&&isFinite(l)&&isFinite(s)?(e.beginPath(),e.moveTo(t,i),e.lineTo(n,l),e.lineTo(n,s),e.lineTo(t,s),e.lineTo(t,i),e.closePath(),e.fillStyle=a,e.fill()):console.warn("Cannot fill below trendline: coordinates contain non-finite values",{x1:t,y1:i,x2:n,y2:l,drawBottom:s})})(i,T,C,v,A,I.bottom,u);const e=Math.atan2(A-C,v-T),t=S.slope();n.trendlineLinear.label&&!1!==p&&((e,t,i,n,l,s,a,o,r,d,c)=>{e.font=`${d}px ${r}`,e.fillStyle=o;const x=e.measureText(t).width,h=(i+l)/2,u=(n+s)/2;e.save(),e.translate(h,u),e.rotate(a);const f=-x/2,y=c;e.fillText(t,f,y),e.restore()})(i,m?`${g} (Slope: ${F?(100*t).toFixed(2)+"%":t.toFixed(2)})`:g,T,C,v,A,e,y,w,L,b)}})(t.getDatasetMeta(a),i,s,n,l)}})),i.setLineDash([])},beforeInit:e=>{e.data.datasets.forEach((t=>{if(t.trendlineLinear&&t.trendlineLinear.label){const i=t.trendlineLinear.label,n=e.legend.options.labels.generateLabels;e.legend.options.labels.generateLabels=function(e){const l=n(e),s=t.trendlineLinear.legend;return s&&!1!==s.display&&l.push({text:s.text||i+" (Trendline)",strokeStyle:s.color||t.borderColor||"rgba(169,169,169, .6)",fillStyle:s.fillStyle||"transparent",lineCap:s.lineCap||"butt",lineDash:s.lineDash||[],lineWidth:s.width||1}),l}}}))}};"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(t):window.Chart.plugins.register(t))})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-plugin-trendline",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Trendline for Chart.js",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -27,4 +27,4 @@
27
27
  "webpack": "^5.92.1",
28
28
  "webpack-cli": "^5.1.4"
29
29
  }
30
- }
30
+ }
@@ -32,12 +32,12 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
32
32
  displayValue = true,
33
33
  offset = 10,
34
34
  percentage = false,
35
- } = dataset.trendlineLinear.label || {};
35
+ } = (dataset.trendlineLinear && dataset.trendlineLinear.label) || {};
36
36
 
37
37
  const {
38
38
  family = "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
39
39
  size = 12,
40
- } = dataset.trendlineLinear.label?.font || {};
40
+ } = (dataset.trendlineLinear && dataset.trendlineLinear.label && dataset.trendlineLinear.label.font) || {};
41
41
 
42
42
  const chartOptions = datasetMeta.controller.chart.options;
43
43
  const parsingOptions =
@@ -101,8 +101,8 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
101
101
  if (trendoffset < 0 && index >= dataset.data.length + trendoffset) return;
102
102
 
103
103
  // Process data based on scale type and data structure.
104
- if (['time', 'timeseries'].includes(xScale.options.type)) {
105
- // For time-based scales, convert x to a numerical timestamp; ensure y is a valid number.
104
+ if (['time', 'timeseries'].includes(xScale.options.type) && xy) {
105
+ // For time-based scales with object data, convert x to a numerical timestamp; ensure y is a valid number.
106
106
  let x = data[xAxisKey] != null ? data[xAxisKey] : data.t; // `data.t` is a Chart.js internal fallback for time data.
107
107
  const yValue = data[yAxisKey];
108
108
 
@@ -123,6 +123,15 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
123
123
  fitter.add(xVal, yVal);
124
124
  }
125
125
  // If either xVal or yVal is invalid, the point is skipped. No fallback to using index.
126
+ } else if (['time', 'timeseries'].includes(xScale.options.type) && !xy) {
127
+ // For time-based scales with array of numbers, get the x-value from the chart labels
128
+ const chartLabels = datasetMeta.controller.chart.data.labels;
129
+ if (chartLabels && chartLabels[index] && data != null && !isNaN(data)) {
130
+ const timeValue = new Date(chartLabels[index]).getTime();
131
+ if (!isNaN(timeValue)) {
132
+ fitter.add(timeValue, data);
133
+ }
134
+ }
126
135
  } else {
127
136
  // Data is an array of numbers (or other non-object types).
128
137
  // The 'data' variable itself is the y-value, and 'index' is the x-value.
@@ -50,6 +50,7 @@ describe('addFitter', () => {
50
50
  scales: { 'y': { getPixelForValue: jest.fn(val => val * 10), getValueForPixel: jest.fn(pixel => pixel / 10) } },
51
51
  options: { parsing: { xAxisKey: 'x', yAxisKey: 'y' } },
52
52
  chartArea: { top: 50, bottom: 450, left: 50, right: 750, width: 700, height: 400 },
53
+ data: { labels: [] }
53
54
  }
54
55
  },
55
56
  data: [{x:0, y:0}]
@@ -353,4 +354,132 @@ describe('addFitter', () => {
353
354
  expect(drawingUtils.drawTrendline).not.toHaveBeenCalled();
354
355
  expect(labelUtils.addTrendlineLabel).not.toHaveBeenCalled();
355
356
  });
357
+
358
+ test('Configuration without label property (issue #118)', () => {
359
+ // Test case for user's exact configuration that was failing
360
+ mockDataset.trendlineLinear = {
361
+ lineStyle: 'dotted',
362
+ width: 2
363
+ };
364
+ mockDataset.data = [10, 20, 30, 40, 50];
365
+ mockDatasetMeta.data = [10, 20, 30, 40, 50];
366
+ mockLineFitterInstance.minx = 0;
367
+ mockLineFitterInstance.maxx = 4;
368
+ mockLineFitterInstance.count = 5;
369
+ mockLineFitterInstance.f = jest.fn(x => {
370
+ if (x === 0) return 100;
371
+ if (x === 4) return 200;
372
+ return x * 25 + 100;
373
+ });
374
+ mockXScale.getPixelForValue = jest.fn(val => {
375
+ if (val === 0) return 100;
376
+ if (val === 4) return 400;
377
+ return val * 100 + 100;
378
+ });
379
+ mockDatasetMeta.controller.chart.scales.y.getPixelForValue = jest.fn(val => {
380
+ if (val === 100) return 200;
381
+ if (val === 200) return 300;
382
+ return val * 1 + 100;
383
+ });
384
+
385
+ addFitter(mockDatasetMeta, mockCtx, mockDataset, mockXScale, mockYScale);
386
+
387
+ expect(mockLineFitterInstance.add).toHaveBeenCalledTimes(5);
388
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(0, 10);
389
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(1, 20);
390
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(2, 30);
391
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(3, 40);
392
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(4, 50);
393
+ expect(drawingUtils.setLineStyle).toHaveBeenCalledWith(mockCtx, 'dotted');
394
+ expect(drawingUtils.drawTrendline).toHaveBeenCalled();
395
+ expect(labelUtils.addTrendlineLabel).not.toHaveBeenCalled(); // No label should be added
396
+ });
397
+
398
+ test('Configuration without label property but with other properties', () => {
399
+ // Test minimal configuration similar to issue #118
400
+ mockDataset.trendlineLinear = {
401
+ colorMin: 'red',
402
+ width: 3,
403
+ lineStyle: 'dashed'
404
+ };
405
+ mockDataset.data = [{ x: 10, y: 30 }, { x: 20, y: 50 }];
406
+ mockDatasetMeta.data = [{ x: 10, y: 30 }];
407
+ mockLineFitterInstance.minx = 10;
408
+ mockLineFitterInstance.maxx = 20;
409
+ mockLineFitterInstance.count = 2;
410
+ mockLineFitterInstance.f = jest.fn(x => {
411
+ if (x === 10) return 30;
412
+ if (x === 20) return 50;
413
+ return x * 2 + 10;
414
+ });
415
+ mockXScale.getPixelForValue = jest.fn(val => {
416
+ if (val === 10) return 100;
417
+ if (val === 20) return 200;
418
+ return val * 10;
419
+ });
420
+ mockDatasetMeta.controller.chart.scales.y.getPixelForValue = jest.fn(val => {
421
+ if (val === 30) return 200;
422
+ if (val === 50) return 300;
423
+ return val * 10;
424
+ });
425
+
426
+ addFitter(mockDatasetMeta, mockCtx, mockDataset, mockXScale, mockYScale);
427
+
428
+ expect(mockLineFitterInstance.add).toHaveBeenCalledTimes(2);
429
+ expect(drawingUtils.setLineStyle).toHaveBeenCalledWith(mockCtx, 'dashed');
430
+ expect(drawingUtils.drawTrendline).toHaveBeenCalled();
431
+ expect(labelUtils.addTrendlineLabel).not.toHaveBeenCalled(); // No label should be added
432
+ });
433
+
434
+ test('Time scale with array of numbers and no label property (issue #118)', () => {
435
+ // Test case for time scale with array data (like lineChartTypeTime.html)
436
+ mockXScale.options.type = 'time';
437
+ mockDataset.trendlineLinear = {
438
+ lineStyle: 'dotted',
439
+ width: 2
440
+ };
441
+ mockDataset.data = [75, 64, 52, 23, 44]; // Just numbers like in the example
442
+ mockDatasetMeta.data = [75, 64, 52, 23, 44];
443
+ mockDatasetMeta.controller.chart.data.labels = [
444
+ '2025-03-01T00:00:00',
445
+ '2025-03-02T00:00:00',
446
+ '2025-03-03T00:00:00',
447
+ '2025-03-04T00:00:00',
448
+ '2025-03-05T00:00:00'
449
+ ];
450
+
451
+ const date1 = new Date('2025-03-01T00:00:00').getTime();
452
+ const date5 = new Date('2025-03-05T00:00:00').getTime();
453
+
454
+ mockLineFitterInstance.minx = date1;
455
+ mockLineFitterInstance.maxx = date5;
456
+ mockLineFitterInstance.count = 5;
457
+ mockLineFitterInstance.f = jest.fn(x => {
458
+ if (x === date1) return 75;
459
+ if (x === date5) return 44;
460
+ return 50; // approximation
461
+ });
462
+ mockXScale.getPixelForValue = jest.fn(val => {
463
+ if (val === date1) return 100;
464
+ if (val === date5) return 300;
465
+ return 200;
466
+ });
467
+ mockDatasetMeta.controller.chart.scales.y.getPixelForValue = jest.fn(val => {
468
+ if (val === 75) return 200;
469
+ if (val === 44) return 250;
470
+ return 225;
471
+ });
472
+
473
+ addFitter(mockDatasetMeta, mockCtx, mockDataset, mockXScale, mockYScale);
474
+
475
+ expect(mockLineFitterInstance.add).toHaveBeenCalledTimes(5);
476
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(date1, 75);
477
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(new Date('2025-03-02T00:00:00').getTime(), 64);
478
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(new Date('2025-03-03T00:00:00').getTime(), 52);
479
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(new Date('2025-03-04T00:00:00').getTime(), 23);
480
+ expect(mockLineFitterInstance.add).toHaveBeenCalledWith(new Date('2025-03-05T00:00:00').getTime(), 44);
481
+ expect(drawingUtils.setLineStyle).toHaveBeenCalledWith(mockCtx, 'dotted');
482
+ expect(drawingUtils.drawTrendline).toHaveBeenCalled();
483
+ expect(labelUtils.addTrendlineLabel).not.toHaveBeenCalled(); // No label should be added
484
+ });
356
485
  });