fix(stats): fix inconsistent stat card styling and add median/session stats to completion race

This commit is contained in:
ACX
2026-02-12 00:33:49 -07:00
committed by acx10
parent 8128269310
commit 120334eb28
8 changed files with 61 additions and 33 deletions

View File

@@ -35,16 +35,26 @@
<span class="stat-value">{{ totalBooks }}</span>
<span class="stat-label">Books</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ totalSessions }}</span>
<span class="stat-label">Sessions</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ avgDaysToFinish }}d</span>
<span class="stat-label">Avg Days</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ medianDaysToFinish }}d</span>
<span class="stat-label">Median Days</span>
</div>
<div class="stat-card fastest">
<span class="stat-value-sm">{{ fastestCompletion }}</span>
<span class="stat-value">{{ fastestDays }}</span>
<span class="stat-subtitle">{{ fastestTitle }}</span>
<span class="stat-label">Fastest</span>
</div>
<div class="stat-card slowest">
<span class="stat-value-sm">{{ slowestCompletion }}</span>
<span class="stat-value">{{ slowestDays }}</span>
<span class="stat-subtitle">{{ slowestTitle }}</span>
<span class="stat-label">Slowest</span>
</div>
</div>

View File

@@ -97,12 +97,15 @@
color: #4caf50;
}
.stat-value-sm {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-color, #ffffff);
.stat-subtitle {
font-size: 0.75rem;
color: var(--text-secondary-color);
text-align: center;
line-height: 1.3;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-label {
@@ -113,11 +116,11 @@
margin-top: 0.25rem;
}
&.fastest .stat-value-sm {
&.fastest .stat-value {
color: #4caf50;
}
&.slowest .stat-value-sm {
&.slowest .stat-value {
color: #ff9800;
}
}

View File

@@ -38,9 +38,13 @@ export class CompletionRaceChartComponent implements OnInit, OnDestroy {
public chartOptions: ChartConfiguration<'line'>['options'];
public totalBooks = 0;
public fastestCompletion = '';
public slowestCompletion = '';
public avgDaysToFinish = 0;
public medianDaysToFinish = 0;
public totalSessions = 0;
public fastestDays = '';
public fastestTitle = '';
public slowestDays = '';
public slowestTitle = '';
private readonly userStatsService = inject(UserStatsService);
private readonly destroy$ = new Subject<void>();
@@ -189,16 +193,26 @@ export class CompletionRaceChartComponent implements OnInit, OnDestroy {
this.totalBooks = races.length;
if (races.length > 0) {
const days = races.map(r => r.totalDays);
const days = races.map(r => r.totalDays).sort((a, b) => a - b);
const fastest = races.reduce((a, b) => a.totalDays <= b.totalDays ? a : b);
const slowest = races.reduce((a, b) => a.totalDays >= b.totalDays ? a : b);
this.fastestCompletion = `${fastest.bookTitle.substring(0, 25)}${fastest.bookTitle.length > 25 ? '...' : ''} (${fastest.totalDays}d)`;
this.slowestCompletion = `${slowest.bookTitle.substring(0, 25)}${slowest.bookTitle.length > 25 ? '...' : ''} (${slowest.totalDays}d)`;
this.avgDaysToFinish = Math.round(days.reduce((a, b) => a + b, 0) / days.length);
this.medianDaysToFinish = days.length % 2 === 0
? Math.round((days[days.length / 2 - 1] + days[days.length / 2]) / 2)
: days[Math.floor(days.length / 2)];
this.totalSessions = races.reduce((sum, r) => sum + r.sessions.length, 0);
this.fastestDays = `${fastest.totalDays}d`;
this.fastestTitle = fastest.bookTitle.length > 25 ? fastest.bookTitle.substring(0, 25) + '...' : fastest.bookTitle;
this.slowestDays = `${slowest.totalDays}d`;
this.slowestTitle = slowest.bookTitle.length > 25 ? slowest.bookTitle.substring(0, 25) + '...' : slowest.bookTitle;
} else {
this.fastestCompletion = '-';
this.slowestCompletion = '-';
this.avgDaysToFinish = 0;
this.medianDaysToFinish = 0;
this.totalSessions = 0;
this.fastestDays = '-';
this.fastestTitle = '';
this.slowestDays = '-';
this.slowestTitle = '';
}
const datasets = races.map((race, i) => {

View File

@@ -25,7 +25,7 @@
<span class="stat-label">Total Read</span>
</div>
<div class="stat-card type">
<span class="stat-value-sm">{{ readerType }}</span>
<span class="stat-value">{{ readerType }}</span>
<span class="stat-label">Reader Type</span>
</div>
</div>

View File

@@ -58,12 +58,6 @@
color: var(--text-color, #ffffff);
}
.stat-value-sm {
font-size: 1rem;
font-weight: 500;
color: var(--text-color, #ffffff);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-secondary-color);
@@ -80,7 +74,7 @@
color: #42a5f5;
}
&.type .stat-value-sm {
&.type .stat-value {
color: #9c27b0;
}
}
@@ -89,7 +83,8 @@
.chart-wrapper {
position: relative;
height: 350px;
width: 100%;
aspect-ratio: 1;
margin: 0 auto;
}
.no-data-message {

View File

@@ -25,11 +25,12 @@
<span class="stat-label">Completion Rate</span>
</div>
<div class="stat-card">
<span class="stat-value-sm">{{ medianDropout }}</span>
<span class="stat-value">{{ medianDropout }}</span>
<span class="stat-label">Median Dropout</span>
</div>
<div class="stat-card danger">
<span class="stat-value-sm">{{ dangerZone }}</span>
<span class="stat-value">{{ dangerZoneRange }}</span>
<span class="stat-subtitle">{{ dangerZoneDrop }}</span>
<span class="stat-label">Danger Zone</span>
</div>
</div>

View File

@@ -58,10 +58,9 @@
color: #e91e63;
}
.stat-value-sm {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-color, #ffffff);
.stat-subtitle {
font-size: 0.75rem;
color: var(--text-secondary-color);
text-align: center;
}
@@ -73,7 +72,11 @@
margin-top: 0.25rem;
}
&.danger .stat-value-sm {
&.danger .stat-value {
color: #ff5722;
}
&.danger .stat-subtitle {
color: #ff5722;
}
}

View File

@@ -28,7 +28,8 @@ export class ReadingSurvivalChartComponent implements OnInit, OnDestroy {
public totalStarted = 0;
public completionRate = 0;
public medianDropout = '';
public dangerZone = '';
public dangerZoneRange = '';
public dangerZoneDrop = '';
public readonly chartOptions: ChartConfiguration<'line'>['options'] = {
responsive: true,
@@ -156,7 +157,8 @@ export class ReadingSurvivalChartComponent implements OnInit, OnDestroy {
dangerIdx = i;
}
}
this.dangerZone = `${THRESHOLDS[dangerIdx - 1]}-${THRESHOLDS[dangerIdx]}% (-${maxDrop.toFixed(0)}%)`;
this.dangerZoneRange = `${THRESHOLDS[dangerIdx - 1]}-${THRESHOLDS[dangerIdx]}%`;
this.dangerZoneDrop = `-${maxDrop.toFixed(0)}%`;
const labels = THRESHOLDS.map(t => `${t}%`);
this.chartDataSubject.next({