mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-08-03 04:52:12 +00:00
feat(metrics): polish heatmap
This commit is contained in:
parent
d1eb1b49b1
commit
ca93982db8
4 changed files with 87 additions and 48 deletions
|
|
@ -11,9 +11,13 @@
|
|||
|
||||
<div class="heatmap-grid">
|
||||
<div class="day-labels">
|
||||
<div class="day-label">Sun</div>
|
||||
<div class="day-label">Mon</div>
|
||||
<div class="day-label">Tue</div>
|
||||
<div class="day-label">Wed</div>
|
||||
<div class="day-label">Thu</div>
|
||||
<div class="day-label">Fri</div>
|
||||
<div class="day-label">Sat</div>
|
||||
</div>
|
||||
|
||||
<div class="weeks">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Task[]> {
|
||||
// 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) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@
|
|||
</section>
|
||||
}
|
||||
|
||||
<activity-heatmap></activity-heatmap>
|
||||
<section style="max-width: 880px; margin: auto; margin-top: 32px">
|
||||
<h1 style="margin-bottom: 0">Activity</h1>
|
||||
<activity-heatmap></activity-heatmap>
|
||||
</section>
|
||||
|
||||
@if (!metricService.hasData()) {
|
||||
<p style="margin-top: 32px">
|
||||
|
|
@ -74,36 +77,6 @@
|
|||
@if (metricService.hasData()) {
|
||||
<section class="metric-metrics">
|
||||
<h1>{{ T.F.METRIC.CMP.GLOBAL_METRICS | translate }}</h1>
|
||||
<section class="pie-charts">
|
||||
@if (metricService.improvementCountsPieChartData(); as improvementCounts) {
|
||||
<section>
|
||||
<h3>{{ T.F.METRIC.CMP.IMPROVEMENT_SELECTION_COUNT | translate }}</h3>
|
||||
<lazy-chart
|
||||
[type]="pieChartType"
|
||||
[datasets]="improvementCounts.datasets"
|
||||
[labels]="improvementCounts.labels"
|
||||
[legend]="improvementCounts?.datasets[0].data.length < 12"
|
||||
[options]="pieChartOptions"
|
||||
height="300px"
|
||||
>
|
||||
</lazy-chart>
|
||||
</section>
|
||||
}
|
||||
@if (metricService.obstructionCountsPieChartData(); as obstructionCounts) {
|
||||
<section>
|
||||
<h3>{{ T.F.METRIC.CMP.OBSTRUCTION_SELECTION_COUNT | translate }}</h3>
|
||||
<lazy-chart
|
||||
[type]="pieChartType"
|
||||
[datasets]="obstructionCounts.datasets"
|
||||
[labels]="obstructionCounts.labels"
|
||||
[legend]="obstructionCounts?.datasets[0].data.length < 12"
|
||||
[options]="pieChartOptions"
|
||||
height="300px"
|
||||
>
|
||||
</lazy-chart>
|
||||
</section>
|
||||
}
|
||||
</section>
|
||||
<section class="line-charts">
|
||||
@if (productivityHappiness(); as productivityHappiness) {
|
||||
<section>
|
||||
|
|
@ -134,6 +107,37 @@
|
|||
}
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="pie-charts">
|
||||
@if (metricService.improvementCountsPieChartData(); as improvementCounts) {
|
||||
<section>
|
||||
<h3>{{ T.F.METRIC.CMP.IMPROVEMENT_SELECTION_COUNT | translate }}</h3>
|
||||
<lazy-chart
|
||||
[type]="pieChartType"
|
||||
[datasets]="improvementCounts.datasets"
|
||||
[labels]="improvementCounts.labels"
|
||||
[legend]="improvementCounts?.datasets[0].data.length < 12"
|
||||
[options]="pieChartOptions"
|
||||
height="300px"
|
||||
>
|
||||
</lazy-chart>
|
||||
</section>
|
||||
}
|
||||
@if (metricService.obstructionCountsPieChartData(); as obstructionCounts) {
|
||||
<section>
|
||||
<h3>{{ T.F.METRIC.CMP.OBSTRUCTION_SELECTION_COUNT | translate }}</h3>
|
||||
<lazy-chart
|
||||
[type]="pieChartType"
|
||||
[datasets]="obstructionCounts.datasets"
|
||||
[labels]="obstructionCounts.labels"
|
||||
[legend]="obstructionCounts?.datasets[0].data.length < 12"
|
||||
[options]="pieChartOptions"
|
||||
height="300px"
|
||||
>
|
||||
</lazy-chart>
|
||||
</section>
|
||||
}
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
@if (metricService.hasData()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue