Búsqueda de sitios web

javascript - Chart.js creando múltiples gráficos en Angular en el mismo componente


Tengo un proyecto Angular 6 con un componente al que se le pasa un objeto tile por su padre. Para cada tile pasado quiero generar el mismo gráfico con chart.js. Funciona bien para el primer gráfico, pero todos los demás no se renderizan. El código de error en la consola: Failed to create chart: can't acquire context from the given item

Mi tile.component.html

<div *ngIf="tile.type === 'tileImg'">
  <div class="card custom-card"
       routerLinkActive="glowing">
    <img class="card-img-top rounded" src="{{ tile.imgPath }}" alt="Tile image" />
    <div class="card-body">
      <p class=" card-text text-center">{{ tile.name }}</p>
    </div>
  </div>
</div>
<div *ngIf="tile.type === 'tileChart'">
  <div class="card custom-card"
       routerLinkActive="glowing">
    <div>
      <canvas id="canvas">{{ chart }}</canvas>
    </div>
    <div class="card-body">
      <p class=" card-text text-center">{{ tile.name }}</p>
    </div>
  </div>
</div>

Mi archivo tile.component.ts - No te preocupes por los comentarios, solo son para fines de prueba

import { Component, OnInit, Input } from '@angular/core';
import { Chart } from 'chart.js';

import { Tile } from 'src/app/tile-container/tile/tile.model';
//import { TileChart } from 'src/app/tile-container/tile/tile-chart.model';

@Component({
  selector: 'app-tile',
  templateUrl: './tile.component.html',
  styleUrls: ['./tile.component.css']
})
export class TileComponent implements OnInit {
  @Input() tile: Tile;
  //tileChart: TileChart;
  chart = [];

  constructor() { }

  ngOnInit() {
    //console.log(this.tile);
    //console.log(this.tile.getType());
    //console.log(this.tile.getChartType() + "   " + this.tile.getChartData() + "  " + this.tile.getType().localeCompare('tileChart'));
    //console.log(this.tile.getType() == 'tileChart');
    if (this.tile.getType() == 'tileChart') {
      this.generateChart(this.tile.getChartType(), this.tile.getChartData());
      
    }
  }

  generateChart(chartType: string, chartData: number[]) {
    

    this.chart = new Chart('canvas', {
      type: chartType,
      data: {
        datasets: [{
          data: chartData,
          backgroundColor: ['#F39E01', '#b8bbc1']
        }],
        labels: [
          'Verbrauch diese Woche',
          'Einsparung in kWh'
        ]
      },
      options: {
        legend: {
          display: false,
        },
        rotation: 1.1 * Math.PI,
        circumference: 0.8 * Math.PI
      }
    });
  }

}

Y el componente padre tile-container.component.html - Realmente no es necesario

<div class="container custom-container">
  <div class="container-heading">
    <h2>{{ tileContainer.name }}</h2>
  </div>
  <hr />
  <div class="row">
    <div class="col text-center"
         *ngFor="let tile of tileContainer.tiles">
      <app-tile      
        [tile]="tile">
      </app-tile>
    </div>
  </div>
</div>

Captura de pantalla de gráficos faltantes

EDITAR

Este es mi código de TypeScript editado. Cada ficha tiene un id que intenté usar para tener un id único para cada gráfico creado.

ngOnInit() {
    console.log(this.tile.id);
    if (this.tile.getType() == 'tileChart') {
      this.chartId = this.tile.id.toString();
      this.ctx = document.getElementById(this.chartId);
      console.log(this.ctx);
      this.generateChart(this.tile.getChartType(), this.tile.getChartData());
    }
 }

Este es el HTML donde usé enlace de datos.

<div>
  <p>{{ chartId }}</p>
  <canvas id="{{ chartId }}">{{ chart }}</canvas>
</div>

Imagen de códigos de error

Respuesta #1

en la plantilla (html), el id del lienzo tiene que ser diferente para cada gráfico

Respuesta #2

Voy a darte diferentes enfoques, el primero es el más simple, los demás necesitas un poco más de conocimiento... espero que sea de ayuda

1.- Puedes generar un solo componente solo para renderizar el gráfico de Chart.js, por ejemplo llamarlo chart-dynamic con varias entradas: id para obtener el ID único que necesitas para renderizar varios gráficos, y dataChart para todo el objeto completo a renderizar, asumiendo que tu componente tile se ve así

¡Importante! tu dataChart debe ser pensado como un arreglo de objetos y cada objeto es básicamente un gráfico que renderizarás en tu plantilla (sigue la documentación oficial de ChartJs)

<div *ngIf="tile.type === 'tileImg'">
  <div class="card custom-card"
       routerLinkActive="glowing">
    <img class="card-img-top rounded" src="{{ tile.imgPath }}" alt="Tile image" />
    <div class="card-body">
      <p class=" card-text text-center">{{ tile.name }}</p>
    </div>
  </div>
</div>
<div *ngIf="tile.type === 'tileChart'">
  <div class="card custom-card"
       routerLinkActive="glowing">
<!-- NEW CODE -->

<ng-container *ngIf="dataChart?.length > 0" >
<div *ngFor="let chart of dataChart; let i=index">
    <app-chart-dynamic [id]="SomeUniqueID" [dataChart]="chart" [type]="chart.type"></app-chart-dynamic>
</div>
</ng-container>

<!-- Finish here -->
    <div class="card-body">
      <p class=" card-text text-center">{{ tile.name }}</p>
    </div>
  </div>
</div>

EN SU tile.component.ts genere el método de datos como un arreglo de objetos, mueva la función generateChart al nuevo componente

import { Component, OnInit, Input } from '@angular/core';
import { Chart } from 'chart.js';

import { Tile } from 'src/app/tile-container/tile/tile.model';
//import { TileChart } from 'src/app/tile-container/tile/tile-chart.model';

@Component({
  selector: 'app-tile',
  templateUrl: './tile.component.html',
  styleUrls: ['./tile.component.css']
})
export class TileComponent implements OnInit {
  @Input() tile: Tile;
  //tileChart: TileChart;
  chart = [];
  public dataChart: [];

  constructor() { }

  ngOnInit() {
    //console.log(this.tile);
    //console.log(this.tile.getType());
    //console.log(this.tile.getChartType() + "   " + this.tile.getChartData() + "  " + this.tile.getType().localeCompare('tileChart'));
    //console.log(this.tile.getType() == 'tileChart');
    this.getCharts();
  }

  public getCharts() { 
    // call data from you service or data mock
    this.dataChart = {....response};
  }


}

ahora, suponiendo que has creado tu nuevo componente, debería verse así (ya has importado ChartJs y otras cosas)

import { Component, OnInit, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { Chart } from 'chart.js';

@Component({
  selector: 'app-chart-dynamic',
  templateUrl: './chart-dynamic.component.html',
  styleUrls: ['./chart-dynamic.component.css']
})
export class ChartDynamic implements OnInit, AfterViewInit {

  @Input() datasChart: any;
  @Input() id: string;
  @Input() type?: string;
  public idChart: any;
  @ViewChild('chart') chart: ElementRef;
  public chartObject: any;

  constructor() { }

  ngOnInit() {

  }

  generateChart(id: string ,chartType?: string, chartData: any) {
    this.idChart = this.id;
    this.chart = new Chart(`${this.idChart}`,  this.datasChart );
  }

  ngAfterViewInit() {
    this.drawGraphics();
  }

}

archivo html de app-chart-dynamic

<div class="some-class-style" >
    <canvas [id]="id" #chart> {{ chart }}</canvas>
</div>

debería funcionar si lo agregas a tus módulos, etc.

el otro enfoque es combinar viewChild y viewChildren con factory resolver, es más complejo pero más poderoso, deberías revisarlo primero basado en la documentación de Angular