glutamatt HF Staff commited on
Commit
3b6e4a8
·
verified ·
1 Parent(s): e12dab7

plan to reach target

Browse files
src/components/charts.ts CHANGED
@@ -56,11 +56,15 @@ function getACWRColor(value: number | null): string {
56
  const acwrGradientPlugin = {
57
  id: 'acwrGradient',
58
  afterDatasetsDraw(chart: Chart) {
59
- const meta = chart.getDatasetMeta(3); // ACWR is the 4th dataset (index 3)
 
 
 
 
60
  if (!meta || meta.hidden) return;
61
 
62
  const ctx = chart.ctx;
63
- const data = chart.data.datasets[3].data as (number | null)[];
64
  const chartArea = chart.chartArea;
65
 
66
  ctx.save();
@@ -350,10 +354,28 @@ function createDualAxisChart(
350
  const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
351
  if (!canvas) throw new Error(`Canvas ${canvasId} not found`);
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  const config: ChartConfiguration = {
354
  type: 'line',
355
  data: {
356
- labels: data.dates,
357
  datasets: [
358
  {
359
  type: 'scatter',
@@ -366,6 +388,20 @@ function createDualAxisChart(
366
  pointHoverRadius: 7,
367
  yAxisID: 'y',
368
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  {
370
  type: 'line',
371
  label: '7-Day Average',
@@ -409,6 +445,21 @@ function createDualAxisChart(
409
  fill: false,
410
  yAxisID: 'y1',
411
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  ],
413
  },
414
  options: {
 
56
  const acwrGradientPlugin = {
57
  id: 'acwrGradient',
58
  afterDatasetsDraw(chart: Chart) {
59
+ // Find the ACWR dataset by label
60
+ const acwrDatasetIndex = chart.data.datasets.findIndex(ds => ds.label === 'ACWR');
61
+ if (acwrDatasetIndex === -1) return;
62
+
63
+ const meta = chart.getDatasetMeta(acwrDatasetIndex);
64
  if (!meta || meta.hidden) return;
65
 
66
  const ctx = chart.ctx;
67
+ const data = chart.data.datasets[acwrDatasetIndex].data as (number | null)[];
68
  const chartArea = chart.chartArea;
69
 
70
  ctx.save();
 
354
  const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
355
  if (!canvas) throw new Error(`Canvas ${canvasId} not found`);
356
 
357
+ // Combine historical and future data for charts
358
+ const allDates = [...data.dates];
359
+ const allValues = [...data.values];
360
+ const allAverage7d = [...data.average7d];
361
+ const allAverage28d = [...data.average28d];
362
+ const allAcwr = [...data.acwr];
363
+
364
+ // Add future data if available
365
+ if (data.futureDates && data.futureValues) {
366
+ allDates.push(...data.futureDates);
367
+ allValues.push(...data.futureValues);
368
+ allAverage7d.push(...(data.futureAverage7d || []));
369
+ allAverage28d.push(...(data.futureAverage28d || []));
370
+ allAcwr.push(...(data.futureAcwr || []));
371
+ }
372
+
373
+ const historicalCount = data.dates.length;
374
+
375
  const config: ChartConfiguration = {
376
  type: 'line',
377
  data: {
378
+ labels: allDates,
379
  datasets: [
380
  {
381
  type: 'scatter',
 
388
  pointHoverRadius: 7,
389
  yAxisID: 'y',
390
  },
391
+ // Future daily values (grey, larger points) - exclude zero values
392
+ ...(data.futureValues ? [{
393
+ type: 'scatter' as const,
394
+ label: `Predicted Daily ${metricLabel}`,
395
+ data: Array(historicalCount).fill(null).concat(
396
+ data.futureValues.map(v => v > 0 ? v : null)
397
+ ),
398
+ backgroundColor: 'rgba(148, 163, 184, 0.6)',
399
+ borderColor: 'rgba(100, 116, 139, 0.8)',
400
+ borderWidth: 2,
401
+ pointRadius: 7,
402
+ pointHoverRadius: 9,
403
+ yAxisID: 'y',
404
+ }] : []),
405
  {
406
  type: 'line',
407
  label: '7-Day Average',
 
445
  fill: false,
446
  yAxisID: 'y1',
447
  },
448
+ // Future ACWR (grey, thicker)
449
+ ...(data.futureAcwr ? [{
450
+ type: 'line' as const,
451
+ label: 'Predicted ACWR',
452
+ data: Array(historicalCount).fill(null).concat(data.futureAcwr),
453
+ borderColor: 'rgba(148, 163, 184, 0.8)',
454
+ backgroundColor: 'rgba(148, 163, 184, 0.1)',
455
+ borderWidth: 5,
456
+ pointRadius: 0,
457
+ pointHoverRadius: 6,
458
+ tension: 0.4,
459
+ fill: false,
460
+ yAxisID: 'y1',
461
+ borderDash: [5, 5],
462
+ }] : []),
463
  ],
464
  },
465
  options: {
src/types/index.ts CHANGED
@@ -30,4 +30,10 @@ export interface MetricACWRData {
30
  restTomorrowACWR?: number | null; // What ACWR would be with a rest day tomorrow
31
  todayValue?: number | null; // Today's value for reference
32
  activitiesByDate?: Map<string, Activity[]>; // Activities grouped by date
 
 
 
 
 
 
33
  }
 
30
  restTomorrowACWR?: number | null; // What ACWR would be with a rest day tomorrow
31
  todayValue?: number | null; // Today's value for reference
32
  activitiesByDate?: Map<string, Activity[]>; // Activities grouped by date
33
+ // Future predictions (next 7 days)
34
+ futureDates?: string[]; // Dates for future predictions
35
+ futureValues?: (number | null)[]; // Optimal activity values for future days
36
+ futureAverage7d?: (number | null)[]; // Predicted 7-day rolling average
37
+ futureAverage28d?: (number | null)[]; // Predicted 28-day rolling average
38
+ futureAcwr?: (number | null)[]; // Predicted ACWR values
39
  }
src/utils/metricAcwr.ts CHANGED
@@ -10,6 +10,119 @@ function formatDateLocal(date: Date): string {
10
  return `${year}-${month}-${day}`;
11
  }
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  /**
14
  * Calculate ACWR for a specific metric (distance, duration, or TSS)
15
  * @param activities - Array of activities
@@ -227,5 +340,7 @@ export function calculateMetricACWR(
227
  restTomorrowACWR: allDates.length >= 28 ? restTomorrowACWR : undefined,
228
  todayValue: undefined,
229
  activitiesByDate,
 
 
230
  };
231
  }
 
10
  return `${year}-${month}-${day}`;
11
  }
12
 
13
+ /**
14
+ * Calculate optimal activity values for the next 7 days to reach and maintain target ACWR
15
+ * Uses a greedy algorithm that optimizes each day to get closer to target ACWR
16
+ */
17
+ function calculateOptimalFutureDays(
18
+ allDates: Date[],
19
+ dailyValues: Map<string, number>,
20
+ targetACWR: number,
21
+ numFutureDays: number = 7
22
+ ): {
23
+ futureDates: string[];
24
+ futureValues: number[];
25
+ futureAverage7d: number[];
26
+ futureAverage28d: number[];
27
+ futureAcwr: number[];
28
+ } {
29
+ const futureDates: string[] = [];
30
+ const futureValues: number[] = [];
31
+ const futureAverage7d: number[] = [];
32
+ const futureAverage28d: number[] = [];
33
+ const futureAcwr: number[] = [];
34
+
35
+ // Create a mutable copy of daily values to simulate future
36
+ const simulatedDailyValues = new Map(dailyValues);
37
+ const simulatedAllDates = [...allDates];
38
+
39
+ // Get the last date in the data
40
+ const lastDate = new Date(allDates[allDates.length - 1]);
41
+
42
+ for (let dayOffset = 1; dayOffset <= numFutureDays; dayOffset++) {
43
+ const futureDate = new Date(lastDate);
44
+ futureDate.setDate(lastDate.getDate() + dayOffset);
45
+ const futureDateStr = formatDateLocal(futureDate);
46
+
47
+ futureDates.push(futureDateStr);
48
+ simulatedAllDates.push(futureDate);
49
+
50
+ const currentIndex = simulatedAllDates.length - 1;
51
+
52
+ // Calculate what the 7-day sum would be (last 6 days + today)
53
+ let acuteSum = 0;
54
+ for (let i = Math.max(0, currentIndex - 6); i < currentIndex; i++) {
55
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
56
+ acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
57
+ }
58
+
59
+ // Calculate what the 28-day sum would be (last 27 days + today)
60
+ let chronicSum = 0;
61
+ for (let i = Math.max(0, currentIndex - 27); i < currentIndex; i++) {
62
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
63
+ chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
64
+ }
65
+
66
+ // Calculate optimal value for today using the formula:
67
+ // targetACWR = [(acuteSum + X) / 7] / [(chronicSum + X) / 28]
68
+ // Solving for X:
69
+ // X = (4 * acuteSum - targetACWR * chronicSum) / (targetACWR - 4)
70
+
71
+ const numerator = 4 * acuteSum - targetACWR * chronicSum;
72
+ const denominator = targetACWR - 4;
73
+
74
+ let optimalValue = 0;
75
+
76
+ if (Math.abs(denominator) > 0.001) {
77
+ optimalValue = numerator / denominator;
78
+
79
+ // Ensure non-negative values (can't have negative activity)
80
+ if (optimalValue < 0) {
81
+ optimalValue = 0;
82
+ }
83
+ } else {
84
+ // Special case: when targetACWR ≈ 4, we need a different approach
85
+ // In this case, set to average of recent values to maintain stability
86
+ const recentSum = acuteSum;
87
+ optimalValue = recentSum / 7;
88
+ }
89
+
90
+ // Store the optimal value for this future day
91
+ futureValues.push(optimalValue);
92
+ simulatedDailyValues.set(futureDateStr, optimalValue);
93
+
94
+ // Calculate the resulting 7-day average (acute load)
95
+ let newAcuteSum = 0;
96
+ for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
97
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
98
+ newAcuteSum += simulatedDailyValues.get(checkDateStr) || 0;
99
+ }
100
+ const newAcuteAvg = newAcuteSum / 7;
101
+ futureAverage7d.push(newAcuteAvg);
102
+
103
+ // Calculate the resulting 28-day average (chronic load)
104
+ let newChronicSum = 0;
105
+ for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
106
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
107
+ newChronicSum += simulatedDailyValues.get(checkDateStr) || 0;
108
+ }
109
+ const newChronicAvg = newChronicSum / 28;
110
+ futureAverage28d.push(newChronicAvg);
111
+
112
+ // Calculate the resulting ACWR
113
+ const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
114
+ futureAcwr.push(newACWR);
115
+ }
116
+
117
+ return {
118
+ futureDates,
119
+ futureValues,
120
+ futureAverage7d,
121
+ futureAverage28d,
122
+ futureAcwr,
123
+ };
124
+ }
125
+
126
  /**
127
  * Calculate ACWR for a specific metric (distance, duration, or TSS)
128
  * @param activities - Array of activities
 
340
  restTomorrowACWR: allDates.length >= 28 ? restTomorrowACWR : undefined,
341
  todayValue: undefined,
342
  activitiesByDate,
343
+ // Add future predictions if we have enough data
344
+ ...(allDates.length >= 28 ? calculateOptimalFutureDays(allDates, dailyValues, targetACWR, 7) : {}),
345
  };
346
  }