Skip to content

Commit 03ed62b

Browse files
committed
Add heatmap visualization for coding activity in summary view
- Introduced CSS styles for heatmap layout and cells. - Implemented functions to create month grids and heatmap based on coding activity data. - Added a legend to represent activity intensity levels. - Integrated heatmap creation into the summary view display logic.
1 parent 1a21951 commit 03ed62b

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

src/summaryView.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,80 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider {
258258
margin: 10px 0 0;
259259
color: var(--vscode-textLink-foreground);
260260
}
261+
.heatmap-container {
262+
margin: 30px 0;
263+
overflow-x: auto;
264+
background: var(--vscode-editor-background);
265+
border: 1px solid var(--vscode-panel-border);
266+
border-radius: 6px;
267+
padding: 20px;
268+
}
269+
.heatmap-wrapper {
270+
display: flex;
271+
flex-direction: column;
272+
width: fit-content;
273+
margin: 0 auto;
274+
}
275+
.months-container {
276+
display: flex;
277+
gap: 30px;
278+
}
279+
.month-grid {
280+
display: flex;
281+
flex-direction: column;
282+
gap: 15px;
283+
}
284+
.month-header {
285+
text-align: center;
286+
font-size: 14px;
287+
color: var(--vscode-foreground);
288+
font-weight: 500;
289+
}
290+
.heatmap-grid {
291+
display: grid;
292+
grid-template-columns: repeat(7, 15px);
293+
grid-auto-rows: 15px;
294+
gap: 4px;
295+
}
296+
.day-labels {
297+
display: grid;
298+
grid-template-columns: repeat(7, 15px);
299+
gap: 4px;
300+
margin-top: 4px;
301+
font-size: 10px;
302+
color: var(--vscode-foreground);
303+
opacity: 0.8;
304+
}
305+
.day-label {
306+
text-align: center;
307+
}
308+
.heatmap-cell {
309+
width: 15px;
310+
height: 15px;
311+
border-radius: 3px;
312+
background-color: var(--vscode-editor-background);
313+
border: 1px solid var(--vscode-panel-border);
314+
transition: all 0.2s ease;
315+
}
316+
.heatmap-cell:hover {
317+
transform: scale(1.1);
318+
}
319+
.heatmap-cell[data-level="0"] { background-color: var(--vscode-editor-background); }
320+
.heatmap-cell[data-level="1"] { background-color: #4a72b0; }
321+
.heatmap-cell[data-level="2"] { background-color: #3861a5; }
322+
.heatmap-cell[data-level="3"] { background-color: #254b91; }
323+
.heatmap-cell[data-level="4"] { background-color: #1a3b7c; }
324+
.heatmap-legend {
325+
display: flex;
326+
align-items: center;
327+
justify-content: center;
328+
gap: 5px;
329+
margin-top: 15px;
330+
padding-top: 15px;
331+
border-top: 1px solid var(--vscode-panel-border);
332+
font-size: 11px;
333+
color: var(--vscode-foreground);
334+
}
261335
</style>
262336
</head>
263337
<body>
@@ -292,6 +366,12 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider {
292366
<p id="all-time-total">Loading...</p>
293367
</div>
294368
</div>
369+
<h2>Coding Activity</h2>
370+
<div class="heatmap-container">
371+
<div class="heatmap-wrapper">
372+
<div class="months-container"></div>
373+
</div>
374+
</div>
295375
<div class="search-form">
296376
<input type="date" id="start-date-search" name="start-date-search">
297377
<input type="date" id="end-date-search" name="end-date-search">
@@ -376,6 +456,9 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider {
376456
.join('')}
377457
</table>
378458
\`;
459+
460+
// Add heatmap creation
461+
createHeatmap(data);
379462
}
380463
381464
function displaySearchResult(entries) {
@@ -406,6 +489,126 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider {
406489
return \`\${hours}h \${mins}m\`;
407490
}
408491
492+
function createMonthGrid(data, date) {
493+
const monthName = new Intl.DateTimeFormat('en-US', { month: 'long' }).format(date);
494+
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
495+
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
496+
497+
const monthGrid = document.createElement('div');
498+
monthGrid.className = 'month-grid';
499+
500+
// Add month header
501+
const header = document.createElement('div');
502+
header.className = 'month-header';
503+
header.textContent = \`\${monthName} \${date.getFullYear()}\`;
504+
monthGrid.appendChild(header);
505+
506+
// Create the grid
507+
const grid = document.createElement('div');
508+
grid.className = 'heatmap-grid';
509+
510+
// Get the day of week (0 = Sunday, ..., 6 = Saturday)
511+
const firstDayOfWeek = firstDay.getDay();
512+
513+
// Create empty cells for padding before the first day
514+
for (let i = 0; i < firstDayOfWeek; i++) {
515+
const cell = document.createElement('div');
516+
cell.className = 'heatmap-cell empty-cell';
517+
cell.style.opacity = '0';
518+
grid.appendChild(cell);
519+
}
520+
521+
// Create cells for each day of the month
522+
let previousCell = null; // Add this line to store previous cell reference
523+
for (let day = 1; day <= lastDay.getDate(); day++) {
524+
const currentDate = new Date(date.getFullYear(), date.getMonth(), day);
525+
const cell = document.createElement('div');
526+
cell.className = 'heatmap-cell';
527+
528+
const dateStr = currentDate.toISOString().split('T')[0];
529+
const minutes = data.dailySummary[dateStr] || 0;
530+
const level = getIntensityLevel(minutes);
531+
532+
// Get the day name (Sunday first)
533+
const dayOfWeek = currentDate.getDay();
534+
const dayNames = [ 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
535+
536+
// Set the data-level on the previous cell if it exists
537+
if (previousCell) {
538+
previousCell.setAttribute('data-level', level.toString());
539+
previousCell.title = \`\${dateStr} (\${dayNames[dayOfWeek]}): \${formatTime(minutes)}\`;
540+
}
541+
542+
grid.appendChild(cell);
543+
previousCell = cell; // Store current cell as previous for next iteration
544+
}
545+
546+
// Don't forget to set the attribute for the last cell
547+
if (previousCell) {
548+
const lastDate = new Date(date.getFullYear(), date.getMonth(), lastDay.getDate());
549+
const lastDateStr = lastDate.toISOString().split('T')[0];
550+
const lastMinutes = data.dailySummary[lastDateStr] || 0;
551+
const lastLevel = getIntensityLevel(lastMinutes);
552+
const lastDayName = ['Saturday','Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'][lastDate.getDay()];
553+
554+
previousCell.setAttribute('data-level', lastLevel.toString());
555+
previousCell.title = \`\${lastDateStr} (\${lastDayName}): \${formatTime(lastMinutes)}\`;
556+
}
557+
558+
monthGrid.appendChild(grid);
559+
560+
// Add day labels below the grid with Sunday first
561+
const dayLabels = document.createElement('div');
562+
dayLabels.className = 'day-labels';
563+
const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
564+
days.forEach(day => {
565+
const label = document.createElement('div');
566+
label.className = 'day-label';
567+
label.textContent = day;
568+
dayLabels.appendChild(label);
569+
});
570+
monthGrid.appendChild(dayLabels);
571+
572+
return monthGrid;
573+
}
574+
575+
function createHeatmap(data) {
576+
const container = document.querySelector('.heatmap-container');
577+
container.innerHTML = '<div class="heatmap-wrapper"><div class="months-container"></div></div>';
578+
579+
const monthsContainer = container.querySelector('.months-container');
580+
const now = new Date();
581+
582+
// Create grids for the last 3 months in reverse order (most recent first)
583+
for (let i = 2; i >= 0; i--) {
584+
const monthDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
585+
const monthGrid = createMonthGrid(data, monthDate);
586+
monthsContainer.appendChild(monthGrid);
587+
}
588+
589+
// Add legend
590+
const legend = document.createElement('div');
591+
legend.className = 'heatmap-legend';
592+
legend.innerHTML = \`
593+
<span>Less</span>
594+
<div class="heatmap-cell" data-level="0"></div>
595+
<div class="heatmap-cell" data-level="1"></div>
596+
<div class="heatmap-cell" data-level="2"></div>
597+
<div class="heatmap-cell" data-level="3"></div>
598+
<div class="heatmap-cell" data-level="4"></div>
599+
<span>More</span>
600+
\`;
601+
container.querySelector('.heatmap-wrapper').appendChild(legend);
602+
}
603+
604+
function getIntensityLevel(minutes) {
605+
if (minutes === 0) return 0;
606+
if (minutes < 60) return 1; // Less than 1 hour
607+
if (minutes < 180) return 2; // 1-3 hours
608+
if (minutes < 360) return 3; // 3-6 hours
609+
return 4; // More than 6 hours
610+
}
611+
409612
// Request a refresh when the webview becomes visible
410613
vscode.postMessage({ command: 'refresh' });
411614
</script>

0 commit comments

Comments
 (0)