glutamatt HF Staff commited on
Commit
51450d9
·
verified ·
1 Parent(s): 564e0d4

local storage, grid line per monday

Browse files
Files changed (2) hide show
  1. src/components/charts.ts +43 -3
  2. src/main.ts +134 -2
src/components/charts.ts CHANGED
@@ -164,6 +164,48 @@ const acwrAxisColorPlugin = {
164
  // Register the axis color plugin
165
  Chart.register(acwrAxisColorPlugin);
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  // Function to get activity emoji based on type
168
  function getActivityEmoji(activityType: string): string {
169
  const type = activityType.toLowerCase();
@@ -355,9 +397,7 @@ const commonOptions = {
355
  scales: {
356
  x: {
357
  grid: {
358
- display: true,
359
- color: 'rgba(148, 163, 184, 0.15)',
360
- lineWidth: 1,
361
  drawBorder: false,
362
  },
363
  ticks: {
 
164
  // Register the axis color plugin
165
  Chart.register(acwrAxisColorPlugin);
166
 
167
+ // Plugin to draw Monday vertical lines
168
+ const mondayGridPlugin = {
169
+ id: 'mondayGrid',
170
+ beforeDatasetsDraw(chart: Chart) {
171
+ const ctx = chart.ctx;
172
+ const chartArea = chart.chartArea;
173
+ const xScale = chart.scales['x'];
174
+
175
+ if (!xScale || !chartArea) return;
176
+
177
+ ctx.save();
178
+ ctx.strokeStyle = 'rgba(148, 163, 184, 0.3)';
179
+ ctx.lineWidth = 1;
180
+
181
+ // Draw a line for each Monday in the data
182
+ const labels = chart.data.labels || [];
183
+ labels.forEach((label, index) => {
184
+ if (typeof label === 'string') {
185
+ const [year, month, day] = label.split('-').map(Number);
186
+ const date = new Date(year, month - 1, day);
187
+ const dayOfWeek = date.getDay();
188
+
189
+ // If it's Monday, draw a vertical line
190
+ if (dayOfWeek === 1) {
191
+ const x = xScale.getPixelForValue(index);
192
+ if (x >= chartArea.left && x <= chartArea.right) {
193
+ ctx.beginPath();
194
+ ctx.moveTo(x, chartArea.top);
195
+ ctx.lineTo(x, chartArea.bottom);
196
+ ctx.stroke();
197
+ }
198
+ }
199
+ }
200
+ });
201
+
202
+ ctx.restore();
203
+ },
204
+ };
205
+
206
+ // Register the Monday grid plugin
207
+ Chart.register(mondayGridPlugin);
208
+
209
  // Function to get activity emoji based on type
210
  function getActivityEmoji(activityType: string): string {
211
  const type = activityType.toLowerCase();
 
397
  scales: {
398
  x: {
399
  grid: {
400
+ display: false, // Disable default grid, we draw Monday lines with plugin
 
 
401
  drawBorder: false,
402
  },
403
  ticks: {
src/main.ts CHANGED
@@ -29,10 +29,124 @@ const helpClose = document.getElementById('help-close') as HTMLButtonElement;
29
  let allActivities: Activity[] = [];
30
  let selectedActivityTypes: Set<string> = new Set(['Running']);
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) => {
@@ -45,6 +159,9 @@ function handleTargetAcwrChange(): void {
45
  // Only refresh if we have activities loaded
46
  if (allActivities.length === 0) return;
47
 
 
 
 
48
  // Filter and render with new target ACWR
49
  const filteredActivities = selectedActivityTypes.size > 0
50
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
@@ -59,6 +176,9 @@ 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 || ''))
@@ -140,6 +260,9 @@ function handleFilterChange(): void {
140
  }
141
  });
142
 
 
 
 
143
  // Filter and render
144
  const filteredActivities = selectedActivityTypes.size > 0
145
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
@@ -167,6 +290,9 @@ async function handleFileUpload(event: Event): Promise<void> {
167
  const thresholdHR = parseInt(thresholdHrInput.value) || 170;
168
  const restingHR = parseInt(restingHrInput.value) || 50;
169
 
 
 
 
170
  // Parse CSV with user-provided FTP and HR values
171
  allActivities = await parseCSV(file, ftp, thresholdHR, restingHR);
172
 
@@ -174,6 +300,10 @@ async function handleFileUpload(event: Event): Promise<void> {
174
  throw new Error('No valid activities found in the CSV file');
175
  }
176
 
 
 
 
 
177
  // Create dynamic activity type filters
178
  createActivityTypeFilters(allActivities);
179
 
@@ -208,14 +338,14 @@ function renderCharts(activities: Activity[], targetAcwr: number, predictToday:
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
-
212
  // Check if today has activities
213
  const todayHasActivities = activities.some(activity => {
214
  const activityDate = new Date(activity.date);
215
  activityDate.setHours(0, 0, 0, 0);
216
  return activityDate.getTime() === today.getTime();
217
  });
218
-
219
  // If predictToday is checked and today has no activities, end range at yesterday
220
  // This prevents today from appearing twice (once as null, once as prediction)
221
  const endDate = new Date();
@@ -273,4 +403,6 @@ function renderCharts(activities: Activity[], targetAcwr: number, predictToday:
273
  }
274
 
275
  // Initialize
 
 
276
  console.log('Training Load Data Visualization initialized');
 
29
  let allActivities: Activity[] = [];
30
  let selectedActivityTypes: Set<string> = new Set(['Running']);
31
 
32
+ // Local storage keys
33
+ const STORAGE_KEYS = {
34
+ CSV_DATA: 'trainingload_csv_data',
35
+ FTP: 'trainingload_ftp',
36
+ THRESHOLD_HR: 'trainingload_threshold_hr',
37
+ RESTING_HR: 'trainingload_resting_hr',
38
+ TARGET_ACWR: 'trainingload_target_acwr',
39
+ PREDICT_TODAY: 'trainingload_predict_today',
40
+ SELECTED_FILTERS: 'trainingload_selected_filters',
41
+ };
42
+
43
+ // Load saved settings on startup
44
+ function loadSavedSettings(): void {
45
+ // Load FTP
46
+ const savedFtp = localStorage.getItem(STORAGE_KEYS.FTP);
47
+ if (savedFtp && ftpInput) {
48
+ ftpInput.value = savedFtp;
49
+ }
50
+
51
+ // Load Threshold HR
52
+ const savedThresholdHr = localStorage.getItem(STORAGE_KEYS.THRESHOLD_HR);
53
+ if (savedThresholdHr && thresholdHrInput) {
54
+ thresholdHrInput.value = savedThresholdHr;
55
+ }
56
+
57
+ // Load Resting HR
58
+ const savedRestingHr = localStorage.getItem(STORAGE_KEYS.RESTING_HR);
59
+ if (savedRestingHr && restingHrInput) {
60
+ restingHrInput.value = savedRestingHr;
61
+ }
62
+
63
+ // Load Target ACWR
64
+ const savedTargetAcwr = localStorage.getItem(STORAGE_KEYS.TARGET_ACWR);
65
+ if (savedTargetAcwr && targetAcwrInput) {
66
+ targetAcwrInput.value = savedTargetAcwr;
67
+ }
68
+
69
+ // Load Predict Today
70
+ const savedPredictToday = localStorage.getItem(STORAGE_KEYS.PREDICT_TODAY);
71
+ if (savedPredictToday && predictTodayInput) {
72
+ predictTodayInput.checked = savedPredictToday === 'true';
73
+ }
74
+
75
+ // Load Selected Filters
76
+ const savedFilters = localStorage.getItem(STORAGE_KEYS.SELECTED_FILTERS);
77
+ if (savedFilters) {
78
+ try {
79
+ const filters = JSON.parse(savedFilters);
80
+ selectedActivityTypes = new Set(filters);
81
+ } catch (e) {
82
+ console.error('Failed to parse saved filters', e);
83
+ }
84
+ }
85
+ }
86
+
87
+ // Save settings to local storage
88
+ function saveSettings(): void {
89
+ if (ftpInput) localStorage.setItem(STORAGE_KEYS.FTP, ftpInput.value);
90
+ if (thresholdHrInput) localStorage.setItem(STORAGE_KEYS.THRESHOLD_HR, thresholdHrInput.value);
91
+ if (restingHrInput) localStorage.setItem(STORAGE_KEYS.RESTING_HR, restingHrInput.value);
92
+ if (targetAcwrInput) localStorage.setItem(STORAGE_KEYS.TARGET_ACWR, targetAcwrInput.value);
93
+ if (predictTodayInput) localStorage.setItem(STORAGE_KEYS.PREDICT_TODAY, String(predictTodayInput.checked));
94
+ localStorage.setItem(STORAGE_KEYS.SELECTED_FILTERS, JSON.stringify(Array.from(selectedActivityTypes)));
95
+ }
96
+
97
+ // Load CSV from local storage
98
+ async function loadSavedCSV(): Promise<void> {
99
+ const savedCSV = localStorage.getItem(STORAGE_KEYS.CSV_DATA);
100
+ if (!savedCSV) return;
101
+
102
+ uploadStatus.textContent = 'Loading saved data...';
103
+ uploadStatus.className = '';
104
+
105
+ try {
106
+ const ftp = parseInt(ftpInput.value) || 343;
107
+ const thresholdHR = parseInt(thresholdHrInput.value) || 170;
108
+ const restingHR = parseInt(restingHrInput.value) || 50;
109
+
110
+ // Create a Blob from the saved CSV string
111
+ const blob = new Blob([savedCSV], { type: 'text/csv' });
112
+ const file = new File([blob], 'saved_activities.csv', { type: 'text/csv' });
113
+
114
+ allActivities = await parseCSV(file, ftp, thresholdHR, restingHR);
115
+
116
+ if (allActivities.length === 0) {
117
+ throw new Error('No valid activities found in saved data');
118
+ }
119
+
120
+ createActivityTypeFilters(allActivities);
121
+
122
+ const filteredActivities = selectedActivityTypes.size > 0
123
+ ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
124
+ : allActivities;
125
+ const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3;
126
+ const predictToday = predictTodayInput?.checked || false;
127
+ renderCharts(filteredActivities, targetAcwr, predictToday);
128
+
129
+ filterSection.classList.remove('hidden');
130
+ chartsSection.classList.remove('hidden');
131
+
132
+ uploadStatus.textContent = `Successfully loaded ${allActivities.length} activities from saved data`;
133
+ uploadStatus.className = 'success';
134
+ } catch (error) {
135
+ console.error('Error loading saved data:', error);
136
+ // Clear corrupted data
137
+ localStorage.removeItem(STORAGE_KEYS.CSV_DATA);
138
+ uploadStatus.textContent = '';
139
+ uploadStatus.className = '';
140
+ }
141
+ }
142
+
143
  // Event listeners
144
  csvUpload?.addEventListener('change', handleFileUpload);
145
  targetAcwrInput?.addEventListener('input', handleTargetAcwrChange);
146
  predictTodayInput?.addEventListener('change', handlePredictTodayChange);
147
+ ftpInput?.addEventListener('input', saveSettings);
148
+ thresholdHrInput?.addEventListener('input', saveSettings);
149
+ restingHrInput?.addEventListener('input', saveSettings);
150
  helpButton?.addEventListener('click', () => helpPopover?.classList.remove('hidden'));
151
  helpClose?.addEventListener('click', () => helpPopover?.classList.add('hidden'));
152
  helpPopover?.addEventListener('click', (e) => {
 
159
  // Only refresh if we have activities loaded
160
  if (allActivities.length === 0) return;
161
 
162
+ // Save to local storage
163
+ saveSettings();
164
+
165
  // Filter and render with new target ACWR
166
  const filteredActivities = selectedActivityTypes.size > 0
167
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
 
176
  // Only refresh if we have activities loaded
177
  if (allActivities.length === 0) return;
178
 
179
+ // Save to local storage
180
+ saveSettings();
181
+
182
  // Filter and render with new predict today setting
183
  const filteredActivities = selectedActivityTypes.size > 0
184
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
 
260
  }
261
  });
262
 
263
+ // Save to local storage
264
+ saveSettings();
265
+
266
  // Filter and render
267
  const filteredActivities = selectedActivityTypes.size > 0
268
  ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || ''))
 
290
  const thresholdHR = parseInt(thresholdHrInput.value) || 170;
291
  const restingHR = parseInt(restingHrInput.value) || 50;
292
 
293
+ // Read file content to save it
294
+ const csvContent = await file.text();
295
+
296
  // Parse CSV with user-provided FTP and HR values
297
  allActivities = await parseCSV(file, ftp, thresholdHR, restingHR);
298
 
 
300
  throw new Error('No valid activities found in the CSV file');
301
  }
302
 
303
+ // Save CSV content and settings to local storage
304
+ localStorage.setItem(STORAGE_KEYS.CSV_DATA, csvContent);
305
+ saveSettings();
306
+
307
  // Create dynamic activity type filters
308
  createActivityTypeFilters(allActivities);
309
 
 
338
  // Determine end date based on whether we'll include today in predictions
339
  const today = new Date();
340
  today.setHours(0, 0, 0, 0);
341
+
342
  // Check if today has activities
343
  const todayHasActivities = activities.some(activity => {
344
  const activityDate = new Date(activity.date);
345
  activityDate.setHours(0, 0, 0, 0);
346
  return activityDate.getTime() === today.getTime();
347
  });
348
+
349
  // If predictToday is checked and today has no activities, end range at yesterday
350
  // This prevents today from appearing twice (once as null, once as prediction)
351
  const endDate = new Date();
 
403
  }
404
 
405
  // Initialize
406
+ loadSavedSettings();
407
+ loadSavedCSV();
408
  console.log('Training Load Data Visualization initialized');