Spaces:
Running
Running
ui
Browse files- index.html +6 -3
- src/components/charts.ts +36 -36
- src/main.ts +1 -1
- src/style.css +7 -5
- src/utils/metricAcwr.ts +2 -2
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"
|
|
|
|
| 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="
|
|
|
|
| 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"
|
|
|
|
| 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"
|
| 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"
|
| 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"
|
| 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"
|
| 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"
|
| 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"
|
| 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 |
-
|
|
|
|
| 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:
|
| 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(
|
| 239 |
-
gap:
|
| 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:
|
| 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, []);
|