andrew Flower

Chart.js with Angular

Charts are an essential visual aid in representing data and, as such, there are many Javascript libraries available for providing charting abilities. Chart.js is a very popular and featureful charting Javascript library.

The demo below, using Chart.js, shows your scroll position on the current page over time. After trying a few other charting libraries which didn't seem to integrate easily with Angular, I found Chart.js. I'll show you how to add Chart.js to your Angular application too so that you can quickly start graphing data.

This article is targeting the following products and versions:

Angular 9
Chart.js 2.9

The accompanying source code is available here:

The Short Version

It's very easy to get this setup. So I'll rush through the few steps to create a simple chart component.

Adding the Chart.js dependencies

All that's required is to add the chart.js package as a dependency along with the corresponding Typescript package.

npm i chart.js @types/chart.js

Creating an Angular chart component

First we need to import Chart from the chart.js package.

import {Chart} from "chart.js";

In order to display the chart, Chart.js requires a canvas element. We can define this in the component template. For now we define explicit width and height. We also give it a name #chart in order to access it from our component's Typescript.

<canvas #chart width="600" height="200"></canvas>

Because we'll need access to the canvas element, we can only create the Chart after the view is initialized. So we use the AfterViewInit interface to hook into the lifecycle just after the view is ready. We can then create a chart in ngAfterViewInit, and our first version looks like below:

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

@Component({
  selector: 'app-line-chart',
  template: `
      <canvas #chart width="600" height="200"></canvas>
  `
})
export class LineChartComponent implements AfterViewInit {
  @ViewChild('chart')
  private chartRef: ElementRef;
  private chart: Chart;
  private data: Point[];

  constructor() {
    this.data = [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 6}, {x: 4, y: 2}, {x: 4.1, y: 6}];
  }

  ngAfterViewInit(): void {
    this.chart = new Chart(this.chartRef.nativeElement, {
      type: 'line',
      data: {
        datasets: [{
          label: 'Interesting Data',
          data: this.data,
          fill: false
        }]
      },
      options: {
        responsive: false,
        scales: {
          xAxes: [{
            type: 'linear'
          }],
        }
      }
    });
  }
}

Note lines 12 and 21 above, where the native canvas element is passed to the Chart constructor.

All that's left is to add this component to another component and you can see a chart.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-line-chart></app-line-chart>`
})
export class AppComponent {
  title = 'blog-angular-chartjs';
}

Making it responsive

In the current state we have to hard code the size of the chart canvas. This is usually not very useful in web applications where screen sizes, aspect ratios and resolutions vary so much.

Because sizing a canvas element using CSS will cause distortion and pixelation, using CSS to style the chart's canvas element directly is not possible. The Chart.js docs explain that way to control the size of the chart:

  1. It must be wrapped in another block element
  2. Any sizing constraints applied to this wrapping element will then affect the chart
  3. The wrapping element must have position: relative
  4. In addition, we need to set responsive to true in the Chart options.

We can use the host element of our component as the wrapping element, and therefore encapsulate this required styling within our Chart component. Note also that because Angular components are by default display: inline, we will change the display type to inline-block by default in order to give it a box.

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

@Component({
  selector: 'app-line-chart',
  template: `<canvas #chart></canvas>`,
  styles: [`
      :host {
          display: inline-block;
          position: relative;
      }
  `]
})
export class LineChartComponent implements AfterViewInit {
  @ViewChild('chart')
  private chartRef: ElementRef;
  private chart: Chart;
  private data: Point[];

  constructor() {
    this.data = [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 6}, {x: 4, y: 2}, {x: 4.1, y: 6}];
  }

  ngAfterViewInit() {
    this.chart = new Chart(this.chartRef.nativeElement, {
      type: 'line',
      data: {
        datasets: [{
          label: 'Interesting Data',
          data: this.data,
          fill: false
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          xAxes: [{
            type: 'linear'
          }],
        }
      }
    });
  }
}

Lines 8-11 style the host component as the wrapper for the chart, to allow for responsive behaviour. The display could be overridden when included in other components depending on what is desired. By default the element will start with the canvas' default size (300x150).

Lines 35-36 tells the chart to be responsive by monitoring the wrapper element, and allows it to grow with different aspect ratios.

Adding it to another component we can see that we now have a fully functional chart component.

import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
        <app-line-chart></app-line-chart>
        <app-line-chart style="width: 60%; height: 300px;"></app-line-chart>
        <app-line-chart style="display: block; height: 200px;"></app-line-chart>
    `
})
export class AppComponent {
  title = 'blog-angular-chartjs';
}

Passing In Data

If you are interested in a few ideas about how to pass data into your chart component read on into this section.

Static Data

Currently our chart component is hard-coded with some line graph data. The first start would be to provide this data as an input to the component.

info Note that this class has a new name and selector app-line-chart-with-data.
...
export class LineChartWithDataComponent implements AfterViewInit {
  @ViewChild('chart')
  private chartRef: ElementRef;
  private chart: Chart;
  @Input()
  data: Point[];

  constructor() {}

  ngAfterViewInit() {
    this.chart = new Chart(this.chartRef.nativeElement, {
      type: 'line',
      data: {
        datasets: [{
          label: 'Interesting Data',
          data: this.data,
          fill: false
        }]
      },
...

On Lines 6-7 we have exposed an Input called data, and we can add it now simply pass in static data from our parent component as below.

...
@Component({
  selector: 'app-root',
  template: `
      ...
      <app-line-chart-with-data [data]="staticData"></app-line-chart-with-data>
  `
})
export class AppComponent {
  staticData: Point[] = [{x: 1, y: 20}, {x: 2, y: 15}, {x: 3, y: 10}, {x: 4, y: 12}, {x: 5, y: 6}];
  title = 'blog-angular-chartjs';
}

In this example we just use a hard-coded array. But you can obviously pull this in from a service or Ajax call from within ngOnInit etc.

warning Modifying the original array of data will not update the array. After modifying the array, you would need to call the update() method of the chart as shown here.

Streaming Data

Static Data might not be enough. In fact, we are often looking to show dynamic data that updates in real time. In true Angular fashion, we should create a reactive component that accepts an Observable to subscribe to for data.

As an example let's assume we have some source of data (or "subject" in ReactiveX lingo) emitting individual Point data that we want to add to the chart.

We can keep the same point data field as the chart data source, but make it private. We will update this with data from our observable as it arrives and set the @Input on the observable data.

...
export class LineChartReactiveComponent implements AfterViewInit {
  @ViewChild('chart')
  private chartRef: ElementRef;
  private chart: Chart;
  @Input()
  private dataSource: Observable<Point>;
  private readonly data: Point[] = [];

  constructor() {}

  ngAfterViewInit() {
    this.chart = new Chart(this.chartRef.nativeElement, {
      ...
    });

    this.dataSource.subscribe(point => {
          this.data.push(point);
          this.chart.update();
    });
  }
}
On lines 17-20 the component subscribes to the observable and for each consumed point it adds it to the existing Chart data source. It then calls the update() function on the Chart so that it will rerender.
warning Depending on the rate of emission of the Observable's data source, it might be quite expensive to re-render after every consumed data point. Instead it could be better to inserts points in groups, based on time slices.
this.dataSource
    .pipe(bufferTime(100))
    .subscribe(points => {
      points.forEach(p => this.data.push(p));
      this.chart.update();
    });
Above the code is batching points every 200s using RxJS' bufferTime and only updating after inserting each batch.

As an example of passing in data, we can use interval to emit a random walk.

...
@Component({
  selector: 'app-root',
  template: `
      <app-line-chart-reactive [dataSource]="randomWalk"></app-line-chart-reactive>
  `
})
export class AppComponent {
  randomWalk: Observable<Point>;
  title = 'blog-angular-chartjs';

  constructor() {
    let last = 0;
    this.randomWalk = interval(30)
        .pipe(
            map(i => (
                {
                  x: i,
                  y: (last += Math.random() * 10 - 5)
                }
            ))
        );
  }
}

View this example on Stackblitz

Summary

In the above we learned the following:

  • How to include dependencies for Chart.js in an Angular application
  • How wrap a Chart.js chart in an Angular component
  • How to make the component responsive
  • Multiple ways to feed data into the component

All the examples here were focused on line charts, but there are many other available options with Chart.js, so have a look on the Chart.js documentation.

You can also make your chart components more configurable by adding @Inputs for the Chart options such as title, axis labels, colouring etc.

The accompanying source code is available here:

References