glutamatt HF Staff commited on
Commit
a586c52
·
verified ·
1 Parent(s): f63ce21
Files changed (4) hide show
  1. index.html +37 -0
  2. src/components/charts.ts +6 -10
  3. src/main.ts +10 -0
  4. src/style.css +123 -0
index.html CHANGED
@@ -12,6 +12,9 @@
12
  <div id="app">
13
  <header>
14
  <h1>Training Load Data Visualization</h1>
 
 
 
15
  </header>
16
 
17
  <main>
@@ -77,6 +80,40 @@
77
  </main>
78
  </div>
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  <script type="module" src="/src/main.ts"></script>
81
  </body>
82
 
 
12
  <div id="app">
13
  <header>
14
  <h1>Training Load Data Visualization</h1>
15
+ <button id="help-button" class="help-button" title="Help">
16
+ <span>?</span>
17
+ </button>
18
  </header>
19
 
20
  <main>
 
80
  </main>
81
  </div>
82
 
83
+ <div id="help-popover" class="help-popover hidden">
84
+ <div class="help-popover-content">
85
+ <button id="help-close" class="help-close">&times;</button>
86
+ <h2>📚 Glossary</h2>
87
+ <div class="help-section">
88
+ <h3>ACWR - Acute:Chronic Workload Ratio</h3>
89
+ <p>The ratio between your recent training load (acute: last 7 days) and your longer-term training load (chronic: last 28 days). Used to manage training progression and injury risk.</p>
90
+ <ul>
91
+ <li><strong>&lt; 0.8 (Blue):</strong> Detraining risk - you may be losing fitness</li>
92
+ <li><strong>0.8 - 1.3 (Green):</strong> Optimal zone - ideal training progression</li>
93
+ <li><strong>1.3 - 1.5 (Orange):</strong> Warning - pushing the limits</li>
94
+ <li><strong>&gt; 1.5 (Red):</strong> Injury risk - training load may be too high</li>
95
+ </ul>
96
+ </div>
97
+ <div class="help-section">
98
+ <h3>TSS - Training Stress Score</h3>
99
+ <p>A composite metric that quantifies the overall training stress of a workout, taking into account both intensity (Normalized Power) and duration.</p>
100
+ <p><strong>Formula:</strong> TSS = (Duration × NP × IF) / (FTP × 3600) × 100</p>
101
+ </div>
102
+ <div class="help-section">
103
+ <h3>FTP - Functional Threshold Power</h3>
104
+ <p>The highest average power you can sustain for approximately one hour, measured in watts. Used as a baseline for calculating training intensity and TSS.</p>
105
+ </div>
106
+ <div class="help-section">
107
+ <h3>NP - Normalized Power</h3>
108
+ <p>A power measurement that accounts for the variable nature of your effort during a workout, providing a more accurate representation of the physiological cost than average power.</p>
109
+ </div>
110
+ <div class="help-section">
111
+ <h3>IF - Intensity Factor</h3>
112
+ <p>The ratio of Normalized Power to FTP (IF = NP / FTP). Indicates how hard a workout was relative to your threshold.</p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
  <script type="module" src="/src/main.ts"></script>
118
  </body>
119
 
src/components/charts.ts CHANGED
@@ -43,12 +43,10 @@ const acwrGradientPlugin = {
43
  // Skip if both values are null
44
  if (value1 === null && value2 === null) continue;
45
 
46
- // Use the average color of the two points
47
- const avgValue = value1 !== null && value2 !== null
48
- ? (value1 + value2) / 2
49
- : (value1 ?? value2);
50
 
51
- const color = getACWRColor(avgValue);
52
  // Make the fill more transparent
53
  const fillColor = color.replace(/[\d.]+\)$/, '0.15)');
54
 
@@ -80,12 +78,10 @@ const acwrGradientPlugin = {
80
  // Skip if both values are null
81
  if (value1 === null && value2 === null) continue;
82
 
83
- // Use the average color of the two points
84
- const avgValue = value1 !== null && value2 !== null
85
- ? (value1 + value2) / 2
86
- : (value1 ?? value2);
87
 
88
- ctx.strokeStyle = getACWRColor(avgValue);
89
  ctx.beginPath();
90
  ctx.moveTo(point1.x, point1.y);
91
  ctx.lineTo(point2.x, point2.y);
 
43
  // Skip if both values are null
44
  if (value1 === null && value2 === null) continue;
45
 
46
+ // Use the second point value (most recent/rightward)
47
+ const segmentValue = value2 ?? value1;
 
 
48
 
49
+ const color = getACWRColor(segmentValue);
50
  // Make the fill more transparent
51
  const fillColor = color.replace(/[\d.]+\)$/, '0.15)');
52
 
 
78
  // Skip if both values are null
79
  if (value1 === null && value2 === null) continue;
80
 
81
+ // Use the second point value (most recent/rightward)
82
+ const segmentValue = value2 ?? value1;
 
 
83
 
84
+ ctx.strokeStyle = getACWRColor(segmentValue);
85
  ctx.beginPath();
86
  ctx.moveTo(point1.x, point1.y);
87
  ctx.lineTo(point2.x, point2.y);
src/main.ts CHANGED
@@ -16,6 +16,9 @@ const uploadStatus = document.getElementById('upload-status') as HTMLDivElement;
16
  const chartsSection = document.getElementById('charts-section') as HTMLElement;
17
  const filterSection = document.getElementById('filter-section') as HTMLElement;
18
  const filterContainer = document.getElementById('filter-container') as HTMLElement;
 
 
 
19
 
20
  // Store all activities globally
21
  let allActivities: Activity[] = [];
@@ -23,6 +26,13 @@ let selectedActivityTypes: Set<string> = new Set(['Running']);
23
 
24
  // Event listeners
25
  csvUpload?.addEventListener('change', handleFileUpload);
 
 
 
 
 
 
 
26
 
27
  function createActivityTypeFilters(activities: Activity[]): void {
28
  // Get unique activity types
 
16
  const chartsSection = document.getElementById('charts-section') as HTMLElement;
17
  const filterSection = document.getElementById('filter-section') as HTMLElement;
18
  const filterContainer = document.getElementById('filter-container') as HTMLElement;
19
+ const helpButton = document.getElementById('help-button') as HTMLButtonElement;
20
+ const helpPopover = document.getElementById('help-popover') as HTMLElement;
21
+ const helpClose = document.getElementById('help-close') as HTMLButtonElement;
22
 
23
  // Store all activities globally
24
  let allActivities: Activity[] = [];
 
26
 
27
  // Event listeners
28
  csvUpload?.addEventListener('change', handleFileUpload);
29
+ helpButton?.addEventListener('click', () => helpPopover?.classList.remove('hidden'));
30
+ helpClose?.addEventListener('click', () => helpPopover?.classList.add('hidden'));
31
+ helpPopover?.addEventListener('click', (e) => {
32
+ if (e.target === helpPopover) {
33
+ helpPopover.classList.add('hidden');
34
+ }
35
+ });
36
 
37
  function createActivityTypeFilters(activities: Activity[]): void {
38
  // Get unique activity types
src/style.css CHANGED
@@ -35,6 +35,9 @@ header {
35
  border-bottom: 1px solid var(--border-color);
36
  padding: 1.5rem 2rem;
37
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 
 
 
38
  }
39
 
40
  header h1 {
@@ -43,6 +46,126 @@ header h1 {
43
  font-weight: 700;
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  main {
47
  flex: 1;
48
  padding: 2rem;
 
35
  border-bottom: 1px solid var(--border-color);
36
  padding: 1.5rem 2rem;
37
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
  }
42
 
43
  header h1 {
 
46
  font-weight: 700;
47
  }
48
 
49
+ .help-button {
50
+ width: 36px;
51
+ height: 36px;
52
+ border-radius: 50%;
53
+ background-color: var(--primary-color);
54
+ color: white;
55
+ border: none;
56
+ font-size: 1.25rem;
57
+ font-weight: bold;
58
+ cursor: pointer;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ transition: background-color 0.2s, transform 0.2s;
63
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
64
+ }
65
+
66
+ .help-button:hover {
67
+ background-color: #1d4ed8;
68
+ transform: scale(1.05);
69
+ }
70
+
71
+ .help-popover {
72
+ position: fixed;
73
+ top: 0;
74
+ left: 0;
75
+ width: 100%;
76
+ height: 100%;
77
+ background-color: rgba(0, 0, 0, 0.5);
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ z-index: 1000;
82
+ padding: 1rem;
83
+ }
84
+
85
+ .help-popover.hidden {
86
+ display: none;
87
+ }
88
+
89
+ .help-popover-content {
90
+ background-color: var(--card-bg);
91
+ border-radius: 12px;
92
+ padding: 2rem;
93
+ max-width: 700px;
94
+ max-height: 90vh;
95
+ overflow-y: auto;
96
+ position: relative;
97
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
98
+ }
99
+
100
+ .help-close {
101
+ position: absolute;
102
+ top: 1rem;
103
+ right: 1rem;
104
+ background: none;
105
+ border: none;
106
+ font-size: 2rem;
107
+ color: var(--secondary-color);
108
+ cursor: pointer;
109
+ width: 32px;
110
+ height: 32px;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ border-radius: 4px;
115
+ transition: background-color 0.2s, color 0.2s;
116
+ }
117
+
118
+ .help-close:hover {
119
+ background-color: var(--bg-color);
120
+ color: var(--text-color);
121
+ }
122
+
123
+ .help-popover-content h2 {
124
+ margin-bottom: 1.5rem;
125
+ color: var(--primary-color);
126
+ font-size: 1.5rem;
127
+ }
128
+
129
+ .help-section {
130
+ margin-bottom: 1.5rem;
131
+ padding-bottom: 1.5rem;
132
+ border-bottom: 1px solid var(--border-color);
133
+ }
134
+
135
+ .help-section:last-child {
136
+ border-bottom: none;
137
+ margin-bottom: 0;
138
+ padding-bottom: 0;
139
+ }
140
+
141
+ .help-section h3 {
142
+ color: var(--text-color);
143
+ font-size: 1.125rem;
144
+ margin-bottom: 0.5rem;
145
+ font-weight: 600;
146
+ }
147
+
148
+ .help-section p {
149
+ color: var(--secondary-color);
150
+ line-height: 1.6;
151
+ margin-bottom: 0.75rem;
152
+ }
153
+
154
+ .help-section ul {
155
+ margin-left: 1.5rem;
156
+ color: var(--secondary-color);
157
+ }
158
+
159
+ .help-section li {
160
+ margin-bottom: 0.5rem;
161
+ line-height: 1.5;
162
+ }
163
+
164
+ .help-section strong {
165
+ color: var(--text-color);
166
+ font-weight: 600;
167
+ }
168
+
169
  main {
170
  flex: 1;
171
  padding: 2rem;