Using Angular and Chart.js to build real-time charts

bartoszgajda55

Bartosz Gajda

Posted on August 10, 2020

Using Angular and Chart.js to build real-time charts

Angular and Chart.js is popular combination when creating any data visualization application. The first one can handle a very large throughput of data and the later is capable of rendering the plots in real-time, thanks to Canvas API. In this post I will guide you through the process of creating a real-time chart using Angular and Chart.js

Prerequisites

Before starting to write any code, make sure you have the following:

  • Node.js — I use version 13.2.0

  • Angular CLI — I use version 8.3.20

  • 10 minutes of free time

Creating new Angular project

The first step required is to create a new Angular project. As mentioned in prerequisites, I am using Angular CLI to do so and I highly advise you to do the same. Open a terminal window, navigate to desired directory and execute the command:

ng new angular-charts --routing=true --styling=scss

This command creates a new Angular project called angular-charts in directory of the same name. Additionally, I have added two optional flags — routing adds the router module to the app and styling sets the extensions of style sheets used.

With the project created, open it up in your IDE of choice — I will be using Visual Studio Code for this.

Adding a service layer*

The next step of this tutorial is to add a service layer. I have marked this step with asterisk, because it is optional. If you already have one, or you don’t need one, then feel free to skip this section.

Let’s start this section with generating a service that will give access to real-time data source using Observable. To generate a service, use the following command:

ng generate service sse

After executing the command, and SseService gets created and that's where the service layer code will be placed. For this tutorial I am using SSE or Server Sent Events data source, tutorial on which you can find here. If you need more explanation, don't hesitate to read that tutorial. To avoid repetition in this post, I will just paste in the following:

import { Injectable, NgZone } from "@angular/core";
import { Observable } from "rxjs";
@Injectable({
  providedIn: "root"
})
export class SseService {
  constructor(private _zone: NgZone) {}
  getServerSentEvent(url: string): Observable<any> {
    return Observable.create(observer => {
      const eventSource = this.getEventSource(url);
      eventSource.onmessage = event => {
        this._zone.run(() => {
          observer.next(event);
        });
      };
      eventSource.onerror = error => {
        this._zone.run(() => {
          observer.error(error);
        });
      };
    });
  }
  private getEventSource(url: string): EventSource {
    return new EventSource(url);
  }
}

Hooking up the Chart.js

The next step is to hook in Chart.js library into our Angular project. There are couple ways to do so, but I will use a dedicated package, called Ng2-Charts. This package exposes a much nicer API while retaining all the required functionality. In my case, I add the following dependencies to my package.json file:

"chart.js": "^2.9.3",
"ng2-charts": "^2.3.0",

After modifying the package.json file, don't forget to run either npm install or yarn depending on your package manager.

Adding HTML template

Going further, we have to add an HTML template that will render the chart. In the case of this tutorial, you can place it anywhere you fancy — the code is single HTML tag with custom properties which we will explore in next step. I place it in a component HTML template called count-events.component.html. The HTML template should include the following:

<canvas
    width="600"
    height="400"
    [datasets]="countEventsData"
    [chartType]="countEventsChartType"
    [labels]="countEventsLabels"
    [colors]="countEventsColors"
    [options]="countEventsOptions"
></canvas>

I have placed my chart is count-events folder, therefore all variables are prepended with those. In the canvas tag we specify height, width and variable configuration, which will be placed in corresponding .ts file.

Configuring Chart.js

As mentioned in chapter above, we will add some custom configuration to the Chart.js plots. This configuration will be placed in your components’ TypeScript file, in my case it is called count-events.component.ts.

The first thing that has to be set is the datasets property. That is a container that will hold the data displayed on the plot itself. The code for this should look like below:

countEventsData: ChartDataSets[] = [
  { data: [], label: "Number of Events", fill: false }
];

This variable is an array, meaning you can have many data sets displayed on a single plot. Inside of each element there are three core parts:

  • data - an array that holds the single values to be displayed on the chart

  • label - label of the data set

  • fill - configuration option setting the appearance of the data set on chart

Next configuration is the chartType property. That is a single string, flagging the type of chart that should be used. There is a wide variety of options available, including line, bar, graph or pie, but for this tutorial we are going to stick with the simplest one - line:

countEventsChartType = "line";

Going further, labels property has to be set. This element sets what labels the X axis receives. In our case however, we don't want to set them as a constant. We want to be able to update the labels in real-time, in cnjuction with the incoming data. This property therefore is set as empty array:

countEventsLabels: Label[] = [];

Next property is colors. The name itself is probably self explanatory, so I will jump straight to code:

countEventsColors: Color[] = [
    {
      borderColor: "#039BE5",
      pointBackgroundColor: "#039BE5"
    }
];

Last bit of configuration is called options. That is the central configuration point, for all major flags that can be set. The amount of available options is very broad, so please refer to Chart.js docs for complete documentation. In our case, we are only interested in removing the animations - that will optimize the chart and make it run faster. To do this, paste the following into your code:

countEventsOptions: ChartOptions = {
    animation: {
      duration: 0
    }
 };

Connecting Service and Chart.js

Last chapter of this tutorial will show you how to glue the service and the Chart.js together. To make this happen, we will implement couple functions in the count-events.component.ts file.

We start off with subscribing to the data source, which is an SseService in our case. That is done in the ngOnInit hook, so that we connect to data source whenever our component is loaded in the application. In here, we create a Subscription to the endpoint and call pushEventToChartData function.

private countEventsSubscription$: Subscription;
ngOnInit() {
    this.countEventsSubscription$ = this.sseService
      .getServerSentEvent("http://localhost:8082/count-events")
      .subscribe(event => {
        let data = JSON.parse(event.data);
        this.pushEventToChartData(data);
      });
  }

The aforementioned function have a simple purpose — it checks whether the datasets have reached an arbitrary limit (20 in this case) and if so, removes the last element before pushing the new one into this collection. On thing has the kept in mind - if adding or removing elements, it has to be done for both datasets collections and labels collections. Both of them have to by kept synced all the time.

private pushEventToChartData(event: CountEvents): void {
    if (this.isChartDataFull(this.countEventsData, 20)) {
      this.removeLastElementFromChartDataAndLabel();
    }
    this.countEventsData[0].data.push(event.count);
    this.countEventsLabels.push(
      this.getLabel(event)
    );
  }

The last pieces of code include the helper functions calls to which can be found in snippet above. First functions could be used to implement some prettier looking labels. Second one removes the last element from both datasets and labels collections. The third checks whether the a collections has reached its limit, which I have set to be 20 in my case. The snippets for those are as follows:

private getLabel(event: CountEvents): string {
    return `${event.window}`;
  }

  private removeLastElementFromChartDataAndLabel(): void {
    this.countEventsData[0].data = this.countEventsData[0].data.slice(1);
    this.countEventsLabels = this.countEventsLabels.slice(1);
  }

  private isChartDataFull(chartData: ChartDataSets[], limit: number): boolean {
    return chartData[0].data.length >= limit;
  }

Wrapping this all up, the complete code for count-events.component.ts file looks like this:

export class CountEventsComponent implements OnInit, OnDestroy {
  private countEventsSubscription$: Subscription;
  private eventsOnChartLimit = 20;
  countEventsChartType = "line";
  countEventsData: ChartDataSets[] = [
    { data: [], label: "Number of Events", fill: false }
  ];
  countEventsLabels: Label[] = [];
  countEventsColors: Color[] = [
    {
      borderColor: "#039BE5",
      pointBackgroundColor: "#039BE5"
    }
  ];
  countEventsOptions: ChartOptions = {
    animation: {
      duration: 0
    }
  };

  constructor(private sseService: SseService) {}

  ngOnInit() {
    this.countEventsSubscription$ = this.sseService
      .getServerSentEvent("http://localhost:8082/count-events")
      .subscribe(event => {
        let data = JSON.parse(event.data);
        this.pushEventToChartData(data);
      });
  }

  private pushEventToChartData(event: CountEvents): void {
    if (this.isChartDataFull(this.countEventsData, 20)) {
      this.removeLastElementFromChartDataAndLabel();
    }
    this.countEventsData[0].data.push(event.count);
    this.countEventsLabels.push(
      this.getLabel(event)
    );
  }

  private getLabel(event: CountEvents): string {
    return `${event.window}`;
  }

  private removeLastElementFromChartDataAndLabel(): void {
    this.countEventsData[0].data = this.countEventsData[0].data.slice(1);
    this.countEventsLabels = this.countEventsLabels.slice(1);
  }

  private isChartDataFull(chartData: ChartDataSets[], limit: number): boolean {
    return chartData[0].data.length >= limit;
  }

  ngOnDestroy() {
    this.countEventsSubscription$.unsubscribe();
  }
}

And that this tutorial finished. Using Angular and Chart.js is not a rocket science and the benefits of having a real-time charts can be huge.

Summary

I hope you have found this post useful. If so, don’t hesitate to like or share this post. Additionally you can follow me on my social media if you fancy so :)

💖 💪 🙅 🚩
bartoszgajda55
Bartosz Gajda

Posted on August 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related