Build a realtime graph using JavaScript

Introduction

The world needs everything uber-fast now. There are plenty of data streams being generated by different systems everyday. Realtime monitoring and analysis has become very important for taking decisions in many industries today. For example: realtime monitoring of website traffic, server performance, weather updates, IOT sensors etc. It is important to analyse and interpret this burst of data, for which interactive Charts and Graphs are an excellent solution.

In this blog post, we will be building a Node.js Server to expose APIs to provide historic data for a metric (in this case, weather in London City) and also provide an API to ingest new data points. We will also be building a front end app with a Line Chart to display the temperature changes in London weather in realtime. The application we build will look something like this:

build-realtime-graph-app-javascript

Signup for Pusher

The first step to start this tutorial is to Signup at Pusher or login with your existing credentials if you already have an account. After logging in, you will need to create a new app and select 'Vanilla JS' for the front end along with 'Node.js' for the backend. You will then be brought to a landing page containing the 'getting started' code for both front end and backend which we will use later on in the tutorial.

Node.js Server APIs for Monitoring & Analytics System

The most essential APIs for any analytics systems for any metric or entity are:

  • Ingestion API - An API to ingest the new data points for any particular entity. In our server for this blog post, we will make an API to ingest new temperature data at a particular time for London city. This API can be called by any global weather system or any IOT sensor.
  • Historical Data API - This API will return all the data within a range from this date in time. For our server, we will create a simple API which will return some static historical data with limited data points for London's temperature values for any day.

Node.js Express Server Skeleton

We will create a basic Express Server along with instantiating the Pusher library server instance. We will create a new folder for our project and create a new file server.js. Add the following code to this file:

1var express = require('express');
2var path = require('path');
3var bodyParser = require('body-parser');
4
5var Pusher = require('pusher');
6
7var pusher = new Pusher({
8  appId: '<your-app-id>',
9  key: '<your-app-key>',
10  secret: '<your-app-secret>',
11  cluster: '<your-app-cluster>',
12  encrypted: true
13});
14
15var app = express();
16
17app.use(bodyParser.json());
18app.use(bodyParser.urlencoded({ extended: false }));
19app.use(express.static(path.join(__dirname, 'public')));
20
21// Error Handler for 404 Pages
22app.use(function(req, res, next) {
23    var error404 = new Error('Route Not Found');
24    error404.status = 404;
25    next(error404);
26});
27
28module.exports = app;
29
30app.listen(9000, function(){
31  console.log('Example app listening on port 9000!')
32});

API to get historical temperature data

Now, we will add some static data regarding London's temperature at certain times during a day and store it in any JavaScript variable. We will also expose a route to return this data whenever someone invokes it using a GET HTTP call.

1var londonTempData = {
2    city: 'London',
3    unit: 'celsius',
4    dataPoints: [
5      {
6        time: 1130,
7        temperature: 12 
8      },
9      {
10        time: 1200,
11        temperature: 13 
12      },
13      {
14        time: 1230,
15        temperature: 15 
16      },
17      {
18        time: 1300,
19        temperature: 14 
20      },
21      {
22        time: 1330,
23        temperature: 15 
24      },
25      {
26        time: 1406,
27        temperature: 12 
28      },
29    ]
30  }
31
32app.get('/getTemperature', function(req,res){
33  res.send(londonTempData);
34});

API to ingest temperature data point

Now we will add the code for exposing an API to ingest the temperature at a particular time. We will expose a GET HTTP API with temperature and time as query parameters. We will validate that they are not empty and store them by pushing in the dataPoints array of our static Javascript variable londonTempData. Please add the following code to the server.js file:

1app.get('/addTemperature', function(req,res){
2  var temp = parseInt(req.query.temperature);
3  var time = parseInt(req.query.time);
4  if(temp && time && !isNaN(temp) && !isNaN(time)){
5    var newDataPoint = {
6      temperature: temp,
7      time: time
8    };
9    londonTempData.dataPoints.push(newDataPoint);
10    pusher.trigger('london-temp-chart', 'new-temperature', {
11      dataPoint: newDataPoint
12    });
13    res.send({success:true});
14  }else{
15    res.send({success:false, errorMessage: 'Invalid Query Paramaters, required - temperature & time.'});
16  }
17});

In the above code, apart from storing in the data source, we will also trigger an event 'new-temperature' on a new channel 'london-temp-chart'. For every unique data source or a chart, you can create a new channel.

The event triggered by our server will be processed by the front end to update the chart/graph in realtime. The event can contain all the important data which the chart needs to display the data point correctly. In our case, we will be sending the temperature at the new time to our front end.

Building the Front End App using Vanilla JS & Chart.js

Now, we will build the front end application to display a Line Chart representing the changes in temperature for London City at different times throughout the day. The key approach for displaying realtime graphs is

  • We have to make an initial Ajax call to fetch historical data, and render the graph with the existing data.
  • We will subscribe to any events for new data points being stored on a particular channel.

Basic HTML Template

We will create a new folder called public in our project root and then create a new file index.html in this folder. This file will contain the basic HTML code to render a simple header and a sub-header with the app name along with few icons. We will also import the Pusher Javascript library from its CDN URL.

1<!DOCTYPE>
2<html>
3    <head>
4        <title>Realtime Analytics</title>
5        <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous">
6        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200">
7        <link rel="stylesheet" href="./style.css">
8        <meta name="viewport" content="width=device-width, initial-scale=1.0">
9    </head>
10    <body>
11        <header>
12            <div class="logo">
13                <img src="./assets/pusher-logo.png" />
14            </div>
15            <div id="logout" class="logout">
16               <a href="/logout">Logout</a>
17            </div>
18        </header>
19        <section class="subheader">
20            <img class="weather-icon" src="./assets/weather.png" />
21            <h2>Realtime Weather Updates for London !</h2>
22            <img class="weather-icon" src="./assets/london-icon.png" height="70px" />
23        </section>
24        <section>
25           <div id="loader" class="loader">
26           </div>
27        </section>
28        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
29        <script type="text/javascript" src="./app.js"></script>
30    </body>
31</html>

Adding Charts Library

In Javascript and HTML apps, we have to use either of the two technologies to build graphical components to represent mathematical graphs, SVG or Canvas. There are numerous open source libraries which can help you render different chart types, such as Bar Charts, Pie Charts, Line Charts and Scatter Charts. Here are links to a few of the popular libraries:

For our project, we will choose Chart.js as it has fairly simple API and renders robust charts using Canvas HTML tag. You can choose any charting library but keep in mind that the library should have a means to update the chart without completely re-rendering it. Chart.js provides a method on any instantiated chart to update it.

Add the following code to your index.html file at appropriate places

1...
2<section>
3   <div id="loader" class="loader">
4    Loading...
5   </div>
6   <canvas id="weatherChart">
7   </canvas>
8</section>
9<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.bundle.min.js"></script>
10<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
11...

Adding JS File & Instantiating Pusher client side library

Now we will create a new file app.js in our public folder and also add the following code to instantiate the Pusher client side library.

1// Using IIFE for Implementing Module Pattern to keep the Local Space for the JS Variables
2(function() {
3    // Enable pusher logging - don't include this in production
4    Pusher.logToConsole = true;
5
6    var serverUrl = "/",
7        members = [],
8        pusher = new Pusher('<your-api-key>', {
9          encrypted: true
10        }),
11        channel,weatherChartRef;
12
13    function showEle(elementId){
14      document.getElementById(elementId).style.display = 'flex';
15    }
16
17    function hideEle(elementId){
18      document.getElementById(elementId).style.display = 'none';
19    }
20
21    function ajax(url, method, payload, successCallback){
22      var xhr = new XMLHttpRequest();
23      xhr.open(method, url, true);
24      xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
25      xhr.onreadystatechange = function () {
26        if (xhr.readyState != 4 || xhr.status != 200) return;
27        successCallback(xhr.responseText);
28      };
29      xhr.send(JSON.stringify(payload));
30    }
31
32})();

In the above code, we have also added few utility methods to make an Ajax call and also show or hide elements from the DOM API.

Adding Code to fetch Historical Data

Now, we will add the code to fetch the historical temperature data to display the graph with the initial values. We will also instantiate a new Chart object with a specific config to render a Line Chart. You can read more about how to construct these configs at the Chart.js documentation.

Please add the following code to the app.js file:

1function renderWeatherChart(weatherData) {
2      var ctx = document.getElementById("weatherChart").getContext("2d");
3      var options = { };
4      weatherChartRef = new Chart(ctx, {
5        type: "line",
6        data: weatherData,
7        options: options
8      });
9   }
10
11   var chartConfig = {
12      labels: [],
13      datasets: [
14         {
15            label: "London Weather",
16            fill: false,
17            lineTension: 0.1,
18            backgroundColor: "rgba(75,192,192,0.4)",
19            borderColor: "rgba(75,192,192,1)",
20            borderCapStyle: 'butt',
21            borderDash: [],
22            borderDashOffset: 0.0,
23            borderJoinStyle: 'miter',
24            pointBorderColor: "rgba(75,192,192,1)",
25            pointBackgroundColor: "#fff",
26            pointBorderWidth: 1,
27            pointHoverRadius: 5,
28            pointHoverBackgroundColor: "rgba(75,192,192,1)",
29            pointHoverBorderColor: "rgba(220,220,220,1)",
30            pointHoverBorderWidth: 2,
31            pointRadius: 1,
32            pointHitRadius: 10,
33            data: [],
34            spanGaps: false,
35         }
36      ]
37   };
38
39   ajax("/getTemperature", "GET",{}, onFetchTempSuccess);
40
41   function onFetchTempSuccess(response){
42      hideEle("loader");
43      var respData = JSON.parse(response);
44      chartConfig.labels = respData.dataPoints.map(dataPoint => dataPoint.time);
45      chartConfig.datasets[0].data = respData.dataPoints.map(dataPoint => dataPoint.temperature);
46      renderWeatherChart(chartConfig)
47  }

In the above code, we have added a function named renderWeatherChart which will be used to render the chart using latest data which is embedded in the chartConfig variable under the key datasets. If we want to draw multiple line charts on same canvas, we can add more elements to this array.

The data key in each of the elements of the array will display the different points on the graph. We will make an ajax request to the /getTemperature api to fetch all the data points and put them into this key. We will call the rendering method to display the graph then. Now we can run the command node server.js and then go to the browser with the following URL to see the initial chart rendered using the data.

http://localhost:9000/

In order to style our app properly, please add the following CSS into a new style.css file inside the public folder. Add the following code to that file:

1body{
2    margin:0;
3    padding:0;
4    overflow: hidden;
5    font-family: Raleway;
6}
7
8header{
9    background: #2b303b;
10    height: 50px;
11    width:100%;
12    display: flex;
13    color:#fff;
14}
15
16.logo img{
17  height: 45px;
18}
19
20.subheader{
21    display: flex;
22    align-items: center;
23    margin: 0px;
24}
25
26.logout{
27    flex:1;
28    justify-content: flex-end;
29    padding:15px;
30    display: none;
31}
32
33.logout a{
34    color:#fff;
35    text-decoration: none;
36}
37
38#weatherChart{
39    height: 80% !important;
40    width: 95% !important;
41    margin: 0 auto;
42}

Code to Update Graph on new event received

Now we want to subscribe to the unique channel on which our server will be sending update events for this graph. For our project the channel is named london-temp-chart and the event is named new-temperature. Please add the following code to process the event and then update the chart in realtime:

1channel = pusher.subscribe('london-temp-chart');
2channel.bind('new-temperature', function(data) {
3    var newTempData = data.dataPoint;
4    if(weatherChartRef.data.labels.length > 15){
5      weatherChartRef.data.labels.shift();  
6      weatherChartRef.data.datasets[0].data.shift();
7    }
8    weatherChartRef.data.labels.push(newTempData.time);
9    weatherChartRef.data.datasets[0].data.push(newTempData.temperature);
10    weatherChartRef.update();
11});

In order to see this code in action, you have to refresh the browser and you will see the initial chart. Now we have to ingest a new data point, for which you would need to call the following API either by using some mock API calling tool or using the following URL with different values in the browser.

http://localhost:9000/addTemperature?temperature=17&time=1500

In order to test your chart update code, you can use the following temporary code in your app.js Javascript file which will make dummy Ajax requests to the above URL after a specific time interval.

1/* TEMP CODE FOR TESTING */
2  var dummyTime = 1500;
3  setInterval(function(){
4    dummyTime = dummyTime + 10;
5    ajax("/addTemperature?temperature="+ getRandomInt(10,20) +"&time="+dummyTime,"GET",{},() => {});
6  }, 1000);
7
8  function getRandomInt(min, max) {
9      return Math.floor(Math.random() * (max - min + 1)) + min;
10  }
11/* TEMP CODE ENDS */

The Github repo for reference to complete code is https://github.com/mappmechanic/realtime-analytics

Conclusion

Finally our realtime analytics app is ready. We will see the weather temperature chart for London city updating in realtime.

build-realtime-graph-app-javascript

We can use the code from this blog post for any charts library and also to render any type of chart like Bar Chart, Scatter Chart or Pie Chart to update in realtime.

This code can also be used in multiple Enterprise Apps like Monitoring dashboards, analytics reports, sensor regulatory apps, financial apps etc. The Pusher library helps us to send realtime events to all connected client side apps which can consume the data to update the charts in realtime.