Creating multiple charts with Chart.js in an Angular component is a breeze

Working on an Angular 6 Project, I have a Component that receives a tile Object from its parent. The goal is to generate a Chart using chart.js for each passed tile. The issue is that only the first Chart gets rendered successfully, while the rest fail with the following console Error Code:

Failed to create chart: can't acquire context from the given item

Here is a snippet of 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>

Additionally, in my tile.component.ts file:

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

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

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

  constructor() { }

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

}

As an example, the parent tile-container.component.html looks like:

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

Take a look at the Screnshot from missing charts for reference.

EDIT

I've made some edits to my typescript code by introducing a unique id for each 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());
    }
 }

Utilizing data binding in the following html:

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

Check out the Picture of error codes for more details.

Answer №1

Each chart in the template (html) must have a unique canvas id.

Answer №2

There are different approaches to consider. The simplest one involves creating a single component specifically for rendering the Chart.js graphic. For example, you can name it chart-dynamic and include inputs for grabbing the unique ID needed to render multiple charts, as well as dataChart for the complete object to render. Make sure your dataChart is structured as an array of objects, with each object representing a chart to be rendered in your template (refer to the official Chart.js documentation).

<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 file, generate a method that returns an array of objects, and move the 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';

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

  constructor() { }

  ngOnInit() {
    this.getCharts();
  }

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

}

Assuming you have created your new component, it should look like this:

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>

Make sure to add the necessary elements to your modules and other files for everything to work properly.

Another approach involves combining ViewChild and ViewChildren with a factory resolver. This method is more complex but offers more flexibility. Check the Angular documentation for more details.

Answer №3

main.html

<h5 class="mb-2">Pie Chart</h5>
  <div style="width: 90%" class="mb-4">
    <canvas id="pie-chart">{{ pieChart }}</canvas>
  </div>
  <h5 class="mb-2">Doughnut Chart</h5>
  <div style="width: 90%" class="mb-4">
    <canvas id="doughnut-chart">{{ doughnutChart }}</canvas>
  </div>

It is crucial to properly initialize your variables ('pieChart' & 'doughnutChart') following the TypeScript syntax as shown below.

MAIN_COMPONENT.ts

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

export class MAIN_COMPONENT implements OnInit {
  public pieChart: chartjs<"pie", string[], string> | null = null;
  public doughnutChart: chartjs<"doughnut", string[], string> | null = null;

  constructor() {}

  ngOnInit(): void {
    this.setupCharts()
  }

  public setupCharts() {
    this.pieChart = new chartjs('pie-chart', {
      type: 'pie', 
      data: {
        labels: ['A', 'B', 'C', 'D'],
        datasets: [
          {
            label: "Category",
            data: ['25', '30', '15', '30'],
            backgroundColor: ['#FF5733', '#33FF57', '#3357FF', '#5733FF']
          }
        ]
      },
      options: {
        aspectRatio: 2.5,
        responsive:true,
      }
    });
    this.doughnutChart = new chartjs("doughnut-chart", {
      type: 'doughnut', 
      data: {
        labels: ['X', 'Y', 'Z'],
        datasets: [
          {
            label: "Values",
            data: ['50', '30', '20'],
            backgroundColor: ['#FF5733', '#33FF57', '#3357FF']
          }
        ]
      },
      options: {
        aspectRatio: 2.5,
        responsive:true,
      }
    });
  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Invoking a plugin function within a page function results in failure

After setting up a plugin named helper.js inside plugins/ directory, I encountered an issue where the plugin's functions did not work when called inside another function. Below is my code snippet for helper.js: import Vue from 'vue' imp ...

Ways to showcase the object on the console

How can I display the object function in the console? When I try, nothing is displayed. Can you please help me figure out what went wrong? I know I must have made a mistake somewhere, as this is my first question on Stack Overflow. import React, ...

"Successfully uploading files with Ajax on Postman, however encountering issues when attempting to do so in

I am currently working on allowing specific users to upload videos to the API using Ajax. The process works smoothly in Postman, however, when attempting the same action from a web browser, I encounter a 500 Internal Server Error. Unfortunately, I do not ...

What causes TypeScript to struggle with inferring types in recursive functions?

Here is a code snippet for calculating the sum: //Function to calculate the sum of an array of numbers let sum = ([head, ...tail]: number[]) => head ? head + sum(tail) : 0 let result: string = sum([1, 2, 3]); alert(result); Can anyone explain why ...

Sending string variable from Perl CGI script to HTML frontend

Having just begun experimenting with AJAX, I'm encountering difficulties in passing variables back to my HTML script. In this setup, I'm using a combination of a XAMPP webserver, JavaScript, and jQuery for the HTML script, along with a Perl cgi ...

Identify and remove numbers from an array that have the same digit, without having prior knowledge of which digit it

Is there an easy way to separate values in an array of random numbers between 0-99 based on their first and second digits? I need two arrays: one for values that share the same first digit, and another for values that share the same second digit. The actua ...

Passing conditional empty variables through `res.render`

I am currently facing a challenge with passing multiple variables into a res.render. One of the variables will have an object to send through, while the other may not have anything to pass. As a result, I am encountering an undefined error. Below is the ...

Why are my API routes being triggered during the build process of my NextJS project?

My project includes an API route that fetches data from my DataBase. This particular API route is triggered by a CRON job set up in Vercel. After each build of the project, new data gets added to the database. I suspect this might be due to NextJS pre-exe ...

Unable to automatically prompt the button inside the iframe

In this scenario, an Iframe is automatically generated by a JavaScript script. I am looking to simulate a click by triggering a button, but unfortunately, it is not working as expected. You can view the code on JSFiddle. I have attempted various approache ...

Learn how to easily insert a marker on a map using leaflet js in vue 3 with just a simple click

Hey everyone! I need some help with a challenge I'm facing. I can't seem to get click coordinates to create a new Marker on my map. Check out the image here And another image here ...

Checkbox in Meteor always returns false after the template has been rendered

I've created a navigation bar with 2 options. One option is a checkbox and the other is a dropdown with a button (code provided below). The checkbox has the ID "inputMode" and the button has the ID "addNewObject" <div class="collapse navbar-colla ...

Encountering an issue with Angular 13 routing where extraction of property fails when returning value as an observable

After receiving an id number from the parent component, I pass it to my child component in order to retrieve a value with multiple properties. To achieve this, I created a service that returns an observable containing the desired object. However, when atte ...

jquery is unable to locate text

UPDATE: I have recently implemented a function that calculates and displays the length of a certain element, but it currently requires user interaction to trigger it: function check() { alert($("#currentTechnicalPositions").html().length); } After s ...

The program was expecting an array to start, but instead encountered an object. Any suggestions on how to convert

{ "workingHours": [ { "date":"2023-02-01", "amount":3, "freigegeben":false } ] } Whenever I include this in my re ...

I am seeking to perform these operations solely using pure WebGL, without relying on the Three.js library

if( keyboard.pressed("up")) pobjects[movementControls.translate].translateX(1); if( keyboard.pressed("down")) pobjects[movementControls.translate].translateX(-1); if( keyboard.pressed("left")) pobjects[mo ...

Update the image on a webpage within a template using AJAX code

I manage a website that utilizes templates. Within the template, there is a main image that I need to replace on specific pages, not throughout the entire site. I am seeking a way to change this main image to a new one on select pages using Ajax. Upon re ...

What is the best way to retrieve the items stored within the array generated by a JavaScript function?

This particular function is responsible for calling a PHP script that outputs JSON data. It then iterates through the JSON object, creates a new object, and adds this new object to an array which is ultimately returned by the function. function getTestQues ...

Having trouble with React's useEffect and React-Query's useQuery?

As a React newbie, I'm trying to implement global error handling using a context provider and a custom hook. Main Objective: Implementing a system to handle errors at the global level. The Issue: Errors reappear immediately after being removed. I s ...

The external javascript file is unable to recognize the HTML table rows that are dynamically inserted through an AJAX request

I have a situation where I'm pulling data from an SQL database and integrating it into my existing HTML table row. Here's the code snippet: Using Ajax to fetch data upon clicking analyze_submit: $(document).ready(function(e) { $('#anal ...

Create a jQuery URL within the $.ajax function

Attempting to execute a $.ajax call and send data to a php file. The php file is situated in a component's directory, while the js file is located in the webroot directory. How can the url section of the $.ajax be configured correctly to point to the ...