chartjs-plugin-trendline 3.2.0 → 3.2.3
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/.github/copilot-instructions.md +40 -40
- package/.github/workflows/release.yml +64 -61
- package/.github/workflows/tests.yml +26 -26
- package/.prettierrc +5 -5
- package/CLAUDE.md +44 -44
- package/GEMINI.md +40 -40
- package/LICENSE +21 -21
- package/MIGRATION.md +126 -126
- package/README.md +166 -166
- package/babel.config.js +3 -3
- package/changelog.md +39 -39
- package/dist/chartjs-plugin-trendline.cjs +884 -885
- package/dist/chartjs-plugin-trendline.esm.js +882 -883
- package/dist/chartjs-plugin-trendline.js +890 -891
- package/dist/chartjs-plugin-trendline.min.js +8 -8
- package/dist/chartjs-plugin-trendline.min.js.map +1 -1
- package/example/barChart.html +165 -165
- package/example/barChartWithNullValues.html +168 -168
- package/example/barChart_label.html +174 -174
- package/example/exponentialChart.html +244 -244
- package/example/lineChart.html +210 -210
- package/example/lineChartProjection.html +261 -261
- package/example/lineChartTypeTime.html +190 -190
- package/example/scatterChart.html +136 -136
- package/example/scatterProjection.html +141 -141
- package/example/test-null-handling.html +59 -59
- package/index.html +215 -215
- package/jest.config.js +4 -4
- package/package.json +45 -40
- package/rollup.config.js +54 -54
- package/src/components/label.js +56 -56
- package/src/components/label.test.js +129 -129
- package/src/components/trendline.js +375 -375
- package/src/components/trendline.test.js +789 -789
- package/src/core/plugin.js +78 -79
- package/src/core/plugin.test.js +307 -0
- package/src/index.js +12 -12
- package/src/utils/drawing.js +125 -125
- package/src/utils/drawing.test.js +308 -308
- package/src/utils/exponentialFitter.js +146 -146
- package/src/utils/exponentialFitter.test.js +362 -362
- package/src/utils/lineFitter.js +86 -86
- package/src/utils/lineFitter.test.js +340 -340
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A class that fits an exponential curve to a series of points using least squares.
|
|
3
|
-
* Fits y = a * e^(b*x) by transforming to ln(y) = ln(a) + b*x
|
|
4
|
-
*/
|
|
5
|
-
export class ExponentialFitter {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.count = 0;
|
|
8
|
-
this.sumx = 0;
|
|
9
|
-
this.sumlny = 0;
|
|
10
|
-
this.sumx2 = 0;
|
|
11
|
-
this.sumxlny = 0;
|
|
12
|
-
this.minx = Number.MAX_VALUE;
|
|
13
|
-
this.maxx = Number.MIN_VALUE;
|
|
14
|
-
this.hasValidData = true;
|
|
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;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Adds a point to the exponential fitter.
|
|
24
|
-
* @param {number} x - The x-coordinate of the point.
|
|
25
|
-
* @param {number} y - The y-coordinate of the point.
|
|
26
|
-
*/
|
|
27
|
-
add(x, y) {
|
|
28
|
-
if (y <= 0) {
|
|
29
|
-
this.hasValidData = false;
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const lny = Math.log(y);
|
|
34
|
-
if (!isFinite(lny)) {
|
|
35
|
-
this.hasValidData = false;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
this.sumx += x;
|
|
40
|
-
this.sumlny += lny;
|
|
41
|
-
this.sumx2 += x * x;
|
|
42
|
-
this.sumxlny += x * lny;
|
|
43
|
-
if (x < this.minx) this.minx = x;
|
|
44
|
-
if (x > this.maxx) this.maxx = x;
|
|
45
|
-
this.dataPoints.push({x, y, lny}); // Store actual data points
|
|
46
|
-
this.count++;
|
|
47
|
-
this._cacheValid = false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Calculates the exponential growth rate (b in y = a * e^(b*x)).
|
|
52
|
-
* @returns {number} - The exponential growth rate.
|
|
53
|
-
*/
|
|
54
|
-
growthRate() {
|
|
55
|
-
if (!this.hasValidData || this.count < 2) return 0;
|
|
56
|
-
if (!this._cacheValid) {
|
|
57
|
-
this._computeCoefficients();
|
|
58
|
-
}
|
|
59
|
-
return this._cachedGrowthRate;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Calculates the exponential coefficient (a in y = a * e^(b*x)).
|
|
64
|
-
* @returns {number} - The exponential coefficient.
|
|
65
|
-
*/
|
|
66
|
-
coefficient() {
|
|
67
|
-
if (!this.hasValidData || this.count < 2) return 1;
|
|
68
|
-
if (!this._cacheValid) {
|
|
69
|
-
this._computeCoefficients();
|
|
70
|
-
}
|
|
71
|
-
return this._cachedCoefficient;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Returns the fitted exponential value (y) for a given x.
|
|
76
|
-
* @param {number} x - The x-coordinate.
|
|
77
|
-
* @returns {number} - The corresponding y-coordinate on the fitted exponential curve.
|
|
78
|
-
*/
|
|
79
|
-
f(x) {
|
|
80
|
-
if (!this.hasValidData || this.count < 2) return 0;
|
|
81
|
-
if (!this._cacheValid) {
|
|
82
|
-
this._computeCoefficients();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Check for potential overflow before calculation
|
|
86
|
-
if (Math.abs(this._cachedGrowthRate * x) > 500) return 0; // Safer limit to prevent overflow
|
|
87
|
-
|
|
88
|
-
const result = this._cachedCoefficient * Math.exp(this._cachedGrowthRate * x);
|
|
89
|
-
return isFinite(result) ? result : 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Calculates the correlation coefficient (R-squared) for the exponential fit.
|
|
94
|
-
* @returns {number} - The correlation coefficient (0-1).
|
|
95
|
-
*/
|
|
96
|
-
correlation() {
|
|
97
|
-
if (!this.hasValidData || this.count < 2) return 0;
|
|
98
|
-
if (!this._cacheValid) {
|
|
99
|
-
this._computeCoefficients();
|
|
100
|
-
}
|
|
101
|
-
return this._cachedCorrelation;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Returns the scale (growth rate) of the fitted exponential curve.
|
|
106
|
-
* @returns {number} - The growth rate of the exponential curve.
|
|
107
|
-
*/
|
|
108
|
-
scale() {
|
|
109
|
-
return this.growthRate();
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* A class that fits an exponential curve to a series of points using least squares.
|
|
3
|
+
* Fits y = a * e^(b*x) by transforming to ln(y) = ln(a) + b*x
|
|
4
|
+
*/
|
|
5
|
+
export class ExponentialFitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.count = 0;
|
|
8
|
+
this.sumx = 0;
|
|
9
|
+
this.sumlny = 0;
|
|
10
|
+
this.sumx2 = 0;
|
|
11
|
+
this.sumxlny = 0;
|
|
12
|
+
this.minx = Number.MAX_VALUE;
|
|
13
|
+
this.maxx = Number.MIN_VALUE;
|
|
14
|
+
this.hasValidData = true;
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Adds a point to the exponential fitter.
|
|
24
|
+
* @param {number} x - The x-coordinate of the point.
|
|
25
|
+
* @param {number} y - The y-coordinate of the point.
|
|
26
|
+
*/
|
|
27
|
+
add(x, y) {
|
|
28
|
+
if (y <= 0) {
|
|
29
|
+
this.hasValidData = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lny = Math.log(y);
|
|
34
|
+
if (!isFinite(lny)) {
|
|
35
|
+
this.hasValidData = false;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.sumx += x;
|
|
40
|
+
this.sumlny += lny;
|
|
41
|
+
this.sumx2 += x * x;
|
|
42
|
+
this.sumxlny += x * lny;
|
|
43
|
+
if (x < this.minx) this.minx = x;
|
|
44
|
+
if (x > this.maxx) this.maxx = x;
|
|
45
|
+
this.dataPoints.push({x, y, lny}); // Store actual data points
|
|
46
|
+
this.count++;
|
|
47
|
+
this._cacheValid = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculates the exponential growth rate (b in y = a * e^(b*x)).
|
|
52
|
+
* @returns {number} - The exponential growth rate.
|
|
53
|
+
*/
|
|
54
|
+
growthRate() {
|
|
55
|
+
if (!this.hasValidData || this.count < 2) return 0;
|
|
56
|
+
if (!this._cacheValid) {
|
|
57
|
+
this._computeCoefficients();
|
|
58
|
+
}
|
|
59
|
+
return this._cachedGrowthRate;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculates the exponential coefficient (a in y = a * e^(b*x)).
|
|
64
|
+
* @returns {number} - The exponential coefficient.
|
|
65
|
+
*/
|
|
66
|
+
coefficient() {
|
|
67
|
+
if (!this.hasValidData || this.count < 2) return 1;
|
|
68
|
+
if (!this._cacheValid) {
|
|
69
|
+
this._computeCoefficients();
|
|
70
|
+
}
|
|
71
|
+
return this._cachedCoefficient;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the fitted exponential value (y) for a given x.
|
|
76
|
+
* @param {number} x - The x-coordinate.
|
|
77
|
+
* @returns {number} - The corresponding y-coordinate on the fitted exponential curve.
|
|
78
|
+
*/
|
|
79
|
+
f(x) {
|
|
80
|
+
if (!this.hasValidData || this.count < 2) return 0;
|
|
81
|
+
if (!this._cacheValid) {
|
|
82
|
+
this._computeCoefficients();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for potential overflow before calculation
|
|
86
|
+
if (Math.abs(this._cachedGrowthRate * x) > 500) return 0; // Safer limit to prevent overflow
|
|
87
|
+
|
|
88
|
+
const result = this._cachedCoefficient * Math.exp(this._cachedGrowthRate * x);
|
|
89
|
+
return isFinite(result) ? result : 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculates the correlation coefficient (R-squared) for the exponential fit.
|
|
94
|
+
* @returns {number} - The correlation coefficient (0-1).
|
|
95
|
+
*/
|
|
96
|
+
correlation() {
|
|
97
|
+
if (!this.hasValidData || this.count < 2) return 0;
|
|
98
|
+
if (!this._cacheValid) {
|
|
99
|
+
this._computeCoefficients();
|
|
100
|
+
}
|
|
101
|
+
return this._cachedCorrelation;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the scale (growth rate) of the fitted exponential curve.
|
|
106
|
+
* @returns {number} - The growth rate of the exponential curve.
|
|
107
|
+
*/
|
|
108
|
+
scale() {
|
|
109
|
+
return this.growthRate();
|
|
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
|
+
}
|
|
147
147
|
}
|