From ca93982db84eabb2308f952a9e1494c3fa7fc0ca Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 21 Oct 2025 12:05:47 +0200 Subject: [PATCH] feat(metrics): polish heatmap --- .../activity-heatmap.component.html | 4 ++ .../activity-heatmap.component.scss | 19 +++--- .../activity-heatmap.component.ts | 46 ++++++++++--- src/app/features/metric/metric.component.html | 66 ++++++++++--------- 4 files changed, 87 insertions(+), 48 deletions(-) diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.html b/src/app/features/metric/activity-heatmap/activity-heatmap.component.html index 840b533def..f36ed4f7a1 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.html +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.html @@ -11,9 +11,13 @@
+
Sun
Mon
+
Tue
Wed
+
Thu
Fri
+
Sat
diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.scss b/src/app/features/metric/activity-heatmap/activity-heatmap.component.scss index 478bbdb7ec..70e66641a8 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.scss +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.scss @@ -36,11 +36,12 @@ .day-labels { display: flex; flex-direction: column; - justify-content: space-around; + gap: 2px; font-size: 10px; color: rgba(0, 0, 0, 0.6); padding-right: 4px; width: 40px; + text-align: right; .day-label { height: 12px; @@ -101,19 +102,19 @@ } &.level-1 { - background: #b2ebf2; + background: color-mix(in srgb, var(--c-primary) 20%, transparent); } &.level-2 { - background: #4dd0e1; + background: color-mix(in srgb, var(--c-primary) 40%, transparent); } &.level-3 { - background: #00bcd4; + background: color-mix(in srgb, var(--c-primary) 60%, transparent); } &.level-4 { - background: #0097a7; + background: var(--c-primary); } &:not(.empty):hover { @@ -146,19 +147,19 @@ } &.level-1 { - background: #b2ebf2; + background: color-mix(in srgb, var(--c-primary) 20%, transparent); } &.level-2 { - background: #4dd0e1; + background: color-mix(in srgb, var(--c-primary) 40%, transparent); } &.level-3 { - background: #00bcd4; + background: color-mix(in srgb, var(--c-primary) 60%, transparent); } &.level-4 { - background: #0097a7; + background: var(--c-primary); } } } diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts index 32fe4ecb4f..3b28d17cbe 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts @@ -3,7 +3,9 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { WorklogService } from '../../worklog/worklog.service'; import { WorkContextService } from '../../work-context/work-context.service'; import { TaskService } from '../../tasks/task.service'; -import { map, switchMap } from 'rxjs/operators'; +import { TaskArchiveService } from '../../time-tracking/task-archive.service'; +import { from } from 'rxjs'; +import { first, map, switchMap } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { MsToStringPipe } from '../../../ui/duration/ms-to-string.pipe'; import { TranslatePipe } from '@ngx-translate/core'; @@ -34,6 +36,7 @@ export class ActivityHeatmapComponent { private readonly _worklogService = inject(WorklogService); private readonly _workContextService = inject(WorkContextService); private readonly _taskService = inject(TaskService); + private readonly _taskArchiveService = inject(TaskArchiveService); T: typeof T = T; monthLabels: string[] = []; @@ -41,14 +44,14 @@ export class ActivityHeatmapComponent { // Compute heatmap data // NOTE: Reacts to work context changes - // - For TODAY tag: shows ALL tasks from all projects/tags + // - For TODAY tag: shows ALL tasks from all projects/tags (current + archived) // - For other tags/projects: shows only tasks from that context heatmapData = toSignal( this._workContextService.activeWorkContext$.pipe( switchMap((context) => { // Special case: TODAY tag shows ALL data if (context.id === TODAY_TAG.id) { - return this._taskService.allTasks$.pipe( + return from(this._loadAllTasks()).pipe( map((tasks) => this._buildHeatmapDataFromTasks(tasks)), ); } @@ -62,6 +65,32 @@ export class ActivityHeatmapComponent { { initialValue: null }, ); + private async _loadAllTasks(): Promise { + // Load both current tasks and archived tasks + const [archive, currentTasks] = await Promise.all([ + this._taskArchiveService.load(), + this._taskService.allTasks$.pipe(first()).toPromise(), + ]); + + const allTasks: Task[] = [...(currentTasks || [])]; + + // Add archived tasks from all projects + if (archive) { + Object.values(archive).forEach((projectArchive) => { + if (projectArchive?.ids) { + projectArchive.ids.forEach((taskId) => { + const archivedTask = projectArchive.entities[taskId]; + if (archivedTask) { + allTasks.push(archivedTask); + } + }); + } + }); + } + + return allTasks; + } + private _buildHeatmapDataFromTasks(tasks: Task[]): { weeks: WeekData[]; monthLabels: string[]; @@ -120,13 +149,15 @@ export class ActivityHeatmapComponent { }); // Calculate levels (0-4) based on activity + // Prioritize time spent (80%) over task count (20%) dayMap.forEach((day) => { if (day.taskCount === 0 && day.timeSpent === 0) { day.level = 0; } else { const taskRatio = maxTasks > 0 ? day.taskCount / maxTasks : 0; const timeRatio = maxTime > 0 ? day.timeSpent / maxTime : 0; - const combinedRatio = (taskRatio + timeRatio) / 2; + // eslint-disable-next-line no-mixed-operators + const combinedRatio = timeRatio * 0.8 + taskRatio * 0.2; if (combinedRatio > 0.75) { day.level = 4; @@ -210,17 +241,16 @@ export class ActivityHeatmapComponent { }); // Calculate levels (0-4) based on activity - // Use a combined metric of tasks and time + // Prioritize time spent (80%) over task count (20%) dayMap.forEach((day) => { if (day.taskCount === 0 && day.timeSpent === 0) { day.level = 0; } else { - // Normalize based on both tasks and time const taskRatio = maxTasks > 0 ? day.taskCount / maxTasks : 0; const timeRatio = maxTime > 0 ? day.timeSpent / maxTime : 0; - const combinedRatio = (taskRatio + timeRatio) / 2; + // eslint-disable-next-line no-mixed-operators + const combinedRatio = timeRatio * 0.8 + taskRatio * 0.2; - // Map to levels 1-4 if (combinedRatio > 0.75) { day.level = 4; } else if (combinedRatio > 0.5) { diff --git a/src/app/features/metric/metric.component.html b/src/app/features/metric/metric.component.html index c6a7de399c..0d623ee168 100644 --- a/src/app/features/metric/metric.component.html +++ b/src/app/features/metric/metric.component.html @@ -64,7 +64,10 @@ } - +
+

Activity

+ +
@if (!metricService.hasData()) {

@@ -74,36 +77,6 @@ @if (metricService.hasData()) {

{{ T.F.METRIC.CMP.GLOBAL_METRICS | translate }}

-
- @if (metricService.improvementCountsPieChartData(); as improvementCounts) { -
-

{{ T.F.METRIC.CMP.IMPROVEMENT_SELECTION_COUNT | translate }}

- - -
- } - @if (metricService.obstructionCountsPieChartData(); as obstructionCounts) { -
-

{{ T.F.METRIC.CMP.OBSTRUCTION_SELECTION_COUNT | translate }}

- - -
- } -
@if (productivityHappiness(); as productivityHappiness) {
@@ -134,6 +107,37 @@ } }
+ +
+ @if (metricService.improvementCountsPieChartData(); as improvementCounts) { +
+

{{ T.F.METRIC.CMP.IMPROVEMENT_SELECTION_COUNT | translate }}

+ + +
+ } + @if (metricService.obstructionCountsPieChartData(); as obstructionCounts) { +
+

{{ T.F.METRIC.CMP.OBSTRUCTION_SELECTION_COUNT | translate }}

+ + +
+ } +
} @if (metricService.hasData()) {