Chart.js creating multiple Charts in Angular in same Component

Viewed 4934

I have an Angular 6 Project with a Component which gets an tile Object passed by its parrent. For every tile passend I want to generate the same Chart with chart.js. I works fine for the first Chart but all the others don't get rendered. The console Error Code: Failed to create chart: can't acquire context from the given item

My 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>

My tile.component.ts - Don't mind the comments, just for testing purposes

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
      }
    });
  }

}

And the parent tile-container.component.html - Not really necessary

<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>

Screnshot from missing charts

EDIT

This is my edited typescript code. every tile has an id which I tried to use to have a unique id for every chart created.

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());
    }
 }

This a the html where I used databinding.

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

Picture of error codes

2 Answers

in the template (html), the id for the canvas has to be different for each chart

i am going to give you diferrents approaches the first one it is the most simple the anothers you need a litle more knowledge...i hope it's help

1.- You can genera a single component just for render the chartjs graphic for example call it chart-dynamic with several inputs id for grabs the unique id you need for render several charts, and dataChart for all fully object to render , asuming that your tile. component looks like this

Important!! you dataChart must be thinking like a array of object and each object it is basicly a chart you will render into your template (follow the oficial documentacion of 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>

IN YOUR tile.component.ts Generate data method as array of objects, move generateChart function into the new component

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};
  }


}

now assuming you has created your new component should looks like this (you have already import charJs and another stuff)

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();
  }

}

app-chart-dynamic html file

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

it should work if you add into your modules and etc

the another approach is combine viewChild and viewChildren with factory resolver it is more complex but more power full you should check firts based on angular documentation

Related