-
Notifications
You must be signed in to change notification settings - Fork 1
/
strava_recent_efforts_fit_curve.js
98 lines (80 loc) · 3.7 KB
/
strava_recent_efforts_fit_curve.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
javascript:(function() {
/* Visualizes performance trends on Strava segment history charts by adding a linear regression line and improvement rate calculation. */
/* E.g. https://www.strava.com/segments/38142135 -- result: https://imgur.com/a/nwlu1Es */
/* Simple polynomial regression implementation */
function polyRegression(points, degree) {
const n = points.length;
const xSum = points.reduce((sum, p) => sum + p.x, 0);
const ySum = points.reduce((sum, p) => sum + p.y, 0);
const xySum = points.reduce((sum, p) => sum + p.x * p.y, 0);
const x2Sum = points.reduce((sum, p) => sum + p.x * p.x, 0);
const m = (n * xySum - xSum * ySum) / (n * x2Sum - xSum * xSum);
const b = (ySum - m * xSum) / n;
return {m, b};
}
/* Extract data points from SVG circles */
function extractDataPoints() {
const circles = document.querySelectorAll('circle.mark:not(.personal-best-mark)');
const points = [];
circles.forEach(circle => {
const x = parseFloat(circle.getAttribute('cx'));
const y = parseFloat(circle.getAttribute('cy'));
if (!isNaN(x) && !isNaN(y)) {
points.push({x, y});
}
});
return points;
}
/* Create SVG path for the fit curve */
function createFitCurvePath(points, reg) {
const sortedPoints = [...points].sort((a, b) => a.x - b.x);
const start = sortedPoints[0].x;
const end = sortedPoints[sortedPoints.length - 1].x;
let d = `M ${start} ${reg.m * start + reg.b}`;
for (let x = start; x <= end; x += (end - start) / 100) {
const y = reg.m * x + reg.b;
d += ` L ${x} ${y}`;
}
return d;
}
/* Main execution */
try {
/* Find the correct SVG and group */
const svg = document.querySelector('#athlete-history-chart svg');
if (!svg) throw new Error('Chart SVG not found');
const transformedGroup = svg.querySelector('g[transform^="translate"]');
if (!transformedGroup) throw new Error('Main group not found');
const points = extractDataPoints();
if (points.length < 2) throw new Error('Insufficient data points');
const regression = polyRegression(points, 1);
/* Create the fit curve path */
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', createFitCurvePath(points, regression));
path.setAttribute('stroke', '#FF4444');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-dasharray', '5,5');
path.setAttribute('class', 'trend-line');
/* Add path to SVG */
transformedGroup.appendChild(path);
/* Add legend text */
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', '100');
text.setAttribute('y', '20');
text.setAttribute('fill', '#FF4444');
text.setAttribute('class', 'trend-label');
text.textContent = 'Trend Line';
transformedGroup.appendChild(text);
/* Calculate and display slope */
const slopeText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const timePerMonth = regression.m * -1 * (3600 / points[points.length - 1].x) * 30; /* Convert to minutes per month */
slopeText.setAttribute('x', '100');
slopeText.setAttribute('y', '40');
slopeText.setAttribute('fill', '#FF4444');
slopeText.setAttribute('class', 'trend-slope');
slopeText.textContent = `${timePerMonth.toFixed(1)} sec/month improvement`;
transformedGroup.appendChild(slopeText);
} catch (error) {
alert('Error: ' + error.message);
})();
})();