Add 'Books Finished per Month' heat map chart

This commit is contained in:
aditya.chandel
2025-09-05 17:54:31 -06:00
committed by Aditya Chandel
parent 0db2913e28
commit af622d9f75
9 changed files with 345 additions and 283 deletions

View File

@@ -25,6 +25,7 @@
"@tweenjs/tween.js": "^25.0.0",
"angular-oauth2-oidc": "^20.0.0",
"chart.js": "^4.5.0",
"chartjs-chart-matrix": "^3.0.0",
"chartjs-plugin-datalabels": "^2.2.0",
"epubjs": "^0.3.93",
"jwt-decode": "^4.0.0",
@@ -7450,6 +7451,15 @@
"pnpm": ">=8"
}
},
"node_modules/chartjs-chart-matrix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-chart-matrix/-/chartjs-chart-matrix-3.0.0.tgz",
"integrity": "sha512-lUWC1UaWkxGdG02dBJ5r1ppbSYB/uWmwAh11VEs7V3ZQItNCk4am+rmacwkgeb+SQeEj2hP9Qq4oGsUmPl/1lQ==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=3.0.0"
}
},
"node_modules/chartjs-plugin-datalabels": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",

View File

@@ -28,6 +28,7 @@
"@tweenjs/tween.js": "^25.0.0",
"angular-oauth2-oidc": "^20.0.0",
"chart.js": "^4.5.0",
"chartjs-chart-matrix": "^3.0.0",
"chartjs-plugin-datalabels": "^2.2.0",
"epubjs": "^0.3.93",
"jwt-decode": "^4.0.0",

View File

@@ -18,8 +18,8 @@ export class ChartConfigService {
private readonly defaultCharts: ChartConfig[] = [
{id: 'readingStatus', name: 'Reading Status', enabled: true, category: 'small', order: 0},
{id: 'bookFormats', name: 'Book Formats', enabled: true, category: 'small', order: 1},
{id: 'languageDistribution', name: 'Language Distribution', enabled: true, category: 'small', order: 2},
{id: 'bookMetadataScore', name: 'Book Metadata Score', enabled: true, category: 'small', order: 3},
{id: 'bookMetadataScore', name: 'Book Metadata Score', enabled: true, category: 'small', order: 2},
{id: 'readingHeatmap', name: 'Books Finished per Month', category: 'small', enabled: true, order: 3},
{id: 'topAuthors', name: 'Top 25 Authors', enabled: true, category: 'large', order: 4},
{id: 'topCategories', name: 'Top 25 Categories', enabled: true, category: 'large', order: 5},
{id: 'monthlyReadingPatterns', name: 'Monthly Reading Patterns', enabled: true, category: 'large', order: 6},
@@ -33,7 +33,7 @@ export class ChartConfigService {
{id: 'readingDNA', name: 'Reading DNA Profile', enabled: true, category: 'large', order: 14},
{id: 'readingHabits', name: 'Reading Habits Analysis', enabled: true, category: 'large', order: 15},
{id: 'publicationYear', name: 'Publication Year Timeline', enabled: true, category: 'full-width', order: 16},
{id: 'finishedBooksTimeline', name: 'Books Finished Timeline', enabled: true, category: 'full-width', order: 17}
{id: 'languageDistribution', name: 'Language Distribution', enabled: true, category: 'small', order: 17}
];
private chartsConfigSubject = new BehaviorSubject<ChartConfig[]>(this.loadConfig());

View File

@@ -1,248 +0,0 @@
import {inject, Injectable, OnDestroy} from '@angular/core';
import {BehaviorSubject, EMPTY, Observable, Subject} from 'rxjs';
import {map, takeUntil, catchError, filter, first, switchMap} from 'rxjs/operators';
import {ChartConfiguration, ChartData, ChartType} from 'chart.js';
import {LibraryFilterService} from './library-filter.service';
import {BookService} from '../../book/service/book.service';
import {Book} from '../../book/model/book.model';
interface FinishedBooksStats {
yearMonth: string;
count: number;
year: number;
month: number;
}
const CHART_COLORS = {
primary: '#EF476F',
primaryBackground: 'rgba(239, 71, 111, 0.1)',
border: '#ffffff'
} as const;
const CHART_DEFAULTS = {
borderColor: CHART_COLORS.primary,
backgroundColor: CHART_COLORS.primaryBackground,
borderWidth: 2,
pointBackgroundColor: CHART_COLORS.primary,
pointBorderColor: CHART_COLORS.border,
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4
} as const;
type FinishedBooksChartData = ChartData<'line', number[], string>;
@Injectable({
providedIn: 'root'
})
export class FinishedBooksTimelineChartService implements OnDestroy {
private readonly bookService = inject(BookService);
private readonly libraryFilterService = inject(LibraryFilterService);
private readonly destroy$ = new Subject<void>();
public readonly finishedBooksChartType = 'line' as const;
public readonly finishedBooksChartOptions: ChartConfiguration['options'] = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {display: false},
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.9)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: '#ffffff',
borderWidth: 1,
cornerRadius: 6,
displayColors: true,
padding: 12,
titleFont: {size: 14, weight: 'bold'},
bodyFont: {size: 13},
position: 'nearest',
callbacks: {
title: (context) => {
return context[0].label; // Already formatted as "Month Year"
},
label: this.formatTooltipLabel.bind(this)
}
},
datalabels: {
display: true,
color: '#ffffff',
font: {
size: 10,
weight: 'bold'
},
align: 'top',
offset: 8,
formatter: (value: number) => value.toString()
}
},
interaction: {
intersect: false,
mode: 'point'
},
scales: {
x: {
beginAtZero: true,
ticks: {
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11.5
},
maxRotation: 45,
callback: function (value, index, values) {
const totalLabels = values.length;
const skipInterval = totalLabels > 24 ? Math.ceil(totalLabels / 12) : totalLabels > 12 ? 2 : 1;
return index % skipInterval === 0 ? this.getLabelForValue(value as number) : '';
}
},
grid: {color: 'rgba(255, 255, 255, 0.1)'},
title: {
display: true,
text: 'Month',
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11.5
}
}
},
y: {
beginAtZero: true,
ticks: {
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11.5
},
stepSize: 1
},
grid: {color: 'rgba(255, 255, 255, 0.05)'},
title: {
display: true,
text: 'Books Finished',
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11.5
}
}
}
}
};
private readonly finishedBooksChartDataSubject = new BehaviorSubject<FinishedBooksChartData>({
labels: [],
datasets: [{
label: 'Books Finished',
data: [],
...CHART_DEFAULTS
}]
});
public readonly finishedBooksChartData$: Observable<FinishedBooksChartData> =
this.finishedBooksChartDataSubject.asObservable();
constructor() {
this.bookService.bookState$
.pipe(
filter(state => state.loaded),
first(),
switchMap(() =>
this.libraryFilterService.selectedLibrary$.pipe(
takeUntil(this.destroy$)
)
),
catchError((error) => {
console.error('Error processing finished books stats:', error);
return EMPTY;
})
)
.subscribe(() => {
const stats = this.calculateFinishedBooksStats();
this.updateChartData(stats);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private updateChartData(stats: FinishedBooksStats[]): void {
try {
const labels = stats.map(s => {
const [year, month] = s.yearMonth.split('-');
const monthName = new Date(parseInt(year), parseInt(month) - 1).toLocaleString('default', { month: 'short' });
return `${monthName} ${year}`;
});
const dataValues = stats.map(s => s.count);
this.finishedBooksChartDataSubject.next({
labels,
datasets: [{
label: 'Books Finished',
data: dataValues,
...CHART_DEFAULTS
}]
});
} catch (error) {
console.error('Error updating chart data:', error);
}
}
private calculateFinishedBooksStats(): FinishedBooksStats[] {
const currentState = this.bookService.getCurrentBookState();
const selectedLibraryId = this.libraryFilterService.getCurrentSelectedLibrary();
if (!this.isValidBookState(currentState)) {
return [];
}
const filteredBooks = this.filterBooksByLibrary(currentState.books!, selectedLibraryId);
return this.processFinishedBooksStats(filteredBooks);
}
private isValidBookState(state: any): boolean {
return state?.loaded && state?.books && Array.isArray(state.books) && state.books.length > 0;
}
private filterBooksByLibrary(books: Book[], selectedLibraryId: string | number | null): Book[] {
return selectedLibraryId
? books.filter(book => book.libraryId === selectedLibraryId)
: books;
}
private processFinishedBooksStats(books: Book[]): FinishedBooksStats[] {
const yearMonthMap = new Map<string, number>();
books.forEach(book => {
if (book.dateFinished) {
const finishedDate = new Date(book.dateFinished);
const yearMonth = `${finishedDate.getFullYear()}-${(finishedDate.getMonth() + 1).toString().padStart(2, '0')}`;
yearMonthMap.set(yearMonth, (yearMonthMap.get(yearMonth) || 0) + 1);
}
});
return Array.from(yearMonthMap.entries())
.filter(([yearMonth, count]) => count > 0)
.map(([yearMonth, count]) => ({
yearMonth,
count,
year: parseInt(yearMonth.split('-')[0]),
month: parseInt(yearMonth.split('-')[1])
}))
.sort((a, b) => a.yearMonth.localeCompare(b.yearMonth));
}
private formatTooltipLabel(context: any): string {
const value = context.parsed.y;
return `${value} book${value === 1 ? '' : 's'} finished`;
}
}

View File

@@ -0,0 +1,248 @@
import {inject, Injectable, OnDestroy} from '@angular/core';
import {BehaviorSubject, EMPTY, Observable, Subject} from 'rxjs';
import {map, takeUntil, catchError, filter, first, switchMap} from 'rxjs/operators';
import {ChartConfiguration, ChartData} from 'chart.js';
import {LibraryFilterService} from './library-filter.service';
import {BookService} from '../../book/service/book.service';
import {Book} from '../../book/model/book.model';
interface MatrixDataPoint {
x: number; // month (0-11)
y: number; // year index
v: number; // book count
}
interface YearMonthData {
year: number;
month: number;
count: number;
}
const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
type HeatmapChartData = ChartData<'matrix', MatrixDataPoint[], string>;
@Injectable({
providedIn: 'root'
})
export class ReadingHeatmapChartService implements OnDestroy {
private readonly bookService = inject(BookService);
private readonly libraryFilterService = inject(LibraryFilterService);
private readonly destroy$ = new Subject<void>();
public readonly heatmapChartType = 'matrix' as const;
private yearLabels: string[] = [];
private maxBookCount = 1;
public readonly heatmapChartOptions: ChartConfiguration['options'] = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 20
}
},
plugins: {
legend: {display: false},
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.9)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: '#ffffff',
borderWidth: 1,
cornerRadius: 6,
displayColors: false,
padding: 12,
titleFont: {size: 14, weight: 'bold'},
bodyFont: {size: 13},
callbacks: {
title: (context) => {
const point = context[0].raw as MatrixDataPoint;
const year = this.yearLabels[point.y];
const month = MONTH_NAMES[point.x];
return `${month} ${year}`;
},
label: (context) => {
const point = context.raw as MatrixDataPoint;
return `${point.v} book${point.v === 1 ? '' : 's'} read`;
}
}
},
datalabels: {
display: true,
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 10,
weight: 'bold'
},
formatter: (value: MatrixDataPoint) => value.v > 0 ? value.v.toString() : ''
}
},
scales: {
x: {
type: 'linear',
position: 'bottom',
ticks: {
stepSize: 1,
callback: (value) => MONTH_NAMES[value as number] || '',
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11
}
},
grid: {display: false},
},
y: {
type: 'linear',
ticks: {
stepSize: 1,
callback: (value) => this.yearLabels[value as number] || '',
color: '#ffffff',
font: {
family: "'Inter', sans-serif",
size: 11
}
},
grid: {display: false},
}
}
};
private readonly heatmapChartDataSubject = new BehaviorSubject<HeatmapChartData>({
labels: [],
datasets: [{
label: 'Books Read',
data: []
}]
});
public readonly heatmapChartData$: Observable<HeatmapChartData> =
this.heatmapChartDataSubject.asObservable();
constructor() {
this.bookService.bookState$
.pipe(
filter(state => state.loaded),
first(),
switchMap(() =>
this.libraryFilterService.selectedLibrary$.pipe(
takeUntil(this.destroy$)
)
),
catchError((error) => {
console.error('Error processing reading heatmap stats:', error);
return EMPTY;
})
)
.subscribe(() => {
const stats = this.calculateHeatmapData();
this.updateChartData(stats);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private updateChartData(yearMonthData: YearMonthData[]): void {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const years = Array.from({length: 10}, (_, i) => currentYear - 9 + i);
this.yearLabels = years.map(String);
this.maxBookCount = Math.max(1, ...yearMonthData.map(d => d.count));
const heatmapData: MatrixDataPoint[] = [];
years.forEach((year, yearIndex) => {
const maxMonth = year === currentYear ? currentMonth : 11;
for (let month = 0; month <= maxMonth; month++) {
const dataPoint = yearMonthData.find(d => d.year === year && d.month === month + 1);
heatmapData.push({
x: month,
y: yearIndex,
v: dataPoint?.count || 0
});
}
});
if (this.heatmapChartOptions?.scales?.['y']) {
(this.heatmapChartOptions.scales['y'] as any).max = years.length - 0.5;
}
this.heatmapChartDataSubject.next({
labels: [],
datasets: [{
label: 'Books Read',
data: heatmapData,
backgroundColor: (context) => {
const point = context.raw as MatrixDataPoint;
if (!point?.v) return 'rgba(255, 255, 255, 0.05)';
const intensity = point.v / this.maxBookCount;
const alpha = Math.max(0.2, Math.min(1.0, intensity * 0.8 + 0.2));
return `rgba(239, 71, 111, ${alpha})`;
},
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1,
width: ({chart}) => (chart.chartArea?.width || 0) / 12 - 1,
height: ({chart}) => (chart.chartArea?.height || 0) / years.length - 1
}]
});
}
private calculateHeatmapData(): YearMonthData[] {
const currentState = this.bookService.getCurrentBookState();
const selectedLibraryId = this.libraryFilterService.getCurrentSelectedLibrary();
if (!this.isValidBookState(currentState)) {
return [];
}
const filteredBooks = this.filterBooksByLibrary(currentState.books!, selectedLibraryId);
return this.processHeatmapData(filteredBooks);
}
private isValidBookState(state: any): boolean {
return state?.loaded && state?.books && Array.isArray(state.books) && state.books.length > 0;
}
private filterBooksByLibrary(books: Book[], selectedLibraryId: string | number | null): Book[] {
return selectedLibraryId
? books.filter(book => book.libraryId === selectedLibraryId)
: books;
}
private processHeatmapData(books: Book[]): YearMonthData[] {
const yearMonthMap = new Map<string, number>();
const currentYear = new Date().getFullYear();
const startYear = currentYear - 9;
books
.filter(book => book.dateFinished)
.forEach(book => {
const finishedDate = new Date(book.dateFinished!);
const year = finishedDate.getFullYear();
if (year >= startYear && year <= currentYear) {
const month = finishedDate.getMonth() + 1;
const key = `${year}-${month}`;
yearMonthMap.set(key, (yearMonthMap.get(key) || 0) + 1);
}
});
return Array.from(yearMonthMap.entries())
.map(([key, count]) => {
const [year, month] = key.split('-').map(Number);
return {year, month, count};
})
.sort((a, b) => a.year - b.year || a.month - b.month);
}
}

View File

@@ -0,0 +1,54 @@
// ...existing code...
private processFinishedBooksStats(books: Book[]): FinishedBooksStats[] {
const yearMonthMap = new Map<string, number>();
books.forEach(book => {
if (book.dateFinished) {
const finishedDate = new Date(book.dateFinished);
const yearMonth = `${finishedDate.getFullYear()}-${(finishedDate.getMonth() + 1).toString().padStart(2, '0')}`;
yearMonthMap.set(yearMonth, (yearMonthMap.get(yearMonth) || 0) + 1);
}
});
// Get the range of months
const monthsWithData = Array.from(yearMonthMap.keys()).sort();
if (monthsWithData.length === 0) {
return [];
}
// Fill in all missing months between first and last data points
const startDate = this.parseYearMonth(monthsWithData[0]);
const endDate = this.parseYearMonth(monthsWithData[monthsWithData.length - 1]);
const completeStats: FinishedBooksStats[] = [];
let currentDate = new Date(startDate.year, startDate.month - 1, 1);
const endDateObj = new Date(endDate.year, endDate.month - 1, 1);
while (currentDate <= endDateObj) {
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const yearMonth = `${year}-${month.toString().padStart(2, '0')}`;
completeStats.push({
yearMonth,
count: yearMonthMap.get(yearMonth) || 0,
year,
month
});
// Move to next month
currentDate.setMonth(currentDate.getMonth() + 1);
}
return completeStats.sort((a, b) => a.yearMonth.localeCompare(b.yearMonth));
}
private parseYearMonth(yearMonthStr: string): { year: number, month: number } {
const [year, month] = yearMonthStr.split('-').map(Number);
return { year, month };
}
// ...existing code...

View File

@@ -184,7 +184,6 @@
</canvas>
</div>
}
@case ('bookFormats') {
<h3>Book Formats</h3>
<div class="chart-wrapper book-type-chart">
@@ -195,7 +194,6 @@
</canvas>
</div>
}
@case ('languageDistribution') {
<h3>Language Distribution</h3>
<div class="chart-wrapper language-chart">
@@ -206,7 +204,6 @@
</canvas>
</div>
}
@case ('bookMetadataScore') {
<h3>Book Metadata Score</h3>
<div class="chart-wrapper quality-chart">
@@ -217,7 +214,6 @@
</canvas>
</div>
}
@case ('topAuthors') {
<h3>Top 25 Authors (with Reading Breakdown)</h3>
<div class="chart-wrapper author-chart">
@@ -228,7 +224,6 @@
</canvas>
</div>
}
@case ('topCategories') {
<h3>Top 25 Categories (with Reading Breakdown)</h3>
<div class="chart-wrapper completion-chart">
@@ -239,7 +234,6 @@
</canvas>
</div>
}
@case ('topBooksBySize') {
<h3>Top 20 Largest Books by File Size</h3>
<div class="chart-wrapper book-size-chart">
@@ -250,7 +244,6 @@
</canvas>
</div>
}
@case ('monthlyReadingPatterns') {
<h3>Monthly Reading Patterns</h3>
<div class="chart-wrapper monthly-patterns-chart">
@@ -261,7 +254,6 @@
</canvas>
</div>
}
@case ('readingProgress') {
<h3>Reading Progress</h3>
<div class="chart-wrapper progress-chart">
@@ -272,7 +264,6 @@
</canvas>
</div>
}
@case ('externalRating') {
<h3>External Rating Distribution</h3>
<div class="chart-wrapper rating-chart">
@@ -283,7 +274,6 @@
</canvas>
</div>
}
@case ('personalRating') {
<h3>Personal Rating Distribution</h3>
<div class="chart-wrapper personal-rating-chart">
@@ -294,7 +284,6 @@
</canvas>
</div>
}
@case ('pageCount') {
<h3>Page Count Distribution</h3>
<div class="chart-wrapper page-count-chart">
@@ -305,7 +294,6 @@
</canvas>
</div>
}
@case ('readingVelocityTimeline') {
<h3>Reading Velocity Timeline</h3>
<div class="chart-wrapper velocity-timeline-chart">
@@ -316,7 +304,6 @@
</canvas>
</div>
}
@case ('topSeries') {
<h3>Top 20 Series by Book Count</h3>
<div class="chart-wrapper top-series-chart">
@@ -327,7 +314,6 @@
</canvas>
</div>
}
@case ('publicationYear') {
<h3>Publication Year Timeline</h3>
<div class="chart-wrapper year-chart">
@@ -339,19 +325,6 @@
</canvas>
</div>
}
@case ('finishedBooksTimeline') {
<h3>Books Finished Timeline (Per Month)</h3>
<div class="chart-wrapper finished-books-timeline-chart">
<canvas baseChart
[data]="(finishedBooksTimelineChartService.finishedBooksChartData$ | async) ?? {labels: [], datasets: []}"
[options]="finishedBooksTimelineChartService.finishedBooksChartOptions"
[type]="finishedBooksTimelineChartService.finishedBooksChartType"
[plugins]="[ChartDataLabels]">
</canvas>
</div>
}
@case ('readingDNA') {
<h3>Your Reading DNA Profile</h3>
<div class="chart-description">
@@ -384,7 +357,6 @@
}
</div>
}
@case ('readingHabits') {
<h3>Reading Habits Analysis</h3>
<div class="chart-description">
@@ -417,6 +389,16 @@
}
</div>
}
@case ('readingHeatmap') {
<h3>Books Finished per Month</h3>
<div class="chart-wrapper reading-heatmap-chart">
<canvas baseChart
[data]="(readingHeatmapChartService.heatmapChartData$ | async) ?? {labels: [], datasets: []}"
[options]="readingHeatmapChartService.heatmapChartOptions"
[type]="readingHeatmapChartService.heatmapChartType">
</canvas>
</div>
}
}
</div>
}

View File

@@ -530,6 +530,10 @@
height: 400px;
}
&.reading-heatmap-chart {
height: 300px;
}
&.reading-profile-radar-chart,
&.reading-dna-chart,
&.reading-habits-chart {
@@ -889,7 +893,11 @@
height: 280px;
}
&.reading-profile-radar-chart,
&.reading-heatmap-chart {
height: 250px;
}
.reading-profile-radar-chart,
&.reading-dna-chart,
&.reading-habits-chart {
height: 350px;
@@ -974,6 +982,10 @@
height: 250px;
}
&.reading-heatmap-chart {
height: 300px;
}
.reading-profile-radar-chart,
&.reading-dna-chart,
&.reading-habits-chart {

View File

@@ -1,6 +1,7 @@
import {Component, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {BaseChartDirective} from 'ng2-charts';
import {Chart, registerables, Tooltip} from 'chart.js';
import 'chartjs-chart-matrix';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {catchError, map, of, startWith, Subject, takeUntil} from 'rxjs';
@@ -23,12 +24,13 @@ import {BookSizeChartService} from './charts-service/book-size-chart.service';
import {ReadingVelocityTimelineChartService} from './charts-service/reading-velocity-timeline-chart.service';
import {MonthlyReadingPatternsChartService} from './charts-service/monthly-reading-patterns-chart.service';
import {TopSeriesChartService} from './charts-service/top-series-chart.service';
import {FinishedBooksTimelineChartService} from './charts-service/finished-books-timeline-chart.service';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import {ReadingDNAChartService} from './charts-service/reading-dna-chart.service';
import {ReadingHabitsChartService} from './charts-service/reading-habits-chart.service';
import {ChartConfigService, ChartConfig} from './charts-service/chart-config.service';
import {Button} from 'primeng/button';
import {ReadingHeatmapChartService} from './charts-service/reading-heatmap-chart.service';
import {MatrixController, MatrixElement} from 'chartjs-chart-matrix';
@Component({
selector: 'app-stats-component',
@@ -65,10 +67,10 @@ export class StatsComponent implements OnInit, OnDestroy {
protected readonly readingVelocityTimelineChartService = inject(ReadingVelocityTimelineChartService);
protected readonly monthlyReadingPatternsChartService = inject(MonthlyReadingPatternsChartService);
protected readonly topSeriesChartService = inject(TopSeriesChartService);
protected readonly finishedBooksTimelineChartService = inject(FinishedBooksTimelineChartService);
protected readonly readingDNAChartService = inject(ReadingDNAChartService);
protected readonly readingHabitsChartService = inject(ReadingHabitsChartService);
protected readonly chartConfigService = inject(ChartConfigService);
protected readonly readingHeatmapChartService = inject(ReadingHeatmapChartService);
private readonly destroy$ = new Subject<void>();
public isLoading = true;
@@ -94,7 +96,8 @@ export class StatsComponent implements OnInit, OnDestroy {
public readonly totalSize$ = this.librariesSummaryService.getFormattedSize().pipe(catchError(() => of('0 KB')));
ngOnInit(): void {
Chart.register(...registerables, Tooltip, ChartDataLabels);
Chart.register(...registerables, Tooltip, ChartDataLabels, MatrixController, MatrixElement);
// Register matrix chart - it's automatically registered when imported
Chart.defaults.plugins.legend.labels.font = {
family: "'Inter', sans-serif",
size: 11.5,