Spaces:
Running
Running
acwr curve render if enough points
Browse files- src/main.ts +7 -2
- src/utils/metricAcwr.ts +10 -23
src/main.ts
CHANGED
|
@@ -355,9 +355,14 @@ function renderCharts(activities: Activity[], targetAcwr: number, predictToday:
|
|
| 355 |
}
|
| 356 |
endDate.setHours(23, 59, 59, 999);
|
| 357 |
|
| 358 |
-
// Calculate start date
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
const startDate = new Date(endDate);
|
| 360 |
-
startDate.setDate(startDate.getDate() - Math.floor(28 *
|
| 361 |
startDate.setHours(0, 0, 0, 0);
|
| 362 |
|
| 363 |
dateRange = {
|
|
|
|
| 355 |
}
|
| 356 |
endDate.setHours(23, 59, 59, 999);
|
| 357 |
|
| 358 |
+
// Calculate start date: use longer history for activities with less frequency
|
| 359 |
+
// Base: 2x chronic period (56 days), extended up to 3x (84 days) for sparse activities
|
| 360 |
+
const activityFrequency = activities.length / Math.max(1,
|
| 361 |
+
(endDate.getTime() - activities[0].date.getTime()) / (1000 * 60 * 60 * 24));
|
| 362 |
+
const multiplier = activityFrequency < 0.2 ? 3 : activityFrequency < 0.5 ? 2.5 : 2;
|
| 363 |
+
|
| 364 |
const startDate = new Date(endDate);
|
| 365 |
+
startDate.setDate(startDate.getDate() - Math.floor(28 * multiplier));
|
| 366 |
startDate.setHours(0, 0, 0, 0);
|
| 367 |
|
| 368 |
dateRange = {
|
src/utils/metricAcwr.ts
CHANGED
|
@@ -334,32 +334,19 @@ export function calculateMetricACWR(
|
|
| 334 |
const chronicAvg = chronicCount > 0 ? chronicSum / 28 : null;
|
| 335 |
average28d.push(chronicAvg);
|
| 336 |
|
| 337 |
-
// Calculate ACWR
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
const checkDateStr = formatDateLocal(allDates[i]);
|
| 347 |
-
const val = dailyValues.get(checkDateStr);
|
| 348 |
-
if (val !== undefined) {
|
| 349 |
-
acwrChronicSum += val;
|
| 350 |
-
}
|
| 351 |
-
}
|
| 352 |
-
const acwrChronicAvg = acwrChronicSum / 28;
|
| 353 |
-
|
| 354 |
-
// Calculate ACWR
|
| 355 |
-
if (acwrChronicAvg > 0 && acuteAvg !== null) {
|
| 356 |
-
acwr.push(acuteAvg / acwrChronicAvg);
|
| 357 |
} else {
|
| 358 |
acwr.push(null);
|
| 359 |
}
|
| 360 |
-
});
|
| 361 |
-
|
| 362 |
-
// Calculate tomorrow's required value to reach ACWR
|
| 363 |
let targetTomorrowValue: number | null = null;
|
| 364 |
|
| 365 |
if (allDates.length >= 28) {
|
|
|
|
| 334 |
const chronicAvg = chronicCount > 0 ? chronicSum / 28 : null;
|
| 335 |
average28d.push(chronicAvg);
|
| 336 |
|
| 337 |
+
// Calculate ACWR with smart requirements:
|
| 338 |
+
// - At the beginning (first 28 days): need at least 8 data points to avoid unrealistic spikes
|
| 339 |
+
// - After that: calculate whenever both averages are available (minimum 4 data points)
|
| 340 |
+
// This avoids spikes at the start while ensuring the end is always visible
|
| 341 |
+
const needsMinimumData = index < 28;
|
| 342 |
+
const hasEnoughData = needsMinimumData ? chronicCount >= 8 : chronicCount >= 4;
|
| 343 |
+
|
| 344 |
+
if (hasEnoughData && acuteAvg !== null && chronicAvg !== null && chronicAvg > 0) {
|
| 345 |
+
acwr.push(acuteAvg / chronicAvg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
} else {
|
| 347 |
acwr.push(null);
|
| 348 |
}
|
| 349 |
+
}); // Calculate tomorrow's required value to reach ACWR
|
|
|
|
|
|
|
| 350 |
let targetTomorrowValue: number | null = null;
|
| 351 |
|
| 352 |
if (allDates.length >= 28) {
|