chartjs-plugin-trendline 3.0.2 → 3.1.0
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 +63 -2
- package/changelog.md +13 -0
- package/dist/chartjs-plugin-trendline.min.js +1 -1
- package/example/barChart.html +135 -23
- package/example/barChartWithNullValues.html +139 -24
- package/example/barChart_label.html +137 -23
- package/example/exponentialChart.html +245 -0
- package/example/lineChart.html +116 -16
- package/example/lineChartProjection.html +87 -17
- package/example/lineChartTypeTime.html +112 -23
- package/example/scatterChart.html +78 -11
- package/example/scatterProjection.html +88 -20
- package/example/test-null-handling.html +60 -0
- package/index.html +216 -0
- package/package.json +1 -1
- package/src/components/trendline.js +66 -41
- package/src/components/trendline.test.js +304 -0
- package/src/core/plugin.js +5 -4
- package/src/utils/drawing.js +15 -4
- package/src/utils/drawing.test.js +51 -0
- package/src/utils/exponentialFitter.js +114 -0
- package/src/utils/exponentialFitter.test.js +251 -0
package/index.html
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
<title>Chart.js Plugin Trendline - Examples</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
background-color: #f8f9fa;
|
|
13
|
+
color: #333;
|
|
14
|
+
}
|
|
15
|
+
.container {
|
|
16
|
+
max-width: 1200px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
}
|
|
19
|
+
h1 {
|
|
20
|
+
text-align: center;
|
|
21
|
+
color: #2c3e50;
|
|
22
|
+
margin-bottom: 10px;
|
|
23
|
+
}
|
|
24
|
+
.subtitle {
|
|
25
|
+
text-align: center;
|
|
26
|
+
color: #7f8c8d;
|
|
27
|
+
margin-bottom: 40px;
|
|
28
|
+
}
|
|
29
|
+
.examples-grid {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
32
|
+
gap: 30px;
|
|
33
|
+
margin-bottom: 40px;
|
|
34
|
+
}
|
|
35
|
+
.example-card {
|
|
36
|
+
background: white;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
transition: transform 0.2s;
|
|
41
|
+
}
|
|
42
|
+
.example-card:hover {
|
|
43
|
+
transform: translateY(-2px);
|
|
44
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
|
45
|
+
}
|
|
46
|
+
.example-header {
|
|
47
|
+
padding: 20px;
|
|
48
|
+
background: #34495e;
|
|
49
|
+
color: white;
|
|
50
|
+
}
|
|
51
|
+
.example-title {
|
|
52
|
+
margin: 0;
|
|
53
|
+
font-size: 18px;
|
|
54
|
+
}
|
|
55
|
+
.example-description {
|
|
56
|
+
margin: 5px 0 0 0;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
opacity: 0.9;
|
|
59
|
+
}
|
|
60
|
+
.example-preview {
|
|
61
|
+
padding: 20px;
|
|
62
|
+
text-align: center;
|
|
63
|
+
}
|
|
64
|
+
.example-link {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
padding: 10px 20px;
|
|
67
|
+
background: #3498db;
|
|
68
|
+
color: white;
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
transition: background-color 0.2s;
|
|
73
|
+
}
|
|
74
|
+
.example-link:hover {
|
|
75
|
+
background: #2980b9;
|
|
76
|
+
}
|
|
77
|
+
.footer {
|
|
78
|
+
text-align: center;
|
|
79
|
+
padding: 40px 0;
|
|
80
|
+
border-top: 1px solid #ecf0f1;
|
|
81
|
+
margin-top: 40px;
|
|
82
|
+
}
|
|
83
|
+
.footer a {
|
|
84
|
+
color: #3498db;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
}
|
|
87
|
+
.footer a:hover {
|
|
88
|
+
text-decoration: underline;
|
|
89
|
+
}
|
|
90
|
+
.install-section {
|
|
91
|
+
background: white;
|
|
92
|
+
padding: 30px;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
margin-bottom: 40px;
|
|
95
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
96
|
+
}
|
|
97
|
+
.install-section h2 {
|
|
98
|
+
margin-top: 0;
|
|
99
|
+
color: #2c3e50;
|
|
100
|
+
}
|
|
101
|
+
pre {
|
|
102
|
+
background: #f8f9fa;
|
|
103
|
+
padding: 15px;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
border-left: 4px solid #3498db;
|
|
107
|
+
}
|
|
108
|
+
code {
|
|
109
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<div class="container">
|
|
115
|
+
<h1>Chart.js Plugin Trendline</h1>
|
|
116
|
+
<p class="subtitle">Interactive examples showcasing linear trendline functionality for Chart.js</p>
|
|
117
|
+
|
|
118
|
+
<div class="install-section">
|
|
119
|
+
<h2>Quick Start</h2>
|
|
120
|
+
<p>Add the plugin to your HTML page:</p>
|
|
121
|
+
<pre><code><script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.min.js"></script>
|
|
122
|
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-trendline/dist/chartjs-plugin-trendline.min.js"></script></code></pre>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="examples-grid">
|
|
126
|
+
<div class="example-card">
|
|
127
|
+
<div class="example-header">
|
|
128
|
+
<h3 class="example-title">Bar Chart</h3>
|
|
129
|
+
<p class="example-description">Basic bar chart with dotted trendline</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="example-preview">
|
|
132
|
+
<a href="example/barChart.html" class="example-link">View Example</a>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="example-card">
|
|
137
|
+
<div class="example-header">
|
|
138
|
+
<h3 class="example-title">Bar Chart with Label</h3>
|
|
139
|
+
<p class="example-description">Bar chart displaying trendline with custom label</p>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="example-preview">
|
|
142
|
+
<a href="example/barChart_label.html" class="example-link">View Example</a>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="example-card">
|
|
147
|
+
<div class="example-header">
|
|
148
|
+
<h3 class="example-title">Bar Chart with Null Values</h3>
|
|
149
|
+
<p class="example-description">Handling datasets with missing data points</p>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="example-preview">
|
|
152
|
+
<a href="example/barChartWithNullValues.html" class="example-link">View Example</a>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="example-card">
|
|
157
|
+
<div class="example-header">
|
|
158
|
+
<h3 class="example-title">Line Chart</h3>
|
|
159
|
+
<p class="example-description">Standard line chart with linear trendline</p>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="example-preview">
|
|
162
|
+
<a href="example/lineChart.html" class="example-link">View Example</a>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="example-card">
|
|
167
|
+
<div class="example-header">
|
|
168
|
+
<h3 class="example-title">Line Chart Projection</h3>
|
|
169
|
+
<p class="example-description">Line chart with trendline projection beyond data</p>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="example-preview">
|
|
172
|
+
<a href="example/lineChartProjection.html" class="example-link">View Example</a>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="example-card">
|
|
177
|
+
<div class="example-header">
|
|
178
|
+
<h3 class="example-title">Time Series Line Chart</h3>
|
|
179
|
+
<p class="example-description">Line chart with time-based x-axis</p>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="example-preview">
|
|
182
|
+
<a href="example/lineChartTypeTime.html" class="example-link">View Example</a>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="example-card">
|
|
187
|
+
<div class="example-header">
|
|
188
|
+
<h3 class="example-title">Scatter Chart</h3>
|
|
189
|
+
<p class="example-description">Scatter plot with fitted trendline</p>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="example-preview">
|
|
192
|
+
<a href="example/scatterChart.html" class="example-link">View Example</a>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="example-card">
|
|
197
|
+
<div class="example-header">
|
|
198
|
+
<h3 class="example-title">Scatter Projection</h3>
|
|
199
|
+
<p class="example-description">Scatter chart with extended trendline projection</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="example-preview">
|
|
202
|
+
<a href="example/scatterProjection.html" class="example-link">View Example</a>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="footer">
|
|
208
|
+
<p>
|
|
209
|
+
<a href="https://github.com/Makanz/chartjs-plugin-trendline">View on GitHub</a> |
|
|
210
|
+
<a href="https://www.npmjs.com/package/chartjs-plugin-trendline">NPM Package</a> |
|
|
211
|
+
<a href="https://github.com/Makanz/chartjs-plugin-trendline/issues">Report Issues</a>
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LineFitter } from '../utils/lineFitter';
|
|
2
|
+
import { ExponentialFitter } from '../utils/exponentialFitter';
|
|
2
3
|
import { drawTrendline, fillBelowTrendline, setLineStyle } from '../utils/drawing';
|
|
3
4
|
import { addTrendlineLabel } from './label';
|
|
4
5
|
|
|
@@ -14,6 +15,10 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
|
|
|
14
15
|
const yAxisID = dataset.yAxisID || 'y'; // Default to 'y' if no yAxisID is specified
|
|
15
16
|
const yScaleToUse = datasetMeta.controller.chart.scales[yAxisID] || yScale;
|
|
16
17
|
|
|
18
|
+
// Determine if we're using exponential or linear trendline
|
|
19
|
+
const isExponential = !!dataset.trendlineExponential;
|
|
20
|
+
const trendlineConfig = dataset.trendlineExponential || dataset.trendlineLinear || {};
|
|
21
|
+
|
|
17
22
|
const defaultColor = dataset.borderColor || 'rgba(169,169,169, .6)';
|
|
18
23
|
const {
|
|
19
24
|
colorMin = defaultColor,
|
|
@@ -22,22 +27,22 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
|
|
|
22
27
|
lineStyle = 'solid',
|
|
23
28
|
fillColor = false,
|
|
24
29
|
// trendoffset is now handled separately
|
|
25
|
-
} =
|
|
26
|
-
let trendoffset =
|
|
30
|
+
} = trendlineConfig;
|
|
31
|
+
let trendoffset = trendlineConfig.trendoffset || 0;
|
|
27
32
|
|
|
28
33
|
const {
|
|
29
34
|
color = defaultColor,
|
|
30
|
-
text = 'Trendline',
|
|
35
|
+
text = isExponential ? 'Exponential Trendline' : 'Trendline',
|
|
31
36
|
display = true,
|
|
32
37
|
displayValue = true,
|
|
33
38
|
offset = 10,
|
|
34
39
|
percentage = false,
|
|
35
|
-
} = (
|
|
40
|
+
} = (trendlineConfig && trendlineConfig.label) || {};
|
|
36
41
|
|
|
37
42
|
const {
|
|
38
43
|
family = "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
|
|
39
44
|
size = 12,
|
|
40
|
-
} = (
|
|
45
|
+
} = (trendlineConfig && trendlineConfig.label && trendlineConfig.label.font) || {};
|
|
41
46
|
|
|
42
47
|
const chartOptions = datasetMeta.controller.chart.options;
|
|
43
48
|
const parsingOptions =
|
|
@@ -45,11 +50,11 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
|
|
|
45
50
|
? chartOptions.parsing
|
|
46
51
|
: undefined;
|
|
47
52
|
const xAxisKey =
|
|
48
|
-
|
|
53
|
+
trendlineConfig?.xAxisKey || parsingOptions?.xAxisKey || 'x';
|
|
49
54
|
const yAxisKey =
|
|
50
|
-
|
|
55
|
+
trendlineConfig?.yAxisKey || parsingOptions?.yAxisKey || 'y';
|
|
51
56
|
|
|
52
|
-
let fitter = new LineFitter();
|
|
57
|
+
let fitter = isExponential ? new ExponentialFitter() : new LineFitter();
|
|
53
58
|
|
|
54
59
|
// --- Data Point Collection and Validation for LineFitter ---
|
|
55
60
|
|
|
@@ -156,31 +161,44 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
|
|
|
156
161
|
const chartArea = datasetMeta.controller.chart.chartArea; // Defines the drawable area in pixels.
|
|
157
162
|
|
|
158
163
|
// Determine trendline start/end points based on the 'projection' option.
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
if (trendlineConfig.projection) {
|
|
165
|
+
let points = [];
|
|
166
|
+
|
|
167
|
+
if (isExponential) {
|
|
168
|
+
// For exponential curves, we generate points across the x-axis range
|
|
169
|
+
const val_x_left = xScale.getValueForPixel(chartArea.left);
|
|
170
|
+
const y_at_left = fitter.f(val_x_left);
|
|
171
|
+
points.push({ x: val_x_left, y: y_at_left });
|
|
172
|
+
|
|
173
|
+
const val_x_right = xScale.getValueForPixel(chartArea.right);
|
|
174
|
+
const y_at_right = fitter.f(val_x_right);
|
|
175
|
+
points.push({ x: val_x_right, y: y_at_right });
|
|
176
|
+
} else {
|
|
177
|
+
// Linear projection logic (existing code)
|
|
178
|
+
const slope = fitter.slope();
|
|
179
|
+
const intercept = fitter.intercept();
|
|
180
|
+
|
|
181
|
+
if (Math.abs(slope) > 1e-6) {
|
|
182
|
+
const val_y_top = yScaleToUse.getValueForPixel(chartArea.top);
|
|
183
|
+
const x_at_top = (val_y_top - intercept) / slope;
|
|
184
|
+
points.push({ x: x_at_top, y: val_y_top });
|
|
185
|
+
|
|
186
|
+
const val_y_bottom = yScaleToUse.getValueForPixel(chartArea.bottom);
|
|
187
|
+
const x_at_bottom = (val_y_bottom - intercept) / slope;
|
|
188
|
+
points.push({ x: x_at_bottom, y: val_y_bottom });
|
|
189
|
+
} else {
|
|
190
|
+
points.push({ x: xScale.getValueForPixel(chartArea.left), y: intercept});
|
|
191
|
+
points.push({ x: xScale.getValueForPixel(chartArea.right), y: intercept});
|
|
192
|
+
}
|
|
176
193
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
const val_x_left = xScale.getValueForPixel(chartArea.left);
|
|
195
|
+
const y_at_left = fitter.f(val_x_left);
|
|
196
|
+
points.push({ x: val_x_left, y: y_at_left });
|
|
180
197
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
198
|
+
const val_x_right = xScale.getValueForPixel(chartArea.right);
|
|
199
|
+
const y_at_right = fitter.f(val_x_right);
|
|
200
|
+
points.push({ x: val_x_right, y: y_at_right });
|
|
201
|
+
}
|
|
184
202
|
|
|
185
203
|
const chartMinX = xScale.getValueForPixel(chartArea.left);
|
|
186
204
|
const chartMaxX = xScale.getValueForPixel(chartArea.right);
|
|
@@ -247,16 +265,23 @@ export const addFitter = (datasetMeta, ctx, dataset, xScale, yScale) => {
|
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
const angle = Math.atan2(y2_px - y1_px, x2_px - x1_px);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
|
|
269
|
+
if (trendlineConfig.label && display !== false) {
|
|
270
|
+
let trendText = text;
|
|
271
|
+
if (displayValue) {
|
|
272
|
+
if (isExponential) {
|
|
273
|
+
const coefficient = fitter.coefficient();
|
|
274
|
+
const growthRate = fitter.growthRate();
|
|
275
|
+
trendText = `${text} (a=${coefficient.toFixed(2)}, b=${growthRate.toFixed(2)})`;
|
|
276
|
+
} else {
|
|
277
|
+
const displaySlope = fitter.slope();
|
|
278
|
+
trendText = `${text} (Slope: ${
|
|
279
|
+
percentage
|
|
280
|
+
? (displaySlope * 100).toFixed(2) + '%'
|
|
281
|
+
: displaySlope.toFixed(2)
|
|
282
|
+
})`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
260
285
|
addTrendlineLabel(
|
|
261
286
|
ctx,
|
|
262
287
|
trendText,
|