How to optimize chart rendering using Chart.js in Ionic?

Discussão em 'Angular' iniciado por georgio bou sleimen, Março 4, 2025.

  1. I'm building an expense manager application where I have a stats tab containing two subtabs, expenses and incomes each of which has its own chart instance. Rendering is first slow, it takes some seconds for the chart in an animated way at the initialization of the tabs, second, chart is becoming undefined when going back and forth between tabs so it's not appearing lets say for the expenses tab, im getting errors like : Chart with id "0" has to be deleted before using chart with id "myChart1" (although I don't know where is chart id 0 coming from) and getting sometimes an error like (can't read properties of null: reading id 'myChart1'). I tried using ngAfterViewInit first but it didn't work, then i tried using a try catch block and it kind of fixed the issue but it still is there unexpectedly. Then i added a timeout function to retry the chart creation since it could've been that chart component in HTML is not rendered properly already, so give it another chance, but still I get the bugs I told you about before. So what's the best way to write the code below ?

    <ion-content [fullscreen]="true">
    @if (!loading){
    <div style="position: relative; height: 100%;" id="expense-chart" *ngIf="!noData">

    <div id="chart">
    <ion-title class="chart-title">Expenses:</ion-title>
    <canvas id="myChart1" style="height: 20%; width: 20%;"></canvas>
    </div>

    </div>
    }
    @else {
    <ion-spinner name="crescent" id="spinner" style="--width: 500px; --height: 500px;"></ion-spinner>

    }

    @if (!loading){
    <div class="percentages" *ngIf="!noData">
    <ion-title class="chart-title">Percentages:</ion-title>
    <div class="percentages-container">
    <div *ngFor="let pair of expensePercentages; let i = index" class="percentage">
    <ion-label id="category">
    {{pair['category']}}:</ion-label>
    <ion-label>{{pair['percentage'] | percent}}</ion-label>
    </div>
    </div>
    </div>

    }

    <div *ngIf="noData" id="no-data">
    <div class="no-data">
    <ion-title>No data available</ion-title>
    </div>
    </div>
    </ion-content>


    import { AfterViewInit, Component, createComponent, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    import { ArcElement, CategoryScale, Chart, DoughnutController, Legend, LinearScale, PieController, registerables, Title, Tooltip } from 'chart.js';
    import { FirestoreService } from '../firestore.service';
    import { ActivatedRoute } from '@angular/router';
    import { Observable } from 'rxjs';
    import { BudgetService } from '../budget.service';
    import {IonicModule} from '@ionic/angular';

    Chart.register(ArcElement, Tooltip, Legend, Title, CategoryScale, LinearScale, DoughnutController, PieController);
    @Component({
    selector: 'app-expenses-stats',
    templateUrl: './expenses-stats.page.html',
    styleUrls: ['./expenses-stats.page.scss'],
    standalone: true,
    imports: [IonicModule, CommonModule, FormsModule]
    })
    export class ExpensesStatsPage implements OnInit, OnDestroy, AfterViewInit{

    noData: boolean = false;
    loading: boolean = true;
    labels: string[] = [];
    selectedMonth$!: Observable<number>;
    selectedMonth: number = new Date().getMonth();

    changedBudget$!: Observable<'Expense' | 'Income' | null>;
    expensePercentages!: {category: string, percentage: number}[] ;
    public myChart1: any;

    constructor(private firestoreService: FirestoreService, private route: ActivatedRoute,
    private budgetService: BudgetService
    ) {
    this.changedBudget$ = budgetService.changedBudget$;
    this.selectedMonth$ = firestoreService.month$;
    this.selectedMonth$.subscribe(month => {
    this.selectedMonth = month;
    this.createChart();
    });

    this.changedBudget$.subscribe(type => {
    if (type === 'Expense') {
    this.createChart();
    }
    });
    }

    ngOnInit() {
    // this.createChart();
    }

    ngAfterViewInit(): void {
    this.createChart();
    }
    ngOnDestroy(): void {
    if (this.myChart1) {
    this.myChart1.destroy();
    this.myChart1 = null;
    }
    }

    async createChart() {
    this.loading = true;
    this.noData = false;

    if (this.myChart1) {
    this.myChart1.destroy();
    this.myChart1 = null;
    }

    const uid = localStorage.getItem('userId')!;
    this.labels = await this.firestoreService.getCategories('Expense');
    const data = await this.firestoreService.getExpenseData(uid, this.selectedMonth);

    if (Object.keys(data).length === 0) {
    this.noData = true;
    this.loading = false;
    return;
    }
    let arrayData = [];
    let total = 0;
    arrayData = this.labels.map((label) => {
    const value = data[label] || 0;
    total += value;
    return value;
    });

    console.log("Array Data: ", arrayData);
    this.expensePercentages = arrayData.map((value, index) => {
    return {
    category: this.labels[index],
    percentage: (value / total)
    };
    });

    const options = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
    tooltip: {
    callbacks: {
    // Format the tooltip label to include the '$' symbol
    label: function(tooltipItem: any) {
    console.log("Tooltip itemraw: ", tooltipItem.raw);

    return '$' + tooltipItem.raw.toFixed(2); // Use toFixed to show two decimal places
    }
    }
    }
    },
    layout: {
    padding: {
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    },
    },
    };

    const chartData = {
    labels: this.labels,
    datasets: [{
    data: arrayData,
    backgroundColor: this.generateHexColors(this.labels.length)
    }]
    };
    try {
    setTimeout(() => {
    this.myChart1 = new Chart('myChart1', {
    type: 'pie',
    data: chartData,
    options: options
    });
    },500);
    } catch (error) {
    this.createChart();
    }
    this.loading = false;

    }

    generateHexColors(n: number): string[] {
    const colors: string[] = [];
    const step = Math.floor(0xffffff / n); // Ensure colors are spaced evenly

    for (let i = 0; i < n; i++) {
    const colorValue = (step * i) % 0xffffff; // Calculate the color value
    const hexColor = `#${colorValue.toString(16).padStart(6, '0')}`;
    colors.push(hexColor);
    }

    return colors;
    }
    }

    Continue reading...

Compartilhe esta Página