Build a live graph with D3.js
You will need Node and npm installed on your machine.
Visual representations of data are one of the most effective means of conveying complex information and D3.js provides great tools and flexibility to create these data visualizations.
D3.js is a JavaScript library used for producing dynamic, interactive data visualizations in web browsers using SVG, HTML and CSS.
In this tutorial, we’ll explore how to build a realtime graph with D3.js and Pusher Channels. If you want to play around with the code as you read this tutorial, check out this GitHub repository, which contains the final version of the code.
Prerequisites
To complete this tutorial, you need to have Node.js and npm installed. The versions I used while creating this tutorial are as follows:
- Node.js v10.4.1
- npm v6.3.0
You also need to have http-server installed on your machine. It can be installed through npm
by running the following command: npm install http-server
.
Although no Pusher knowledge is required, a basic familiarity with JavaScript and D3.js will be helpful.
Getting started
To get started, create a new directory for the app we will build. Call it realtime-graph
or whatever you like. Inside the newly created directory, create a new index.html
file and paste in the following code:
//index.html
<!DOCTYPE html>
<hml lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="style.css">
<title>Realtime D3 Chart</title>
</head>
<body>
<script src="https://js.pusher.com/4.2/pusher.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="app.js"></script>
</body>
</html>
As you can see, the HTML file is just pulling up the styles and scripts we need to build the graph. We’re making use of D3.js to build the chart and Pusher to add realtime functionality. The app.js
file is where the code for the frontend of the app will be written.
Before we start implementing the chart, let’s add the styles for the app in style.css
:
// style.css
html {
height: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
}
*, *::before, *::after {
box-sizing: inherit;
}
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #ffffff 0%,#e8f1f5 100%);
}
.container {
position: absolute;
padding: 20px;
top: 50%;
left: 50%;
background-color: white;
border-radius: 4px;
transform: translate(-50%, -50%);
box-shadow: 0px 50px 100px 0px rgba(0,0,102,0.1);
text-align: center;
}
.container h1 {
color: #333;
}
.bar {
fill: #6875ff;
border-radius: 2px;
}
.bar:hover {
fill: #1edede;
}
.tooltip {
opacity: 0;
background-color: rgb(170, 204, 247);
padding: 5px;
border-radius: 4px;
transition: opacity 0.2s ease;
}
Install the server dependencies
Assuming you have Node and npm
installed, run the following command to install all the dependencies we will need for the server component of the application:
npm install express dotenv cors pusher
Pusher setup
Head over to the Pusher website and sign up for a free account. Select Channels apps on the sidebar, and hit Create Channels app to create a new app.
Once your app is created, retrieve your credentials from the API Keys tab, then add the following to a new variables.env
file in the root of your project directory.
// variables.env
PUSHER_APP_ID=<your app id>
PUSHER_APP_KEY=<your app key>
PUSHER_APP_SECRET=<your app secret>
PUSHER_APP_CLUSTER=<your app cluster>
Set up the server
Now that we’ve installed the relevant dependencies and our Pusher account has been setup, we can start building the server.
Create a new file called server.js
in the root of your project directory and paste in the following code:
// server.js
require('dotenv').config({ path: 'variables.env' });
const express = require('express');
const cors = require('cors');
const poll = [
{
name: 'Chelsea',
votes: 100,
},
{
name: 'Arsenal',
votes: 70,
},
{
name: 'Liverpool',
votes: 250,
},
{
name: 'Manchester City',
votes: 689,
},
{
name: 'Manchester United',
votes: 150,
},
];
const app = express();
app.use(cors());
app.get('/poll', (req, res) => {
res.json(poll);
});
app.set('port', process.env.PORT || 4000);
const server = app.listen(app.get('port'), () => {
console.log(`Express running → PORT ${server.address().port}`);
});
Save the file and run node server.js
from the root of your project directory to start the server.
Set up the app frontend
The frontend of the application will be written in the app.js
file we referenced earlier. Create this file in the root of your project directory and paste the following code therein:
// app.js
// set the dimensions and margins of the graph
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// set the ranges for the graph
const x = d3
.scaleBand()
.range([0, width])
.padding(0.1);
const y = d3.scaleLinear().range([height, 0]);
// append the container for the graph to the page
const container = d3
.select('body')
.append('div')
.attr('class', 'container');
container.append('h1').text('Who will win the 2018/19 Premier League Season?');
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
const svg = container
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Create a skeleton structure for a tooltip and append it to the page
const tip = d3
.select('body')
.append('div')
.attr('class', 'tooltip');
// Get the poll data from the `/poll` endpoint
fetch('http://localhost:4000/poll')
.then(response => response.json())
.then(poll => {
// add the x Axis
svg
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x-axis')
.call(d3.axisBottom(x));
// add the y Axis
svg
.append('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(y));
update(poll);
});
function update(poll) {
// Scale the range of the data in the x axis
x.domain(
poll.map(d => {
return d.name;
})
);
// Scale the range of the data in the y axis
y.domain([
0,
d3.max(poll, d => {
return d.votes + 200;
}),
]);
// Select all bars on the graph, take them out, and exit the previous data set.
// Enter the new data and append the rectangles for each object in the poll array
svg
.selectAll('.bar')
.remove()
.exit()
.data(poll)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', d => {
return x(d.name);
})
.attr('width', x.bandwidth())
.attr('y', d => {
return y(d.votes);
})
.attr('height', d => {
return height - y(d.votes);
})
.on('mousemove', d => {
tip
.style('position', 'absolute')
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY + 20}px`)
.style('display', 'inline-block')
.style('opacity', '0.9')
.html(
`<div><strong>${d.name}</strong></div> <span>${d.votes} votes</span>`
);
})
.on('mouseout', () => tip.style('display', 'none'));
// update the x-axis
svg.select('.x-axis').call(d3.axisBottom(x));
// update the y-axis
svg.select('.y-axis').call(d3.axisLeft(y));
}
In the code block above, we’ve created a basic bar chart using the initial data received via the /poll
endpoint. If you’re familiar with how D3 works, the code should be familiar to you. I’ve added comments in key parts of the code to walk you through how the chart is constructed.
In a new terminal, start a development server to serve the index.html
file:
npx http-server
I’m using http-server
here, but you can use whatever server you want. You can even open index.html
in the browser directly.
At this point, your graph should look like this:
Update the graph in realtime with Pusher
Let’s make sure that updates to the poll can be reflected in the app’s frontend in realtime with Pusher Channels. Paste the following code at the end of the app.js
file.
// app.js
const pusher = new Pusher('<your app key>', {
cluster: '<your app cluster>',
encrypted: true,
});
const channel = pusher.subscribe('poll-channel');
channel.bind('update-poll', data => {
update(data.poll);
});
Here, we opened a connection to Channels and used the subscribe()
method from Pusher to subscribe to a new channel called poll-channel
. Updates to the poll are listened for via the bind
method, and the update()
function is invoked with the latest data once an update is received so that the graph is re-rendered.
Don’t forget to replace the <your app key>
and <your app cluster>
placeholders with the appropriate details from your Pusher account dashboard.
Trigger updates from the server
We’re going to simulate a poll that updates every second and use Pusher to trigger an update when the data changes so that subscribers to the poll (the client) can receive the updated data in realtime.
Add the following code at the top of server.js
below the other imports:
const Pusher = require('pusher');
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
encrypted: true,
});
function getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function increment() {
const num = getRandomNumber(0, poll.length);
poll[num].votes += 20;
}
function updatePoll() {
setInterval(() => {
increment();
pusher.trigger('poll-channel', 'update-poll', {
poll,
});
}, 1000);
}
Then change the /poll
endpoint to look like this:
app.get('/poll', (req, res) => {
res.json(poll);
updatePoll();
});
The /poll
route sends the initial poll data to the client and calls the updatePoll()
function which increments the votes for a random club at three second intervals and triggers an update on the poll-channel
which we created on the client in the last step.
Kill your server and restart it by running node server.js
from the root of your project directory. At this point, you should have a bar graph that updates in realtime.
Conclusion
You have seen the procedure for creating a bar graph with D3.js and how to it in realtime with Pusher Channels. It was easy enough, wasn’t it?
We have covered a simple use case for Pusher and D3 but one that’s only scratching the surface. I recommend digging into the docs to find more about Pusher and other awesome features it has.
Thanks for reading! Remember that you can find the complete source code for this tutorial in this GitHub repository.
24 August 2018
by Ayooluwa Isaiah