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:
- It must be wrapped in another block element
- Any sizing constraints applied to this wrapping element will then affect the chart
- The wrapping element must have
position: relative
- In addition, we need to set
responsive
totrue
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.
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.
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, OnDestroy {
@ViewChild('chart')
private chartRef: ElementRef;
private chart: Chart;
@Input()
private dataSource: Observable<Point>;
private readonly data: Point[] = [];
private dataSourceSubscription: Subscription;
constructor() {}
ngAfterViewInit() {
this.chart = new Chart(this.chartRef.nativeElement, {
...
});
this.dataSourceSubscription =
this.dataSource.subscribe(point => {
this.data.push(point);
this.chart.update();
});
}
ngOnDestroy() {
this.dataSourceSubscription.unsubscribe();
}
}
update()
function on the Chart so that it
will rerender.
this.dataSource
.pipe(bufferTime(100))
.subscribe(points => {
points.forEach(p => this.data.push(p));
this.chart.update();
});
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)
}
))
);
}
}
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 @Input
s for the Chart options
such as title, axis labels, colouring etc.
The accompanying source code is available here:
References
Bitcoin
Zap me some sats
This blog post itself is licensed under a Creative Commons Attribution 4.0 International License. However, the code itself may be adapted and used freely.