glutamatt HF Staff commited on
Commit
fcc9f70
·
verified ·
1 Parent(s): 62a5cee

some fixes

Browse files
src/components/charts.ts CHANGED
@@ -526,13 +526,24 @@ function createDualAxisChart(
526
  const allAverage28d = [...data.average28d];
527
  const allAcwr = [...data.acwr];
528
 
529
- // Add future data if available
530
- if (data.futureDates && data.futureValues) {
531
- allDates.push(...data.futureDates);
532
- allValues.push(...data.futureValues);
533
- allAverage7d.push(...(data.futureAverage7d || []));
534
- allAverage28d.push(...(data.futureAverage28d || []));
535
- allAcwr.push(...(data.futureAcwr || []));
 
 
 
 
 
 
 
 
 
 
 
536
  }
537
 
538
  const historicalCount = data.dates.length;
@@ -553,12 +564,12 @@ function createDualAxisChart(
553
  pointHoverRadius: 7,
554
  yAxisID: 'y',
555
  },
556
- // Future daily values (grey, larger points) - exclude zero values
557
- ...(data.futureValues ? [{
558
  type: 'scatter' as const,
559
  label: `Predicted Daily ${metricLabel}`,
560
  data: Array(historicalCount).fill(null).concat(
561
- data.futureValues.map(v => (v !== null && v > 0) ? v : null)
562
  ),
563
  backgroundColor: 'rgba(148, 163, 184, 0.6)',
564
  borderColor: 'rgba(100, 116, 139, 0.8)',
@@ -610,11 +621,13 @@ function createDualAxisChart(
610
  fill: false,
611
  yAxisID: 'y1',
612
  },
613
- // Future ACWR (grey, thicker)
614
- ...(data.futureAcwr ? [{
615
  type: 'line' as const,
616
  label: 'Predicted ACWR',
617
- data: Array(historicalCount).fill(null).concat(data.futureAcwr),
 
 
618
  borderColor: 'rgba(148, 163, 184, 0.8)',
619
  backgroundColor: 'rgba(148, 163, 184, 0.1)',
620
  borderWidth: 5,
@@ -729,7 +742,8 @@ export function createDistanceChart(data: MetricACWRData): void {
729
  '(km)',
730
  'rgba(234, 179, 8, 0.8)'
731
  );
732
- updateTargetInfo('distance-target', data.targetTomorrowValue, 'km', data.targetACWR, data.restTomorrowACWR);
 
733
 
734
  // Update ACWR display in title
735
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
@@ -747,7 +761,8 @@ export function createDurationChart(data: MetricACWRData): void {
747
  '(min)',
748
  'rgba(234, 179, 8, 0.8)'
749
  );
750
- updateTargetInfo('duration-target', data.targetTomorrowValue, 'minutes', data.targetACWR, data.restTomorrowACWR);
 
751
 
752
  // Update ACWR display in title
753
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
@@ -765,7 +780,10 @@ export function createTSSChart(data: MetricACWRData): void {
765
  '',
766
  'rgba(234, 179, 8, 0.8)'
767
  );
768
- updateTargetInfo('tss-target', data.targetTomorrowValue, 'TSS', data.targetACWR, data.restTomorrowACWR);
 
 
 
769
 
770
  // Update ACWR display in title
771
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
@@ -783,33 +801,57 @@ export function createCaloriesChart(data: MetricACWRData): void {
783
  '',
784
  'rgba(234, 179, 8, 0.8)'
785
  );
786
- updateTargetInfo('calories-target', data.targetTomorrowValue, 'cal', data.targetACWR, data.restTomorrowACWR);
 
787
 
788
  // Update ACWR display in title
789
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
790
  updateACWRDisplay('calories-acwr-display', currentACWR);
791
  }
792
 
793
- function updateTargetInfo(elementId: string, targetValue: number | null | undefined, unit: string, targetACWR: number | undefined, restTomorrowACWR: number | null | undefined): void {
794
  const element = document.getElementById(elementId);
795
  if (!element) return;
796
 
797
- if (targetACWR !== undefined) {
798
  let html: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
 
800
  if (targetValue === null || targetValue === undefined) {
801
- html = `<strong>🎯 Next activity :</strong> <span style="color: var(--secondary-color);">ACWR of ${targetACWR} cannot be reached</span>`;
802
  } else if (targetValue === 0) {
803
- html = `<strong>🎯 Next activity :</strong> <span style="color: var(--secondary-color);">Rest recommended to reach ACWR of ${targetACWR}</span>`;
804
  } else {
805
  const formattedValue = targetValue.toFixed(1);
806
- html = `<strong>🎯 Next activity :</strong> <span class="target-value">${formattedValue} ${unit}</span> <span style="color: var(--secondary-color);">to reach ACWR of ${targetACWR}</span>`;
807
  }
808
 
809
  // Add rest tomorrow information
810
- if (restTomorrowACWR !== null && restTomorrowACWR !== undefined) {
811
- const color = getACWRColor(restTomorrowACWR);
812
- html += `<br><strong>😴 If Rest :</strong> <span style="color: ${color};">ACWR ${restTomorrowACWR.toFixed(2)}</span>`;
813
  }
814
 
815
  element.innerHTML = html;
 
526
  const allAverage28d = [...data.average28d];
527
  const allAcwr = [...data.acwr];
528
 
529
+ // Add ALL future dates to maintain x-axis continuity
530
+ // Use allFuture* arrays for complete data including ACWR curve
531
+ if (data.allFutureDates && data.allFutureValues) {
532
+ const MIN_THRESHOLD = 1;
533
+
534
+ // Use the complete future data that includes all days
535
+ data.allFutureDates.forEach((date, i) => {
536
+ allDates.push(date);
537
+
538
+ // Only show activity value if above threshold
539
+ const value = data.allFutureValues![i];
540
+ allValues.push(value >= MIN_THRESHOLD ? value : null);
541
+
542
+ // Include all ACWR and average data for curve continuity
543
+ allAverage7d.push(data.allFutureAverage7d?.[i] ?? null);
544
+ allAverage28d.push(data.allFutureAverage28d?.[i] ?? null);
545
+ allAcwr.push(data.allFutureAcwr?.[i] ?? null);
546
+ });
547
  }
548
 
549
  const historicalCount = data.dates.length;
 
564
  pointHoverRadius: 7,
565
  yAxisID: 'y',
566
  },
567
+ // Future daily values (grey, larger points) - filtered to exclude insignificant values
568
+ ...(data.futureValues && data.futureValues.some(v => v !== null && v >= 1) ? [{
569
  type: 'scatter' as const,
570
  label: `Predicted Daily ${metricLabel}`,
571
  data: Array(historicalCount).fill(null).concat(
572
+ allValues.slice(historicalCount)
573
  ),
574
  backgroundColor: 'rgba(148, 163, 184, 0.6)',
575
  borderColor: 'rgba(100, 116, 139, 0.8)',
 
621
  fill: false,
622
  yAxisID: 'y1',
623
  },
624
+ // Future ACWR (grey, thicker) - filtered to match future values
625
+ ...(data.futureAcwr && allAcwr.length > historicalCount ? [{
626
  type: 'line' as const,
627
  label: 'Predicted ACWR',
628
+ data: Array(historicalCount).fill(null).concat(
629
+ allAcwr.slice(historicalCount)
630
+ ),
631
  borderColor: 'rgba(148, 163, 184, 0.8)',
632
  backgroundColor: 'rgba(148, 163, 184, 0.1)',
633
  borderWidth: 5,
 
742
  '(km)',
743
  'rgba(234, 179, 8, 0.8)'
744
  );
745
+ // Use first predicted value if it's for tomorrow/today, otherwise use targetTomorrowValue
746
+ updateTargetInfo('distance-target', data, 'km');
747
 
748
  // Update ACWR display in title
749
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
 
761
  '(min)',
762
  'rgba(234, 179, 8, 0.8)'
763
  );
764
+ // Use first predicted value if it's for tomorrow/today, otherwise use targetTomorrowValue
765
+ updateTargetInfo('duration-target', data, 'minutes');
766
 
767
  // Update ACWR display in title
768
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
 
780
  '',
781
  'rgba(234, 179, 8, 0.8)'
782
  );
783
+ // Use first predicted value if it's for tomorrow/today, otherwise use targetTomorrowValue
784
+ updateTargetInfo('tss-target', data, 'TSS');
785
+
786
+ // Update ACWR display in titleet', nextDistanceValue, 'km', data.targetACWR, data.restTomorrowACWR);
787
 
788
  // Update ACWR display in title
789
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
 
801
  '',
802
  'rgba(234, 179, 8, 0.8)'
803
  );
804
+ // Use first predicted value if it's for tomorrow/today, otherwise use targetTomorrowValue
805
+ updateTargetInfo('calories-target', data, 'cal');
806
 
807
  // Update ACWR display in title
808
  const currentACWR = data.acwr.length > 0 ? data.acwr[data.acwr.length - 1] : null;
809
  updateACWRDisplay('calories-acwr-display', currentACWR);
810
  }
811
 
812
+ function updateTargetInfo(elementId: string, data: MetricACWRData, unit: string): void {
813
  const element = document.getElementById(elementId);
814
  if (!element) return;
815
 
816
+ if (data.targetACWR !== undefined) {
817
  let html: string;
818
+ let targetValue: number | null | undefined;
819
+
820
+ // Check if first prediction is for tomorrow (day after last date in data)
821
+ if (data.futureDates && data.futureDates.length > 0 && data.futureValues && data.futureValues.length > 0) {
822
+ const lastHistoricalDate = data.dates[data.dates.length - 1];
823
+ const firstPredictionDate = data.futureDates[0];
824
+
825
+ // Calculate tomorrow's date from last historical date
826
+ const lastDate = new Date(lastHistoricalDate);
827
+ const tomorrowDate = new Date(lastDate);
828
+ tomorrowDate.setDate(lastDate.getDate() + 1);
829
+ const tomorrowStr = tomorrowDate.toISOString().split('T')[0];
830
+
831
+ // If first prediction is for tomorrow, use it; otherwise use targetTomorrowValue
832
+ if (firstPredictionDate === tomorrowStr) {
833
+ targetValue = data.futureValues[0];
834
+ } else {
835
+ // First prediction is not for tomorrow, so tomorrow must be a rest day
836
+ targetValue = data.targetTomorrowValue;
837
+ }
838
+ } else {
839
+ targetValue = data.targetTomorrowValue;
840
+ }
841
 
842
  if (targetValue === null || targetValue === undefined) {
843
+ html = `<strong>🎯 Next activity :</strong> <span style="color: var(--secondary-color);">ACWR of ${data.targetACWR} cannot be reached</span>`;
844
  } else if (targetValue === 0) {
845
+ html = `<strong>🎯 Next activity :</strong> <span style="color: var(--secondary-color);">Rest recommended to reach ACWR of ${data.targetACWR}</span>`;
846
  } else {
847
  const formattedValue = targetValue.toFixed(1);
848
+ html = `<strong>🎯 Next activity :</strong> <span class="target-value">${formattedValue} ${unit}</span> <span style="color: var(--secondary-color);">to reach ACWR of ${data.targetACWR}</span>`;
849
  }
850
 
851
  // Add rest tomorrow information
852
+ if (data.restTomorrowACWR !== null && data.restTomorrowACWR !== undefined) {
853
+ const color = getACWRColor(data.restTomorrowACWR);
854
+ html += `<br><strong>😴 If Rest :</strong> <span style="color: ${color};">ACWR ${data.restTomorrowACWR.toFixed(2)}</span>`;
855
  }
856
 
857
  element.innerHTML = html;
src/types/index.ts CHANGED
@@ -30,10 +30,16 @@ 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
- // 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
  }
 
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 (filtered - only significant activities)
34
+ futureDates?: string[]; // Dates for future predictions (filtered)
35
+ futureValues?: (number | null)[]; // Optimal activity values for future days (filtered)
36
+ futureAverage7d?: (number | null)[]; // Predicted 7-day rolling average (filtered)
37
+ futureAverage28d?: (number | null)[]; // Predicted 28-day rolling average (filtered)
38
+ futureAcwr?: (number | null)[]; // Predicted ACWR values (filtered)
39
+ // Complete future predictions (including rest days for ACWR curve continuity)
40
+ allFutureDates?: string[]; // All future dates including rest days
41
+ allFutureValues?: number[]; // All future values including low values
42
+ allFutureAverage7d?: number[]; // All 7-day averages
43
+ allFutureAverage28d?: number[]; // All 28-day averages
44
+ allFutureAcwr?: number[]; // All ACWR values for curve continuity
45
  }
src/utils/metricAcwr.ts CHANGED
@@ -31,6 +31,11 @@ function calculateOptimalFutureDays(
31
  futureAverage7d: number[];
32
  futureAverage28d: number[];
33
  futureAcwr: number[];
 
 
 
 
 
34
  } {
35
  // Variance weight parameter (0-1): controls trade-off between ACWR accuracy and value consistency
36
  // Higher weight = more preference for consistent daily values
@@ -184,26 +189,46 @@ function calculateOptimalFutureDays(
184
  }
185
 
186
  // Build final results with optimized values
 
 
 
187
  const futureDates: string[] = [];
188
- const futureValues: number[] = bestValues;
189
  const futureAverage7d: number[] = [];
190
  const futureAverage28d: number[] = [];
191
  const futureAcwr: number[] = [];
192
 
193
  const finalSimulatedDailyValues = new Map(dailyValues);
194
  const finalSimulatedAllDates = [...allDates];
 
 
 
 
 
 
 
195
 
196
  for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
197
  const futureDate = new Date(lastDate);
198
  futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
199
  const futureDateStr = formatDateLocal(futureDate);
 
 
 
 
 
 
 
 
 
 
200
 
201
- futureDates.push(futureDateStr);
202
  finalSimulatedDailyValues.set(futureDateStr, bestValues[dayOffset]);
203
  finalSimulatedAllDates.push(futureDate);
204
 
205
  const currentIndex = finalSimulatedAllDates.length - 1;
206
 
 
207
  // Calculate 7-day average
208
  let newAcuteSum = 0;
209
  for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
@@ -211,7 +236,7 @@ function calculateOptimalFutureDays(
211
  newAcuteSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
212
  }
213
  const newAcuteAvg = newAcuteSum / 7;
214
- futureAverage7d.push(newAcuteAvg);
215
 
216
  // Calculate 28-day average
217
  let newChronicSum = 0;
@@ -220,11 +245,11 @@ function calculateOptimalFutureDays(
220
  newChronicSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
221
  }
222
  const newChronicAvg = newChronicSum / 28;
223
- futureAverage28d.push(newChronicAvg);
224
 
225
  // Calculate ACWR
226
  const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
227
- futureAcwr.push(newACWR);
228
  }
229
 
230
  return {
@@ -233,6 +258,12 @@ function calculateOptimalFutureDays(
233
  futureAverage7d,
234
  futureAverage28d,
235
  futureAcwr,
 
 
 
 
 
 
236
  };
237
  }
238
 
 
31
  futureAverage7d: number[];
32
  futureAverage28d: number[];
33
  futureAcwr: number[];
34
+ allFutureDates: string[];
35
+ allFutureValues: number[];
36
+ allFutureAverage7d: number[];
37
+ allFutureAverage28d: number[];
38
+ allFutureAcwr: number[];
39
  } {
40
  // Variance weight parameter (0-1): controls trade-off between ACWR accuracy and value consistency
41
  // Higher weight = more preference for consistent daily values
 
189
  }
190
 
191
  // Build final results with optimized values
192
+ // Filter out insignificant values (< 1) - likely rest days
193
+ const MIN_THRESHOLD = 1;
194
+
195
  const futureDates: string[] = [];
196
+ const futureValues: number[] = [];
197
  const futureAverage7d: number[] = [];
198
  const futureAverage28d: number[] = [];
199
  const futureAcwr: number[] = [];
200
 
201
  const finalSimulatedDailyValues = new Map(dailyValues);
202
  const finalSimulatedAllDates = [...allDates];
203
+
204
+ // We need to track ALL dates and their ACWR for curve continuity
205
+ const allFutureDates: string[] = [];
206
+ const allFutureValues: number[] = [];
207
+ const allFutureAverage7d: number[] = [];
208
+ const allFutureAverage28d: number[] = [];
209
+ const allFutureAcwr: number[] = [];
210
 
211
  for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
212
  const futureDate = new Date(lastDate);
213
  futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
214
  const futureDateStr = formatDateLocal(futureDate);
215
+
216
+ // Store ALL dates for ACWR calculation
217
+ allFutureDates.push(futureDateStr);
218
+ allFutureValues.push(bestValues[dayOffset]);
219
+
220
+ // Only include days with significant values (>= MIN_THRESHOLD) in the filtered arrays
221
+ if (bestValues[dayOffset] >= MIN_THRESHOLD) {
222
+ futureDates.push(futureDateStr);
223
+ futureValues.push(bestValues[dayOffset]);
224
+ }
225
 
 
226
  finalSimulatedDailyValues.set(futureDateStr, bestValues[dayOffset]);
227
  finalSimulatedAllDates.push(futureDate);
228
 
229
  const currentIndex = finalSimulatedAllDates.length - 1;
230
 
231
+ // Calculate averages and ACWR for ALL days to maintain curve continuity
232
  // Calculate 7-day average
233
  let newAcuteSum = 0;
234
  for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
 
236
  newAcuteSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
237
  }
238
  const newAcuteAvg = newAcuteSum / 7;
239
+ allFutureAverage7d.push(newAcuteAvg);
240
 
241
  // Calculate 28-day average
242
  let newChronicSum = 0;
 
245
  newChronicSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
246
  }
247
  const newChronicAvg = newChronicSum / 28;
248
+ allFutureAverage28d.push(newChronicAvg);
249
 
250
  // Calculate ACWR
251
  const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
252
+ allFutureAcwr.push(newACWR);
253
  }
254
 
255
  return {
 
258
  futureAverage7d,
259
  futureAverage28d,
260
  futureAcwr,
261
+ // Include all dates and ACWR for curve continuity
262
+ allFutureDates,
263
+ allFutureValues,
264
+ allFutureAverage7d,
265
+ allFutureAverage28d,
266
+ allFutureAcwr,
267
  };
268
  }
269