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

predict today checkbox

Browse files
Files changed (4) hide show
  1. index.html +6 -0
  2. src/components/charts.ts +1 -1
  3. src/main.ts +50 -10
  4. src/utils/metricAcwr.ts +35 -5
index.html CHANGED
@@ -23,6 +23,12 @@
23
  <input type="number" id="target-acwr-input" value="1.3" min="0.5" max="3" step="0.1"
24
  placeholder="1.3" />
25
  </div>
 
 
 
 
 
 
26
  <div class="ftp-input-container">
27
  <label for="threshold-hr-input">Threshold HR</label>
28
  <input type="number" id="threshold-hr-input" value="190" min="100" max="220" step="1"
 
23
  <input type="number" id="target-acwr-input" value="1.3" min="0.5" max="3" step="0.1"
24
  placeholder="1.3" />
25
  </div>
26
+ <div class="ftp-input-container">
27
+ <label for="predict-today-input" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
28
+ <input type="checkbox" id="predict-today-input" style="width: auto; margin: 0;" />
29
+ <span>Predict today</span>
30
+ </label>
31
+ </div>
32
  <div class="ftp-input-container">
33
  <label for="threshold-hr-input">Threshold HR</label>
34
  <input type="number" id="threshold-hr-input" value="190" min="100" max="220" step="1"
src/components/charts.ts CHANGED
@@ -59,7 +59,7 @@ const acwrGradientPlugin = {
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
 
 
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
 
src/main.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
  const csvUpload = document.getElementById('csv-upload') as HTMLInputElement;
15
  const ftpInput = document.getElementById('ftp-input') as HTMLInputElement;
16
  const targetAcwrInput = document.getElementById('target-acwr-input') as HTMLInputElement;
 
17
  const thresholdHrInput = document.getElementById('threshold-hr-input') as HTMLInputElement;
18
  const restingHrInput = document.getElementById('resting-hr-input') as HTMLInputElement;
19
  const uploadStatus = document.getElementById('upload-status') as HTMLDivElement;
@@ -31,6 +32,7 @@ let selectedActivityTypes: Set<string> = new Set(['Running']);
31
  // Event listeners
32
  csvUpload?.addEventListener('change', handleFileUpload);
33
  targetAcwrInput?.addEventListener('input', handleTargetAcwrChange);
 
34
  helpButton?.addEventListener('click', () => helpPopover?.classList.remove('hidden'));
35
  helpClose?.addEventListener('click', () => helpPopover?.classList.add('hidden'));
36
  helpPopover?.addEventListener('click', (e) => {
@@ -49,7 +51,22 @@ function handleTargetAcwrChange(): void {
49
  : allActivities;
50
 
51
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
52
- renderCharts(filteredActivities, targetAcwr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
 
55
  function createActivityTypeFilters(activities: Activity[]): void {
@@ -129,7 +146,8 @@ function handleFilterChange(): void {
129
  : allActivities;
130
 
131
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
132
- renderCharts(filteredActivities, targetAcwr);
 
133
  }
134
 
135
  async function handleFileUpload(event: Event): Promise<void> {
@@ -164,7 +182,8 @@ async function handleFileUpload(event: Event): Promise<void> {
164
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
165
  : allActivities;
166
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
167
- renderCharts(filteredActivities, targetAcwr);
 
168
 
169
  // Show filter and charts sections
170
  filterSection.classList.remove('hidden');
@@ -182,15 +201,32 @@ async function handleFileUpload(event: Event): Promise<void> {
182
  }
183
  }
184
 
185
- function renderCharts(activities: Activity[], targetAcwr: number): void {
186
  // Calculate date range from first activity to today
187
  let dateRange: { start: Date; end: Date } | undefined;
188
  if (allActivities.length > 0) {
189
- // Use current date/time as end date (today)
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  const endDate = new Date();
 
 
 
 
191
  endDate.setHours(23, 59, 59, 999);
192
 
193
- // Calculate start date as 1.5 times chronic workload period (28 days) before today
194
  const startDate = new Date(endDate);
195
  startDate.setDate(startDate.getDate() - Math.floor(28 * 1.5));
196
  startDate.setHours(0, 0, 0, 0);
@@ -206,25 +242,29 @@ function renderCharts(activities: Activity[], targetAcwr: number): void {
206
  activities,
207
  (activity) => activity.distance,
208
  dateRange,
209
- targetAcwr
 
210
  );
211
  const durationData = calculateMetricACWR(
212
  activities,
213
  (activity) => activity.duration,
214
  dateRange,
215
- targetAcwr
 
216
  );
217
  const tssData = calculateMetricACWR(
218
  activities,
219
  (activity) => activity.trainingStressScore,
220
  dateRange,
221
- targetAcwr
 
222
  );
223
  const caloriesData = calculateMetricACWR(
224
  activities,
225
  (activity) => activity.calories,
226
  dateRange,
227
- targetAcwr
 
228
  );
229
 
230
  // Destroy existing charts
 
14
  const csvUpload = document.getElementById('csv-upload') as HTMLInputElement;
15
  const ftpInput = document.getElementById('ftp-input') as HTMLInputElement;
16
  const targetAcwrInput = document.getElementById('target-acwr-input') as HTMLInputElement;
17
+ const predictTodayInput = document.getElementById('predict-today-input') as HTMLInputElement;
18
  const thresholdHrInput = document.getElementById('threshold-hr-input') as HTMLInputElement;
19
  const restingHrInput = document.getElementById('resting-hr-input') as HTMLInputElement;
20
  const uploadStatus = document.getElementById('upload-status') as HTMLDivElement;
 
32
  // Event listeners
33
  csvUpload?.addEventListener('change', handleFileUpload);
34
  targetAcwrInput?.addEventListener('input', handleTargetAcwrChange);
35
+ predictTodayInput?.addEventListener('change', handlePredictTodayChange);
36
  helpButton?.addEventListener('click', () => helpPopover?.classList.remove('hidden'));
37
  helpClose?.addEventListener('click', () => helpPopover?.classList.add('hidden'));
38
  helpPopover?.addEventListener('click', (e) => {
 
51
  : allActivities;
52
 
53
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
54
+ const predictToday = predictTodayInput?.checked || false;
55
+ renderCharts(filteredActivities, targetAcwr, predictToday);
56
+ }
57
+
58
+ function handlePredictTodayChange(): void {
59
+ // Only refresh if we have activities loaded
60
+ if (allActivities.length === 0) return;
61
+
62
+ // Filter and render with new predict today setting
63
+ const filteredActivities = selectedActivityTypes.size > 0
64
+ ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
65
+ : allActivities;
66
+
67
+ const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
68
+ const predictToday = predictTodayInput?.checked || false;
69
+ renderCharts(filteredActivities, targetAcwr, predictToday);
70
  }
71
 
72
  function createActivityTypeFilters(activities: Activity[]): void {
 
146
  : allActivities;
147
 
148
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
149
+ const predictToday = predictTodayInput?.checked || false;
150
+ renderCharts(filteredActivities, targetAcwr, predictToday);
151
  }
152
 
153
  async function handleFileUpload(event: Event): Promise<void> {
 
182
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
183
  : allActivities;
184
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
185
+ const predictToday = predictTodayInput?.checked || false;
186
+ renderCharts(filteredActivities, targetAcwr, predictToday);
187
 
188
  // Show filter and charts sections
189
  filterSection.classList.remove('hidden');
 
201
  }
202
  }
203
 
204
+ function renderCharts(activities: Activity[], targetAcwr: number, predictToday: boolean = false): void {
205
  // Calculate date range from first activity to today
206
  let dateRange: { start: Date; end: Date } | undefined;
207
  if (allActivities.length > 0) {
208
+ // Determine end date based on whether we'll include today in predictions
209
+ const today = new Date();
210
+ today.setHours(0, 0, 0, 0);
211
+ const todayStr = today.toISOString().split('T')[0];
212
+
213
+ // Check if today has activities
214
+ const todayHasActivities = activities.some(activity => {
215
+ const activityDate = new Date(activity.date);
216
+ activityDate.setHours(0, 0, 0, 0);
217
+ return activityDate.getTime() === today.getTime();
218
+ });
219
+
220
+ // If predictToday is checked and today has no activities, end range at yesterday
221
+ // This prevents today from appearing twice (once as null, once as prediction)
222
  const endDate = new Date();
223
+ if (predictToday && !todayHasActivities) {
224
+ // End at yesterday so today only appears in predictions
225
+ endDate.setDate(endDate.getDate() - 1);
226
+ }
227
  endDate.setHours(23, 59, 59, 999);
228
 
229
+ // Calculate start date as 1.5 times chronic workload period (28 days) before end date
230
  const startDate = new Date(endDate);
231
  startDate.setDate(startDate.getDate() - Math.floor(28 * 1.5));
232
  startDate.setHours(0, 0, 0, 0);
 
242
  activities,
243
  (activity) => activity.distance,
244
  dateRange,
245
+ targetAcwr,
246
+ predictToday
247
  );
248
  const durationData = calculateMetricACWR(
249
  activities,
250
  (activity) => activity.duration,
251
  dateRange,
252
+ targetAcwr,
253
+ predictToday
254
  );
255
  const tssData = calculateMetricACWR(
256
  activities,
257
  (activity) => activity.trainingStressScore,
258
  dateRange,
259
+ targetAcwr,
260
+ predictToday
261
  );
262
  const caloriesData = calculateMetricACWR(
263
  activities,
264
  (activity) => activity.calories,
265
  dateRange,
266
+ targetAcwr,
267
+ predictToday
268
  );
269
 
270
  // Destroy existing charts
src/utils/metricAcwr.ts CHANGED
@@ -13,12 +13,18 @@ function formatDateLocal(date: Date): string {
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[];
@@ -39,9 +45,12 @@ function calculateOptimalFutureDays(
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);
@@ -128,12 +137,15 @@ function calculateOptimalFutureDays(
128
  * @param activities - Array of activities
129
  * @param metricExtractor - Function to extract the metric value from an activity
130
  * @param dateRange - Optional date range to maintain consistency
 
 
131
  */
132
  export function calculateMetricACWR(
133
  activities: Activity[],
134
  metricExtractor: (activity: Activity) => number | undefined,
135
  dateRange?: { start: Date; end: Date },
136
- targetACWR: number = 1.3
 
137
  ): MetricACWRData {
138
  if (activities.length === 0 && !dateRange) {
139
  return {
@@ -341,6 +353,24 @@ export function calculateMetricACWR(
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
  }
 
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
+ * @param allDates - Array of all historical dates
17
+ * @param dailyValues - Map of date strings to activity values
18
+ * @param targetACWR - Target ACWR to achieve
19
+ * @param numFutureDays - Number of future days to predict (default 7)
20
+ * @param startOffset - Day offset to start predictions (0 for today, 1 for tomorrow)
21
  */
22
  function calculateOptimalFutureDays(
23
  allDates: Date[],
24
  dailyValues: Map<string, number>,
25
  targetACWR: number,
26
+ numFutureDays: number = 7,
27
+ startOffset: number = 1
28
  ): {
29
  futureDates: string[];
30
  futureValues: number[];
 
45
  // Get the last date in the data
46
  const lastDate = new Date(allDates[allDates.length - 1]);
47
 
48
+ for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
49
  const futureDate = new Date(lastDate);
50
+ // Add 1 + startOffset + dayOffset to go beyond lastDate
51
+ // startOffset=0: start from day after lastDate (tomorrow if lastDate is today)
52
+ // startOffset=1: skip one day, start from 2 days after lastDate
53
+ futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
54
  const futureDateStr = formatDateLocal(futureDate);
55
 
56
  futureDates.push(futureDateStr);
 
137
  * @param activities - Array of activities
138
  * @param metricExtractor - Function to extract the metric value from an activity
139
  * @param dateRange - Optional date range to maintain consistency
140
+ * @param targetACWR - Target ACWR value for predictions
141
+ * @param predictToday - If true, include today in predictions; if false, predictions start tomorrow
142
  */
143
  export function calculateMetricACWR(
144
  activities: Activity[],
145
  metricExtractor: (activity: Activity) => number | undefined,
146
  dateRange?: { start: Date; end: Date },
147
+ targetACWR: number = 1.3,
148
+ predictToday: boolean = false
149
  ): MetricACWRData {
150
  if (activities.length === 0 && !dateRange) {
151
  return {
 
353
  todayValue: undefined,
354
  activitiesByDate,
355
  // Add future predictions if we have enough data
356
+ // Determine start offset based on predictToday and whether today has activities
357
+ ...(allDates.length >= 28 ? (() => {
358
+ const today = new Date();
359
+ const todayStr = formatDateLocal(today);
360
+ const lastDateStr = formatDateLocal(allDates[allDates.length - 1]);
361
+
362
+ // Check if today is the last date in our data and if it has any activities
363
+ const todayHasActivities = todayStr === lastDateStr && dailyValues.has(todayStr);
364
+
365
+ // startOffset determines which day to start predictions from relative to lastDate
366
+ // startOffset=0: predictions start from lastDate + 1 (next day after last data)
367
+ // startOffset=1: predictions start from lastDate + 2 (skip one day)
368
+ // predictToday unchecked: start from next day (offset 0)
369
+ // predictToday checked and today has activities: start from next day (offset 0)
370
+ // predictToday checked and today has no activities: start from next day (offset 0), but lastDate was yesterday
371
+ const startOffset = 0;
372
+
373
+ return calculateOptimalFutureDays(allDates, dailyValues, targetACWR, 7, startOffset);
374
+ })() : {}),
375
  };
376
  }