glutamatt HF Staff commited on
Commit
7f733b0
·
verified ·
1 Parent(s): d831612
index.html CHANGED
@@ -20,16 +20,19 @@
20
  </div>
21
  <div class="ftp-input-container">
22
  <label for="target-acwr-input">Target ACWR</label>
23
- <input type="number" id="target-acwr-input" value="1.3" min="0.5" max="3" step="0.1" placeholder="1.3" />
 
24
  </div>
25
  <div class="ftp-input-container">
26
  <label for="threshold-hr-input">Threshold HR</label>
27
- <input type="number" id="threshold-hr-input" value="170" min="100" max="220" step="1" placeholder="170" />
 
28
  <span class="ftp-unit">bpm</span>
29
  </div>
30
  <div class="ftp-input-container">
31
  <label for="resting-hr-input">Resting HR</label>
32
- <input type="number" id="resting-hr-input" value="50" min="30" max="100" step="1" placeholder="50" />
 
33
  <span class="ftp-unit">bpm</span>
34
  </div>
35
  <input type="file" id="csv-upload" accept=".csv" />
 
20
  </div>
21
  <div class="ftp-input-container">
22
  <label for="target-acwr-input">Target ACWR</label>
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"
29
+ placeholder="170" />
30
  <span class="ftp-unit">bpm</span>
31
  </div>
32
  <div class="ftp-input-container">
33
  <label for="resting-hr-input">Resting HR</label>
34
+ <input type="number" id="resting-hr-input" value="50" min="30" max="100" step="1"
35
+ placeholder="50" />
36
  <span class="ftp-unit">bpm</span>
37
  </div>
38
  <input type="file" id="csv-upload" accept=".csv" />
src/components/charts.ts CHANGED
@@ -115,110 +115,110 @@ function showActivityDetails(dateStr: string, activities: Activity[]): void {
115
  const popover = document.getElementById('activity-popover');
116
  const dateTitle = document.getElementById('activity-date-title');
117
  const activityList = document.getElementById('activity-list');
118
-
119
  if (!popover || !dateTitle || !activityList) return;
120
-
121
  // Format date
122
  const date = new Date(dateStr);
123
- const formattedDate = date.toLocaleDateString('en-US', {
124
- weekday: 'long',
125
- year: 'numeric',
126
- month: 'long',
127
- day: 'numeric'
128
  });
129
-
130
  dateTitle.textContent = formattedDate;
131
-
132
  // Clear previous activities
133
  activityList.innerHTML = '';
134
-
135
  // Add each activity
136
  activities.forEach(activity => {
137
  const activityItem = document.createElement('div');
138
  activityItem.className = 'activity-item';
139
-
140
  const displayName = activity.title || activity.activityType || 'Activity';
141
  const emoji = getActivityEmoji(activity.activityType || 'Activity');
142
-
143
  const header = document.createElement('div');
144
  header.className = 'activity-item-header';
145
  header.innerHTML = `<span>${emoji}</span><span>${displayName}</span>`;
146
-
147
  const details = document.createElement('div');
148
  details.className = 'activity-item-details';
149
-
150
  // Distance
151
  if (activity.distance !== undefined && activity.distance > 0) {
152
  const distanceDetail = document.createElement('div');
153
  distanceDetail.className = 'activity-detail';
154
  distanceDetail.innerHTML = `
155
- <span class="activity-detail-label">Distance</span>
156
  <span class="activity-detail-value">${activity.distance.toFixed(2)} km</span>
157
  `;
158
  details.appendChild(distanceDetail);
159
  }
160
-
161
  // Duration
162
  if (activity.duration !== undefined && activity.duration > 0) {
163
  const durationDetail = document.createElement('div');
164
  durationDetail.className = 'activity-detail';
165
  const durationMin = Math.round(activity.duration);
166
  durationDetail.innerHTML = `
167
- <span class="activity-detail-label">Duration</span>
168
  <span class="activity-detail-value">${durationMin} min</span>
169
  `;
170
  details.appendChild(durationDetail);
171
  }
172
-
173
  // TSS
174
  if (activity.trainingStressScore !== undefined && activity.trainingStressScore > 0) {
175
  const tssDetail = document.createElement('div');
176
  tssDetail.className = 'activity-detail';
177
  tssDetail.innerHTML = `
178
- <span class="activity-detail-label">TSS</span>
179
  <span class="activity-detail-value">${activity.trainingStressScore.toFixed(0)}</span>
180
  `;
181
  details.appendChild(tssDetail);
182
  }
183
-
184
  // Calories
185
  if (activity.calories !== undefined && activity.calories > 0) {
186
  const caloriesDetail = document.createElement('div');
187
  caloriesDetail.className = 'activity-detail';
188
  caloriesDetail.innerHTML = `
189
- <span class="activity-detail-label">Calories</span>
190
  <span class="activity-detail-value">${activity.calories.toFixed(0)} kcal</span>
191
  `;
192
  details.appendChild(caloriesDetail);
193
  }
194
-
195
  // Average HR
196
  if (activity.averageHR !== undefined && activity.averageHR > 0) {
197
  const avgHRDetail = document.createElement('div');
198
  avgHRDetail.className = 'activity-detail';
199
  avgHRDetail.innerHTML = `
200
- <span class="activity-detail-label">Avg HR</span>
201
  <span class="activity-detail-value">${activity.averageHR.toFixed(0)} bpm</span>
202
  `;
203
  details.appendChild(avgHRDetail);
204
  }
205
-
206
  // Max HR
207
  if (activity.maxHR !== undefined && activity.maxHR > 0) {
208
  const maxHRDetail = document.createElement('div');
209
  maxHRDetail.className = 'activity-detail';
210
  maxHRDetail.innerHTML = `
211
- <span class="activity-detail-label">Max HR</span>
212
  <span class="activity-detail-value">${activity.maxHR.toFixed(0)} bpm</span>
213
  `;
214
  details.appendChild(maxHRDetail);
215
  }
216
-
217
  activityItem.appendChild(header);
218
  activityItem.appendChild(details);
219
  activityList.appendChild(activityItem);
220
  });
221
-
222
  // Show popover
223
  popover.classList.remove('hidden');
224
  }
@@ -227,21 +227,21 @@ function showActivityDetails(dateStr: string, activities: Activity[]): void {
227
  function setupActivityPopoverHandlers(): void {
228
  const popover = document.getElementById('activity-popover');
229
  const closeButton = document.getElementById('activity-close');
230
-
231
  if (!popover || !closeButton) return;
232
-
233
  // Close button click
234
  closeButton.addEventListener('click', () => {
235
  popover.classList.add('hidden');
236
  });
237
-
238
  // Click outside to close
239
  popover.addEventListener('click', (e) => {
240
  if (e.target === popover) {
241
  popover.classList.add('hidden');
242
  }
243
  });
244
-
245
  // Escape key to close
246
  document.addEventListener('keydown', (e) => {
247
  if (e.key === 'Escape' && !popover.classList.contains('hidden')) {
@@ -383,12 +383,12 @@ function createDualAxisChart(
383
  if (elements.length > 0) {
384
  const element = elements[0];
385
  const datasetIndex = element.datasetIndex;
386
-
387
  // Only handle clicks on the scatter (daily values) dataset
388
  if (datasetIndex === 0) {
389
  const index = element.index;
390
  const dateStr = data.dates[index];
391
-
392
  // Get activities for this date
393
  if (data.activitiesByDate) {
394
  const activities = data.activitiesByDate.get(dateStr);
@@ -527,7 +527,7 @@ function updateTargetInfo(elementId: string, targetValue: number | null | undefi
527
 
528
  if (targetACWR !== undefined) {
529
  let html: string;
530
-
531
  if (targetValue === null || targetValue === undefined) {
532
  html = `<strong>💡 Target for tomorrow:</strong> <span style="color: var(--secondary-color);">Target ACWR of ${targetACWR} cannot be reached in one day</span>`;
533
  } else if (targetValue === 0) {
@@ -536,13 +536,13 @@ function updateTargetInfo(elementId: string, targetValue: number | null | undefi
536
  const formattedValue = targetValue.toFixed(1);
537
  html = `<strong>💡 Target for tomorrow:</strong> <span class="target-value">${formattedValue} ${unit}</span> <span style="color: var(--secondary-color);">to reach ACWR of ${targetACWR}</span>`;
538
  }
539
-
540
  // Add rest tomorrow information
541
  if (restTomorrowACWR !== null && restTomorrowACWR !== undefined) {
542
  const color = getACWRColor(restTomorrowACWR);
543
  html += `<br><strong>😴 Rest tomorrow:</strong> <span style="color: ${color};">ACWR ${restTomorrowACWR.toFixed(2)}</span>`;
544
  }
545
-
546
  element.innerHTML = html;
547
  element.classList.add('visible');
548
  } else {
 
115
  const popover = document.getElementById('activity-popover');
116
  const dateTitle = document.getElementById('activity-date-title');
117
  const activityList = document.getElementById('activity-list');
118
+
119
  if (!popover || !dateTitle || !activityList) return;
120
+
121
  // Format date
122
  const date = new Date(dateStr);
123
+ const formattedDate = date.toLocaleDateString('en-US', {
124
+ weekday: 'long',
125
+ year: 'numeric',
126
+ month: 'long',
127
+ day: 'numeric'
128
  });
129
+
130
  dateTitle.textContent = formattedDate;
131
+
132
  // Clear previous activities
133
  activityList.innerHTML = '';
134
+
135
  // Add each activity
136
  activities.forEach(activity => {
137
  const activityItem = document.createElement('div');
138
  activityItem.className = 'activity-item';
139
+
140
  const displayName = activity.title || activity.activityType || 'Activity';
141
  const emoji = getActivityEmoji(activity.activityType || 'Activity');
142
+
143
  const header = document.createElement('div');
144
  header.className = 'activity-item-header';
145
  header.innerHTML = `<span>${emoji}</span><span>${displayName}</span>`;
146
+
147
  const details = document.createElement('div');
148
  details.className = 'activity-item-details';
149
+
150
  // Distance
151
  if (activity.distance !== undefined && activity.distance > 0) {
152
  const distanceDetail = document.createElement('div');
153
  distanceDetail.className = 'activity-detail';
154
  distanceDetail.innerHTML = `
155
+ <span class="activity-detail-label">🗺️ Distance</span>
156
  <span class="activity-detail-value">${activity.distance.toFixed(2)} km</span>
157
  `;
158
  details.appendChild(distanceDetail);
159
  }
160
+
161
  // Duration
162
  if (activity.duration !== undefined && activity.duration > 0) {
163
  const durationDetail = document.createElement('div');
164
  durationDetail.className = 'activity-detail';
165
  const durationMin = Math.round(activity.duration);
166
  durationDetail.innerHTML = `
167
+ <span class="activity-detail-label">⏱️ Duration</span>
168
  <span class="activity-detail-value">${durationMin} min</span>
169
  `;
170
  details.appendChild(durationDetail);
171
  }
172
+
173
  // TSS
174
  if (activity.trainingStressScore !== undefined && activity.trainingStressScore > 0) {
175
  const tssDetail = document.createElement('div');
176
  tssDetail.className = 'activity-detail';
177
  tssDetail.innerHTML = `
178
+ <span class="activity-detail-label">🥵 TSS</span>
179
  <span class="activity-detail-value">${activity.trainingStressScore.toFixed(0)}</span>
180
  `;
181
  details.appendChild(tssDetail);
182
  }
183
+
184
  // Calories
185
  if (activity.calories !== undefined && activity.calories > 0) {
186
  const caloriesDetail = document.createElement('div');
187
  caloriesDetail.className = 'activity-detail';
188
  caloriesDetail.innerHTML = `
189
+ <span class="activity-detail-label">🔋 Calories</span>
190
  <span class="activity-detail-value">${activity.calories.toFixed(0)} kcal</span>
191
  `;
192
  details.appendChild(caloriesDetail);
193
  }
194
+
195
  // Average HR
196
  if (activity.averageHR !== undefined && activity.averageHR > 0) {
197
  const avgHRDetail = document.createElement('div');
198
  avgHRDetail.className = 'activity-detail';
199
  avgHRDetail.innerHTML = `
200
+ <span class="activity-detail-label">💚 Avg HR</span>
201
  <span class="activity-detail-value">${activity.averageHR.toFixed(0)} bpm</span>
202
  `;
203
  details.appendChild(avgHRDetail);
204
  }
205
+
206
  // Max HR
207
  if (activity.maxHR !== undefined && activity.maxHR > 0) {
208
  const maxHRDetail = document.createElement('div');
209
  maxHRDetail.className = 'activity-detail';
210
  maxHRDetail.innerHTML = `
211
+ <span class="activity-detail-label">❤️ Max HR</span>
212
  <span class="activity-detail-value">${activity.maxHR.toFixed(0)} bpm</span>
213
  `;
214
  details.appendChild(maxHRDetail);
215
  }
216
+
217
  activityItem.appendChild(header);
218
  activityItem.appendChild(details);
219
  activityList.appendChild(activityItem);
220
  });
221
+
222
  // Show popover
223
  popover.classList.remove('hidden');
224
  }
 
227
  function setupActivityPopoverHandlers(): void {
228
  const popover = document.getElementById('activity-popover');
229
  const closeButton = document.getElementById('activity-close');
230
+
231
  if (!popover || !closeButton) return;
232
+
233
  // Close button click
234
  closeButton.addEventListener('click', () => {
235
  popover.classList.add('hidden');
236
  });
237
+
238
  // Click outside to close
239
  popover.addEventListener('click', (e) => {
240
  if (e.target === popover) {
241
  popover.classList.add('hidden');
242
  }
243
  });
244
+
245
  // Escape key to close
246
  document.addEventListener('keydown', (e) => {
247
  if (e.key === 'Escape' && !popover.classList.contains('hidden')) {
 
383
  if (elements.length > 0) {
384
  const element = elements[0];
385
  const datasetIndex = element.datasetIndex;
386
+
387
  // Only handle clicks on the scatter (daily values) dataset
388
  if (datasetIndex === 0) {
389
  const index = element.index;
390
  const dateStr = data.dates[index];
391
+
392
  // Get activities for this date
393
  if (data.activitiesByDate) {
394
  const activities = data.activitiesByDate.get(dateStr);
 
527
 
528
  if (targetACWR !== undefined) {
529
  let html: string;
530
+
531
  if (targetValue === null || targetValue === undefined) {
532
  html = `<strong>💡 Target for tomorrow:</strong> <span style="color: var(--secondary-color);">Target ACWR of ${targetACWR} cannot be reached in one day</span>`;
533
  } else if (targetValue === 0) {
 
536
  const formattedValue = targetValue.toFixed(1);
537
  html = `<strong>💡 Target for tomorrow:</strong> <span class="target-value">${formattedValue} ${unit}</span> <span style="color: var(--secondary-color);">to reach ACWR of ${targetACWR}</span>`;
538
  }
539
+
540
  // Add rest tomorrow information
541
  if (restTomorrowACWR !== null && restTomorrowACWR !== undefined) {
542
  const color = getACWRColor(restTomorrowACWR);
543
  html += `<br><strong>😴 Rest tomorrow:</strong> <span style="color: ${color};">ACWR ${restTomorrowACWR.toFixed(2)}</span>`;
544
  }
545
+
546
  element.innerHTML = html;
547
  element.classList.add('visible');
548
  } else {
src/main.ts CHANGED
@@ -113,7 +113,7 @@ function handleFilterChange(): void {
113
  const filteredActivities = selectedActivityTypes.size > 0
114
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
115
  : allActivities;
116
-
117
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
118
  renderCharts(filteredActivities, targetAcwr);
119
  }
 
113
  const filteredActivities = selectedActivityTypes.size > 0
114
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
115
  : allActivities;
116
+
117
  const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
118
  renderCharts(filteredActivities, targetAcwr);
119
  }
src/style.css CHANGED
@@ -197,7 +197,8 @@ header h1 {
197
  background-color: var(--card-bg);
198
  border-radius: 12px;
199
  padding: 2rem;
200
- max-width: 800px;
 
201
  max-height: 90vh;
202
  overflow-y: auto;
203
  position: relative;
@@ -219,7 +220,7 @@ header h1 {
219
  .activity-item {
220
  background-color: var(--bg-color);
221
  border-radius: 8px;
222
- padding: 1rem;
223
  border-left: 4px solid var(--primary-color);
224
  }
225
 
@@ -235,14 +236,15 @@ header h1 {
235
 
236
  .activity-item-details {
237
  display: grid;
238
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
239
- gap: 0.5rem;
240
  font-size: 0.875rem;
241
  }
242
 
243
  .activity-detail {
244
  display: flex;
245
  flex-direction: column;
 
246
  }
247
 
248
  .activity-detail-label {
@@ -255,7 +257,7 @@ header h1 {
255
  .activity-detail-value {
256
  color: var(--text-color);
257
  font-weight: 600;
258
- font-size: 1rem;
259
  }
260
 
261
  main {
 
197
  background-color: var(--card-bg);
198
  border-radius: 12px;
199
  padding: 2rem;
200
+ width: 90%;
201
+ max-width: 1200px;
202
  max-height: 90vh;
203
  overflow-y: auto;
204
  position: relative;
 
220
  .activity-item {
221
  background-color: var(--bg-color);
222
  border-radius: 8px;
223
+ padding: 1.25rem;
224
  border-left: 4px solid var(--primary-color);
225
  }
226
 
 
236
 
237
  .activity-item-details {
238
  display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
240
+ gap: 1rem;
241
  font-size: 0.875rem;
242
  }
243
 
244
  .activity-detail {
245
  display: flex;
246
  flex-direction: column;
247
+ gap: 0.25rem;
248
  }
249
 
250
  .activity-detail-label {
 
257
  .activity-detail-value {
258
  color: var(--text-color);
259
  font-weight: 600;
260
+ font-size: 1.125rem;
261
  }
262
 
263
  main {
src/utils/metricAcwr.ts CHANGED
@@ -43,14 +43,14 @@ export function calculateMetricACWR(
43
  const dailyValues = new Map<string, number>();
44
  // Create a map of date -> activities
45
  const activitiesByDate = new Map<string, Activity[]>();
46
-
47
  sortedActivities.forEach(activity => {
48
  const dateStr = formatDateLocal(activity.date);
49
  const value = metricExtractor(activity);
50
  if (value !== undefined) {
51
  dailyValues.set(dateStr, (dailyValues.get(dateStr) || 0) + value);
52
  }
53
-
54
  // Group activities by date
55
  if (!activitiesByDate.has(dateStr)) {
56
  activitiesByDate.set(dateStr, []);
 
43
  const dailyValues = new Map<string, number>();
44
  // Create a map of date -> activities
45
  const activitiesByDate = new Map<string, Activity[]>();
46
+
47
  sortedActivities.forEach(activity => {
48
  const dateStr = formatDateLocal(activity.date);
49
  const value = metricExtractor(activity);
50
  if (value !== undefined) {
51
  dailyValues.set(dateStr, (dailyValues.get(dateStr) || 0) + value);
52
  }
53
+
54
  // Group activities by date
55
  if (!activitiesByDate.has(dateStr)) {
56
  activitiesByDate.set(dateStr, []);