GameSentenceMiner 2.18.14__py3-none-any.whl → 2.18.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/anki.py +8 -53
- GameSentenceMiner/obs.py +1 -2
- GameSentenceMiner/ui/anki_confirmation.py +16 -2
- GameSentenceMiner/util/db.py +11 -7
- GameSentenceMiner/util/games_table.py +320 -0
- GameSentenceMiner/vad.py +3 -3
- GameSentenceMiner/web/anki_api_endpoints.py +506 -0
- GameSentenceMiner/web/database_api.py +239 -117
- GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
- GameSentenceMiner/web/static/css/search.css +54 -0
- GameSentenceMiner/web/static/css/stats.css +76 -0
- GameSentenceMiner/web/static/js/anki_stats.js +304 -50
- GameSentenceMiner/web/static/js/database.js +44 -7
- GameSentenceMiner/web/static/js/heatmap.js +326 -0
- GameSentenceMiner/web/static/js/overview.js +20 -224
- GameSentenceMiner/web/static/js/search.js +190 -23
- GameSentenceMiner/web/static/js/stats.js +371 -1
- GameSentenceMiner/web/stats.py +188 -0
- GameSentenceMiner/web/templates/anki_stats.html +145 -58
- GameSentenceMiner/web/templates/components/date-range.html +19 -0
- GameSentenceMiner/web/templates/components/html-head.html +45 -0
- GameSentenceMiner/web/templates/components/js-config.html +37 -0
- GameSentenceMiner/web/templates/components/popups.html +15 -0
- GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
- GameSentenceMiner/web/templates/database.html +13 -3
- GameSentenceMiner/web/templates/goals.html +9 -31
- GameSentenceMiner/web/templates/overview.html +16 -223
- GameSentenceMiner/web/templates/search.html +46 -0
- GameSentenceMiner/web/templates/stats.html +49 -311
- GameSentenceMiner/web/texthooking_page.py +4 -66
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/RECORD +36 -27
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Heatmap Component for GSM
|
|
3
|
+
* Provides reusable GitHub-style heatmap visualization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class HeatmapRenderer {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.containerId = options.containerId || 'heatmapContainer';
|
|
9
|
+
this.metricName = options.metricName || 'characters';
|
|
10
|
+
this.metricLabel = options.metricLabel || 'characters';
|
|
11
|
+
this.calculateStreaks = options.calculateStreaks || this.defaultCalculateStreaks.bind(this);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper function to get week number of year (GitHub style - week starts on Sunday)
|
|
16
|
+
*/
|
|
17
|
+
getWeekOfYear(date) {
|
|
18
|
+
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
19
|
+
const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
|
|
20
|
+
const dayOfWeek = yearStart.getDay(); // 0 = Sunday
|
|
21
|
+
|
|
22
|
+
// Calculate week number (1-indexed)
|
|
23
|
+
const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
|
|
24
|
+
return Math.min(53, weekNum); // Cap at 53 weeks
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Helper function to get the first Sunday of the year (or before)
|
|
29
|
+
*/
|
|
30
|
+
getFirstSunday(year) {
|
|
31
|
+
const jan1 = new Date(year, 0, 1);
|
|
32
|
+
const dayOfWeek = jan1.getDay();
|
|
33
|
+
const firstSunday = new Date(year, 0, 1 - dayOfWeek);
|
|
34
|
+
return firstSunday;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default streak calculation function
|
|
39
|
+
*/
|
|
40
|
+
defaultCalculateStreaks(grid, yearData, allLinesForYear) {
|
|
41
|
+
const dates = [];
|
|
42
|
+
|
|
43
|
+
// Collect all dates in chronological order
|
|
44
|
+
for (let week = 0; week < 53; week++) {
|
|
45
|
+
for (let day = 0; day < 7; day++) {
|
|
46
|
+
const date = grid[day][week];
|
|
47
|
+
if (date) {
|
|
48
|
+
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
49
|
+
const activity = yearData[dateStr] || 0;
|
|
50
|
+
dates.push({ date: dateStr, activity: activity });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Sort dates chronologically
|
|
56
|
+
dates.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
57
|
+
|
|
58
|
+
let longestStreak = 0;
|
|
59
|
+
let tempStreak = 0;
|
|
60
|
+
|
|
61
|
+
// Calculate longest streak
|
|
62
|
+
for (let i = 0; i < dates.length; i++) {
|
|
63
|
+
if (dates[i].activity > 0) {
|
|
64
|
+
tempStreak++;
|
|
65
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
66
|
+
} else {
|
|
67
|
+
tempStreak = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Calculate current streak from today backwards
|
|
72
|
+
const date = new Date();
|
|
73
|
+
const today = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
74
|
+
const streakRequirement = window.statsConfig ? window.statsConfig.streakRequirementHours : 1.0;
|
|
75
|
+
|
|
76
|
+
// Find today's index or the most recent date before today
|
|
77
|
+
let todayIndex = -1;
|
|
78
|
+
for (let i = dates.length - 1; i >= 0; i--) {
|
|
79
|
+
if (dates[i].date <= today) {
|
|
80
|
+
todayIndex = i;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Count backwards from today (or most recent date)
|
|
86
|
+
let currentStreak = 0;
|
|
87
|
+
if (todayIndex >= 0) {
|
|
88
|
+
for (let i = todayIndex; i >= 0; i--) {
|
|
89
|
+
if (dates[i].activity >= streakRequirement) {
|
|
90
|
+
currentStreak++;
|
|
91
|
+
} else {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate average metric (e.g., avg daily characters or avg daily mining)
|
|
98
|
+
let totalActivity = 0;
|
|
99
|
+
let activeDays = 0;
|
|
100
|
+
for (let i = 0; i < dates.length; i++) {
|
|
101
|
+
if (dates[i].activity > 0) {
|
|
102
|
+
totalActivity += dates[i].activity;
|
|
103
|
+
activeDays++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const avgDaily = activeDays > 0 ? Math.round(totalActivity / activeDays) : 0;
|
|
107
|
+
|
|
108
|
+
return { longestStreak, currentStreak, avgDaily };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create GitHub-style heatmap visualization
|
|
113
|
+
* @param {Object} heatmapData - Object with year keys containing date->value mappings
|
|
114
|
+
* @param {Array} allLinesData - Optional array of all line data for detailed calculations
|
|
115
|
+
*/
|
|
116
|
+
render(heatmapData, allLinesData = []) {
|
|
117
|
+
const container = document.getElementById(this.containerId);
|
|
118
|
+
if (!container) {
|
|
119
|
+
console.error(`Heatmap container #${this.containerId} not found`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
container.innerHTML = ''; // Clear existing content
|
|
124
|
+
|
|
125
|
+
if (!heatmapData || Object.keys(heatmapData).length === 0) {
|
|
126
|
+
container.innerHTML = `<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">No ${this.metricLabel} data available for the selected date range.</p>`;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Object.keys(heatmapData).sort().forEach(year => {
|
|
131
|
+
const yearData = heatmapData[year];
|
|
132
|
+
const yearDiv = document.createElement('div');
|
|
133
|
+
yearDiv.className = 'heatmap-year';
|
|
134
|
+
|
|
135
|
+
const yearTitle = document.createElement('h3');
|
|
136
|
+
yearTitle.textContent = year;
|
|
137
|
+
yearDiv.appendChild(yearTitle);
|
|
138
|
+
|
|
139
|
+
// Find maximum activity value for this year to scale colors
|
|
140
|
+
const maxActivity = Math.max(...Object.values(yearData));
|
|
141
|
+
|
|
142
|
+
// Create main wrapper to center everything
|
|
143
|
+
const mainWrapper = document.createElement('div');
|
|
144
|
+
mainWrapper.className = 'heatmap-wrapper';
|
|
145
|
+
|
|
146
|
+
// Create container wrapper for labels and grid
|
|
147
|
+
const containerWrapper = document.createElement('div');
|
|
148
|
+
containerWrapper.className = 'heatmap-container-wrapper';
|
|
149
|
+
|
|
150
|
+
// Create day labels (S, M, T, W, T, F, S)
|
|
151
|
+
const dayLabels = document.createElement('div');
|
|
152
|
+
dayLabels.className = 'heatmap-day-labels';
|
|
153
|
+
const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
|
|
154
|
+
dayNames.forEach(dayName => {
|
|
155
|
+
const dayLabel = document.createElement('div');
|
|
156
|
+
dayLabel.className = 'heatmap-day-label';
|
|
157
|
+
dayLabel.textContent = dayName;
|
|
158
|
+
dayLabels.appendChild(dayLabel);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Create grid container
|
|
162
|
+
const gridContainer = document.createElement('div');
|
|
163
|
+
|
|
164
|
+
// Create month labels
|
|
165
|
+
const monthLabels = document.createElement('div');
|
|
166
|
+
monthLabels.className = 'heatmap-month-labels';
|
|
167
|
+
|
|
168
|
+
// Create the main grid
|
|
169
|
+
const gridDiv = document.createElement('div');
|
|
170
|
+
gridDiv.className = 'heatmap-grid';
|
|
171
|
+
|
|
172
|
+
// Initialize 7x53 grid with empty cells
|
|
173
|
+
const grid = Array(7).fill(null).map(() => Array(53).fill(null));
|
|
174
|
+
|
|
175
|
+
// Get the first Sunday of the year (start of week 1)
|
|
176
|
+
const firstSunday = this.getFirstSunday(parseInt(year));
|
|
177
|
+
|
|
178
|
+
// Populate grid with dates for the entire year
|
|
179
|
+
for (let week = 0; week < 53; week++) {
|
|
180
|
+
for (let day = 0; day < 7; day++) {
|
|
181
|
+
const currentDate = new Date(firstSunday);
|
|
182
|
+
currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
|
|
183
|
+
|
|
184
|
+
// Only include dates that belong to the current year
|
|
185
|
+
if (currentDate.getFullYear() === parseInt(year)) {
|
|
186
|
+
grid[day][week] = currentDate;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create month labels based on grid positions
|
|
192
|
+
const monthTracker = new Set();
|
|
193
|
+
for (let week = 0; week < 53; week++) {
|
|
194
|
+
const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
|
|
195
|
+
grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
|
|
196
|
+
|
|
197
|
+
if (dateInWeek) {
|
|
198
|
+
const month = dateInWeek.getMonth();
|
|
199
|
+
const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
|
|
200
|
+
|
|
201
|
+
// Add month label if it's the first week of the month
|
|
202
|
+
if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
|
|
203
|
+
const monthLabel = document.createElement('div');
|
|
204
|
+
monthLabel.className = 'heatmap-month-label';
|
|
205
|
+
monthLabel.style.gridColumn = `${week + 1}`;
|
|
206
|
+
monthLabel.textContent = monthName;
|
|
207
|
+
monthLabels.appendChild(monthLabel);
|
|
208
|
+
monthTracker.add(month);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Create cells for the grid
|
|
214
|
+
for (let day = 0; day < 7; day++) {
|
|
215
|
+
for (let week = 0; week < 53; week++) {
|
|
216
|
+
const cell = document.createElement('div');
|
|
217
|
+
cell.className = 'heatmap-cell';
|
|
218
|
+
|
|
219
|
+
const date = grid[day][week];
|
|
220
|
+
if (date) {
|
|
221
|
+
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
222
|
+
const activity = yearData[dateStr] || 0;
|
|
223
|
+
|
|
224
|
+
if (activity > 0 && maxActivity > 0) {
|
|
225
|
+
// Calculate percentage of maximum activity
|
|
226
|
+
const percentage = (activity / maxActivity) * 100;
|
|
227
|
+
|
|
228
|
+
// Assign discrete color levels based on percentage thresholds
|
|
229
|
+
let colorLevel;
|
|
230
|
+
if (percentage <= 25) {
|
|
231
|
+
colorLevel = 1; // Light green
|
|
232
|
+
} else if (percentage <= 50) {
|
|
233
|
+
colorLevel = 2; // Medium green
|
|
234
|
+
} else if (percentage <= 75) {
|
|
235
|
+
colorLevel = 3; // Dark green
|
|
236
|
+
} else {
|
|
237
|
+
colorLevel = 4; // Darkest green
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Define discrete colors for each level
|
|
241
|
+
const colors = {
|
|
242
|
+
1: '#c6e48b', // Light green (1-25%)
|
|
243
|
+
2: '#7bc96f', // Medium green (26-50%)
|
|
244
|
+
3: '#239a3b', // Dark green (51-75%)
|
|
245
|
+
4: '#196127' // Darkest green (76-100%)
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
cell.style.backgroundColor = colors[colorLevel];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Format tooltip based on metric type
|
|
252
|
+
const activityLabel = this.metricName === 'sentences'
|
|
253
|
+
? `sentence${activity !== 1 ? 's' : ''} mined`
|
|
254
|
+
: `${this.metricLabel}`;
|
|
255
|
+
cell.title = `${dateStr}: ${activity} ${activityLabel}`;
|
|
256
|
+
} else {
|
|
257
|
+
// Empty cell for dates outside the year
|
|
258
|
+
cell.style.backgroundColor = 'transparent';
|
|
259
|
+
cell.style.cursor = 'default';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
gridDiv.appendChild(cell);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
gridContainer.appendChild(monthLabels);
|
|
267
|
+
gridContainer.appendChild(gridDiv);
|
|
268
|
+
containerWrapper.appendChild(dayLabels);
|
|
269
|
+
containerWrapper.appendChild(gridContainer);
|
|
270
|
+
mainWrapper.appendChild(containerWrapper);
|
|
271
|
+
|
|
272
|
+
// Filter allLinesData for this specific year
|
|
273
|
+
const yearLines = allLinesData ? allLinesData.filter(line => {
|
|
274
|
+
if (!line.timestamp) return false;
|
|
275
|
+
const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
|
|
276
|
+
return lineYear === parseInt(year);
|
|
277
|
+
}) : [];
|
|
278
|
+
|
|
279
|
+
// Calculate and display streaks with year-specific data
|
|
280
|
+
const streaks = this.calculateStreaks(grid, yearData, yearLines);
|
|
281
|
+
const streaksDiv = document.createElement('div');
|
|
282
|
+
streaksDiv.className = 'heatmap-streaks';
|
|
283
|
+
|
|
284
|
+
// Format the third metric label based on type
|
|
285
|
+
const thirdMetricLabel = this.metricName === 'sentences' ? 'Avg Daily Mining' : 'Avg Daily Time';
|
|
286
|
+
|
|
287
|
+
streaksDiv.innerHTML = `
|
|
288
|
+
<div class="heatmap-streak-item">
|
|
289
|
+
<div class="heatmap-streak-number">${streaks.longestStreak}</div>
|
|
290
|
+
<div class="heatmap-streak-label">Longest Streak</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="heatmap-streak-item">
|
|
293
|
+
<div class="heatmap-streak-number">${streaks.currentStreak}</div>
|
|
294
|
+
<div class="heatmap-streak-label">Current Streak</div>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="heatmap-streak-item">
|
|
297
|
+
<div class="heatmap-streak-number">${streaks.avgDaily}</div>
|
|
298
|
+
<div class="heatmap-streak-label">${thirdMetricLabel}</div>
|
|
299
|
+
</div>
|
|
300
|
+
`;
|
|
301
|
+
mainWrapper.appendChild(streaksDiv);
|
|
302
|
+
yearDiv.appendChild(mainWrapper);
|
|
303
|
+
|
|
304
|
+
// Add legend with discrete colors
|
|
305
|
+
const legend = document.createElement('div');
|
|
306
|
+
legend.className = 'heatmap-legend';
|
|
307
|
+
legend.innerHTML = `
|
|
308
|
+
<span>Less</span>
|
|
309
|
+
<div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
|
|
310
|
+
<div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
|
|
311
|
+
<div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
|
|
312
|
+
<div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
|
|
313
|
+
<div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
|
|
314
|
+
<span>More</span>
|
|
315
|
+
`;
|
|
316
|
+
yearDiv.appendChild(legend);
|
|
317
|
+
|
|
318
|
+
container.appendChild(yearDiv);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Export for use in other scripts
|
|
324
|
+
if (typeof window !== 'undefined') {
|
|
325
|
+
window.HeatmapRenderer = HeatmapRenderer;
|
|
326
|
+
}
|
|
@@ -22,32 +22,8 @@ function getThemeTextColor() {
|
|
|
22
22
|
|
|
23
23
|
document.addEventListener('DOMContentLoaded', function () {
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
function
|
|
27
|
-
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
28
|
-
const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
|
|
29
|
-
const dayOfWeek = yearStart.getDay(); // 0 = Sunday
|
|
30
|
-
|
|
31
|
-
// Calculate week number (1-indexed)
|
|
32
|
-
const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
|
|
33
|
-
return Math.min(53, weekNum); // Cap at 53 weeks
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Helper function to get day of week (0 = Sunday, 6 = Saturday)
|
|
37
|
-
function getDayOfWeek(date) {
|
|
38
|
-
return date.getDay();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Helper function to get the first Sunday of the year (or before)
|
|
42
|
-
function getFirstSunday(year) {
|
|
43
|
-
const jan1 = new Date(year, 0, 1);
|
|
44
|
-
const dayOfWeek = jan1.getDay();
|
|
45
|
-
const firstSunday = new Date(year, 0, 1 - dayOfWeek);
|
|
46
|
-
return firstSunday;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Function to calculate heatmap streaks and average daily time
|
|
50
|
-
function calculateHeatmapStreaks(grid, yearData, allLinesForYear = []) {
|
|
25
|
+
// Custom streak calculation function for activity heatmap (includes average daily time)
|
|
26
|
+
function calculateActivityStreaks(grid, yearData, allLinesForYear = []) {
|
|
51
27
|
const dates = [];
|
|
52
28
|
|
|
53
29
|
// Collect all dates in chronological order
|
|
@@ -160,194 +136,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
160
136
|
}
|
|
161
137
|
}
|
|
162
138
|
|
|
163
|
-
return { longestStreak, currentStreak, avgDailyTime };
|
|
139
|
+
return { longestStreak, currentStreak, avgDaily: avgDailyTime };
|
|
164
140
|
}
|
|
165
|
-
|
|
166
|
-
//
|
|
141
|
+
|
|
142
|
+
// Initialize heatmap renderer with custom configuration for activity tracking
|
|
143
|
+
const activityHeatmapRenderer = new HeatmapRenderer({
|
|
144
|
+
containerId: 'heatmapContainer',
|
|
145
|
+
metricName: 'characters',
|
|
146
|
+
metricLabel: 'characters',
|
|
147
|
+
calculateStreaks: calculateActivityStreaks
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Function to create GitHub-style heatmap using shared component
|
|
167
151
|
function createHeatmap(heatmapData) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Object.keys(heatmapData).sort().forEach(year => {
|
|
171
|
-
const yearData = heatmapData[year];
|
|
172
|
-
const yearDiv = document.createElement('div');
|
|
173
|
-
yearDiv.className = 'heatmap-year';
|
|
174
|
-
|
|
175
|
-
const yearTitle = document.createElement('h3');
|
|
176
|
-
yearTitle.textContent = year;
|
|
177
|
-
yearDiv.appendChild(yearTitle);
|
|
178
|
-
|
|
179
|
-
// Find maximum activity value for this year to scale colors
|
|
180
|
-
const maxActivity = Math.max(...Object.values(yearData));
|
|
181
|
-
|
|
182
|
-
// Create main wrapper to center everything
|
|
183
|
-
const mainWrapper = document.createElement('div');
|
|
184
|
-
mainWrapper.className = 'heatmap-wrapper';
|
|
185
|
-
|
|
186
|
-
// Create container wrapper for labels and grid
|
|
187
|
-
const containerWrapper = document.createElement('div');
|
|
188
|
-
containerWrapper.className = 'heatmap-container-wrapper';
|
|
189
|
-
|
|
190
|
-
// Create day labels (S, M, T, W, T, F, S)
|
|
191
|
-
const dayLabels = document.createElement('div');
|
|
192
|
-
dayLabels.className = 'heatmap-day-labels';
|
|
193
|
-
const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
|
|
194
|
-
dayNames.forEach(dayName => {
|
|
195
|
-
const dayLabel = document.createElement('div');
|
|
196
|
-
dayLabel.className = 'heatmap-day-label';
|
|
197
|
-
dayLabel.textContent = dayName;
|
|
198
|
-
dayLabels.appendChild(dayLabel);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Create grid container
|
|
202
|
-
const gridContainer = document.createElement('div');
|
|
203
|
-
|
|
204
|
-
// Create month labels
|
|
205
|
-
const monthLabels = document.createElement('div');
|
|
206
|
-
monthLabels.className = 'heatmap-month-labels';
|
|
207
|
-
|
|
208
|
-
// Create the main grid
|
|
209
|
-
const gridDiv = document.createElement('div');
|
|
210
|
-
gridDiv.className = 'heatmap-grid';
|
|
211
|
-
|
|
212
|
-
// Initialize 7x53 grid with empty cells
|
|
213
|
-
const grid = Array(7).fill(null).map(() => Array(53).fill(null));
|
|
214
|
-
|
|
215
|
-
// Get the first Sunday of the year (start of week 1)
|
|
216
|
-
const firstSunday = getFirstSunday(parseInt(year));
|
|
217
|
-
|
|
218
|
-
// Populate grid with dates for the entire year
|
|
219
|
-
for (let week = 0; week < 53; week++) {
|
|
220
|
-
for (let day = 0; day < 7; day++) {
|
|
221
|
-
const currentDate = new Date(firstSunday);
|
|
222
|
-
currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
|
|
223
|
-
|
|
224
|
-
// Only include dates that belong to the current year
|
|
225
|
-
if (currentDate.getFullYear() === parseInt(year)) {
|
|
226
|
-
grid[day][week] = currentDate;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Create month labels based on grid positions
|
|
232
|
-
const monthTracker = new Set();
|
|
233
|
-
for (let week = 0; week < 53; week++) {
|
|
234
|
-
const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
|
|
235
|
-
grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
|
|
236
|
-
|
|
237
|
-
if (dateInWeek) {
|
|
238
|
-
const month = dateInWeek.getMonth();
|
|
239
|
-
const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
|
|
240
|
-
|
|
241
|
-
// Add month label if it's the first week of the month
|
|
242
|
-
if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
|
|
243
|
-
const monthLabel = document.createElement('div');
|
|
244
|
-
monthLabel.className = 'heatmap-month-label';
|
|
245
|
-
monthLabel.style.gridColumn = `${week + 1}`;
|
|
246
|
-
monthLabel.textContent = monthName;
|
|
247
|
-
monthLabels.appendChild(monthLabel);
|
|
248
|
-
monthTracker.add(month);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Create cells for the grid
|
|
254
|
-
for (let day = 0; day < 7; day++) {
|
|
255
|
-
for (let week = 0; week < 53; week++) {
|
|
256
|
-
const cell = document.createElement('div');
|
|
257
|
-
cell.className = 'heatmap-cell';
|
|
258
|
-
|
|
259
|
-
const date = grid[day][week];
|
|
260
|
-
if (date) {
|
|
261
|
-
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
262
|
-
const activity = yearData[dateStr] || 0;
|
|
263
|
-
|
|
264
|
-
if (activity > 0 && maxActivity > 0) {
|
|
265
|
-
// Calculate percentage of maximum activity
|
|
266
|
-
const percentage = (activity / maxActivity) * 100;
|
|
267
|
-
|
|
268
|
-
// Assign discrete color levels based on percentage thresholds
|
|
269
|
-
let colorLevel;
|
|
270
|
-
if (percentage <= 25) {
|
|
271
|
-
colorLevel = 1; // Light green
|
|
272
|
-
} else if (percentage <= 50) {
|
|
273
|
-
colorLevel = 2; // Medium green
|
|
274
|
-
} else if (percentage <= 75) {
|
|
275
|
-
colorLevel = 3; // Dark green
|
|
276
|
-
} else {
|
|
277
|
-
colorLevel = 4; // Darkest green
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Define discrete colors for each level
|
|
281
|
-
const colors = {
|
|
282
|
-
1: '#c6e48b', // Light green (1-25%)
|
|
283
|
-
2: '#7bc96f', // Medium green (26-50%)
|
|
284
|
-
3: '#239a3b', // Dark green (51-75%)
|
|
285
|
-
4: '#196127' // Darkest green (76-100%)
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
cell.style.backgroundColor = colors[colorLevel];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
cell.title = `${dateStr}: ${activity} characters`;
|
|
292
|
-
} else {
|
|
293
|
-
// Empty cell for dates outside the year
|
|
294
|
-
cell.style.backgroundColor = 'transparent';
|
|
295
|
-
cell.style.cursor = 'default';
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
gridDiv.appendChild(cell);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
gridContainer.appendChild(monthLabels);
|
|
303
|
-
gridContainer.appendChild(gridDiv);
|
|
304
|
-
containerWrapper.appendChild(dayLabels);
|
|
305
|
-
containerWrapper.appendChild(gridContainer);
|
|
306
|
-
mainWrapper.appendChild(containerWrapper);
|
|
307
|
-
|
|
308
|
-
// Calculate and display streaks with average daily time
|
|
309
|
-
const yearLines = window.allLinesData ? window.allLinesData.filter(line => {
|
|
310
|
-
if (!line.timestamp) return false;
|
|
311
|
-
const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
|
|
312
|
-
return lineYear === parseInt(year);
|
|
313
|
-
}) : [];
|
|
314
|
-
|
|
315
|
-
const streaks = calculateHeatmapStreaks(grid, yearData, yearLines);
|
|
316
|
-
const streaksDiv = document.createElement('div');
|
|
317
|
-
streaksDiv.className = 'heatmap-streaks';
|
|
318
|
-
streaksDiv.innerHTML = `
|
|
319
|
-
<div class="heatmap-streak-item">
|
|
320
|
-
<div class="heatmap-streak-number">${streaks.longestStreak}</div>
|
|
321
|
-
<div class="heatmap-streak-label">Longest Streak</div>
|
|
322
|
-
</div>
|
|
323
|
-
<div class="heatmap-streak-item">
|
|
324
|
-
<div class="heatmap-streak-number">${streaks.currentStreak}</div>
|
|
325
|
-
<div class="heatmap-streak-label">Current Streak</div>
|
|
326
|
-
</div>
|
|
327
|
-
<div class="heatmap-streak-item">
|
|
328
|
-
<div class="heatmap-streak-number">${streaks.avgDailyTime}</div>
|
|
329
|
-
<div class="heatmap-streak-label">Avg Daily Time</div>
|
|
330
|
-
</div>
|
|
331
|
-
`;
|
|
332
|
-
mainWrapper.appendChild(streaksDiv);
|
|
333
|
-
yearDiv.appendChild(mainWrapper);
|
|
334
|
-
|
|
335
|
-
// Add legend with discrete colors
|
|
336
|
-
const legend = document.createElement('div');
|
|
337
|
-
legend.className = 'heatmap-legend';
|
|
338
|
-
legend.innerHTML = `
|
|
339
|
-
<span>Less</span>
|
|
340
|
-
<div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
|
|
341
|
-
<div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
|
|
342
|
-
<div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
|
|
343
|
-
<div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
|
|
344
|
-
<div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
|
|
345
|
-
<span>More</span>
|
|
346
|
-
`;
|
|
347
|
-
yearDiv.appendChild(legend);
|
|
348
|
-
|
|
349
|
-
container.appendChild(yearDiv);
|
|
350
|
-
});
|
|
152
|
+
activityHeatmapRenderer.render(heatmapData, window.allLinesData || []);
|
|
351
153
|
}
|
|
352
154
|
|
|
353
155
|
function showNoDataPopup() {
|
|
@@ -911,11 +713,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
911
713
|
document.getElementById('currentSessionTotalHours').textContent = hoursDisplay;
|
|
912
714
|
document.getElementById('currentSessionTotalChars').textContent = lastSession.totalChars.toLocaleString();
|
|
913
715
|
document.getElementById('currentSessionStartTime').textContent = startTimeDisplay;
|
|
914
|
-
|
|
915
|
-
document.getElementById('currentSessionEndTime').textContent = 'Now';
|
|
916
|
-
} else {
|
|
917
|
-
document.getElementById('currentSessionEndTime').textContent = endTimeDisplay;
|
|
918
|
-
}
|
|
716
|
+
document.getElementById('currentSessionEndTime').textContent = endTimeDisplay;
|
|
919
717
|
document.getElementById('currentSessionCharsPerHour').textContent = lastSession.readSpeed !== '-' ? lastSession.readSpeed.toLocaleString() : '-';
|
|
920
718
|
}
|
|
921
719
|
|
|
@@ -926,6 +724,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
926
724
|
const today = new Date();
|
|
927
725
|
const pad = n => n.toString().padStart(2, '0');
|
|
928
726
|
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
727
|
+
const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
929
728
|
document.getElementById('todayDate').textContent = todayStr;
|
|
930
729
|
|
|
931
730
|
// Filter lines for today
|
|
@@ -992,11 +791,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
992
791
|
};
|
|
993
792
|
} else {
|
|
994
793
|
// Continue current session
|
|
995
|
-
currentSession.endTime = ts;
|
|
794
|
+
currentSession.endTime = ts + afkTimerSeconds;
|
|
996
795
|
currentSession.totalChars += chars;
|
|
997
796
|
currentSession.lines.push(line);
|
|
998
797
|
if (lastTimestamp !== null) {
|
|
999
|
-
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
1000
798
|
currentSession.totalSeconds += Math.min(ts - lastTimestamp, afkTimerSeconds);
|
|
1001
799
|
}
|
|
1002
800
|
}
|
|
@@ -1022,7 +820,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1022
820
|
}
|
|
1023
821
|
|
|
1024
822
|
// Optionally, you can expose sessionDetails for debugging or further UI use:
|
|
1025
|
-
console.log(sessionDetails);
|
|
823
|
+
// console.log(sessionDetails);
|
|
1026
824
|
window.todaySessionDetails = sessionDetails;
|
|
1027
825
|
|
|
1028
826
|
// Calculate total reading time (reuse AFK logic from calculateHeatmapStreaks)
|
|
@@ -1032,7 +830,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1032
830
|
.filter(ts => !isNaN(ts))
|
|
1033
831
|
.sort((a, b) => a - b);
|
|
1034
832
|
// Get AFK timer from settings modal if available
|
|
1035
|
-
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
1036
833
|
if (timestamps.length >= 2) {
|
|
1037
834
|
for (let i = 1; i < timestamps.length; i++) {
|
|
1038
835
|
const gap = timestamps[i] - timestamps[i-1];
|
|
@@ -1076,6 +873,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1076
873
|
// Determine target date string (YYYY-MM-DD) from the end timestamp
|
|
1077
874
|
const endDateObj = new Date(endTimestamp * 1000);
|
|
1078
875
|
const targetDateStr = `${endDateObj.getFullYear()}-${pad(endDateObj.getMonth() + 1)}-${pad(endDateObj.getDate())}`;
|
|
876
|
+
const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
1079
877
|
document.getElementById('todayDate').textContent = targetDateStr;
|
|
1080
878
|
|
|
1081
879
|
// Filter lines that fall on the target date
|
|
@@ -1141,7 +939,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1141
939
|
};
|
|
1142
940
|
} else {
|
|
1143
941
|
// Continue current session
|
|
1144
|
-
currentSession.endTime = ts;
|
|
942
|
+
currentSession.endTime = ts + afkTimerSeconds;
|
|
1145
943
|
currentSession.totalChars += chars;
|
|
1146
944
|
currentSession.lines.push(line);
|
|
1147
945
|
if (lastTimestamp !== null) {
|
|
@@ -1184,8 +982,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1184
982
|
.filter(ts => !isNaN(ts))
|
|
1185
983
|
.sort((a, b) => a - b);
|
|
1186
984
|
|
|
1187
|
-
let afkTimerSeconds = window.statsConfig?.afkTimerSeconds || 120;
|
|
1188
|
-
|
|
1189
985
|
if (timestamps.length >= 2) {
|
|
1190
986
|
for (let i = 1; i < timestamps.length; i++) {
|
|
1191
987
|
const gap = timestamps[i] - timestamps[i - 1];
|