CREATING SIMPLE LINE CHARTS USING D3.JS — PART 01
--
D3.js is a javascript library used to make interactive data-driven charts. It can be used to make the coolest charts. It has a very steep learning curve. But once you understand the basics of D3.js, you can make the coolest charts work for you. In this tutorial, we are going to create a line chart displaying my 2020 internet Usage. I am going to take you through how to build a simple line chart using D3.js Library. As a prerequisite, I would request you to go through the first blog — D3.JS — DEMYSTIFIED in this series, that is if you are not already aware of d3.js basics.
Dataset
Bill DateInternetUsage01/01/2020001/02/202027701/03/202034301/04/202034201/05/202031001/06/202020801/07/202020901/08/202022001/09/202026701/10/202027901/11/202024401/12/2020339
WHAT IS A LINE-PLOT?
According to the Investopedia,
A line chart is a graphical representation of an asset’s historical price action that connects a series of data points with a continuous line. This is the most basic type of chart used in finance and typically only depicts a security’s closing prices over time. Line charts can be used on any timeframe, but most often using day-to-day price changes.
A line chart is a series of data points, connected by a line, can hold surprising stories along its peaks and valleys. Line charts have the power to display trends, highlight successes, and warn about potential dangers — all in seconds. That’s part of their appeal, and part of the reason they’re one of the most common ways to visualize data that changes over time. Let’s divide the line chart creation steps into different sub-steps
STEP 1: GETTING STARTED
First of all, we will import the D3.js library directly from the CDN inside our HTML. I would request you to download the latest d3 library file and place it in a folder where index.html is created. Hypertext Markup Language (HTML) is the most common language used to create documents on the World Wide Web. HTML uses hundreds of different tags to define a layout for web pages. Most tags require an opening <tag> and a closing </tag>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sample SuperStore Timeline</title>
</head>
<body>
<div id="wrapper"></div> <script src="./../../d3.v5.js"></script>
<script src="./chart.js"></script>
</body>
</html>
Let’s now move towards writing our JavaScript code.
<!DOCTYPE html>
The doctype should define the page as an HTML5 document
<html lang="en">
The HTML lang attribute is used to identify the language of text content on the web. This information helps search engines return language-specific results, and it is also used by screen readers that switch language profiles to provide the correct accent and pronunciation.
HTML <head> Tag
The <head> tag in HTML is used to define the head portion of the document which contains information related to the document.
HTML <div> Tag
The <div>
tag defines a division or a section in an HTML document.It is used as a container for HTML elements – which is then styled with CSS or manipulated with JavaScript.The <div>
tag is easily styled by using the class or id attribute.
HTML <script> Tag
The <script> tag in HTML is used to define the client-side script. The <script> tag contains the scripting statements, or it points to an external script file.
STEP 2 — SETTING UP AN NPM-BASED DEVELOPMENT ENVIRONMENT
Before we start please make sure you have NPM properly installed. NPM comes as part of the Node.js installation. You can download Node.js from http://nodejs.org/download/. Select the correct Node.js binary build for your OS. Once installed the npm
command will become available in your terminal console. When you type, you can see something like the below image.
https://riptutorial.com/d3-js/example/2955/installation
If you are using NPM, then you can install d3 by using the following command
npm install d3
Then type the below command for starting the Live Server
Live-server --quiet
If you are using VScode, you can also install Live-Server Plugin from the below link https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer
Some of you might be thinking why do we even need a Node Package manager for D3.js?
Node.js makes it possible to write applications in JavaScript on the server. It’s built on the V8 JavaScript runtime and written in C++ — so it’s fast. Originally, it was intended as a server environment for applications, but developers started using it to create tools to aid them in local task automation. Since then, a whole new ecosystem of Node-based tools (such as Grunt, Gulp and webpack) has evolved to transform the face of front-end development.
STEP 3: LOAD DATASET
Create a file within the same folder and rename it to Chart.js. As you can see, in your HTML File, we are referring to the Chart.js within the Script Tag.
Our next job is to define a function and named it as drawLineChart() . We will write all our code inside the function. We are also using async function which is used to specify the script is executed asynchronously. The word “async” before a function means one simple thing: a function always returns a promise. Other values are wrapped in a resolved promise automatically.
async function drawLineChart() {
// write your code here
}
drawLineChart()
Now we will load the Dataset by using the inbuilt D3 function.D3 can handle different types of data defined either locally in variables or from external files.D3 provides the following methods to load different types of data from external files.
MethodDescriptiond3.json() The d3.json() function is used to fetch the JSON filed3.csv()The d3.csv() function is used to load the csv filed3.xml()The d3.xml() method takes a url of xml file and returns an xml object
All of the code for this project can be viewed in its entirety in the pen below.
async function drawLineChart() {
//1. Load your Dataset
const dataset = await d3.csv("./../../Internet Usage.csv"); //Check the sample values available in the dataset
//console.table(dataset[0]);
}
drawLineChart()
await
literally suspends the function execution until the promise settles, and then resumes it with the promise result. That doesn’t cost any CPU resources, because the JavaScript engine can do other jobs in the meantime: execute other scripts, handle events, etc.
A promise is used to handle the asynchronous result of an operation. JavaScript is designed to not wait for an asynchrnous block of code to completely execute before other synchronous parts of the code can run. For instance, when making API requests to servers, we have no idea if these servers are offline or online, or how long it takes to process the server request.
With Promises, we can defer the execution of a code block until an async request is completed. This way, other operations can keep running without interruption.
Please refer to the below link which talks about await and Async in detail
https://medium.com/jspoint/javascript-promises-and-async-await-as-fast-as-possible-d7c8c8ff0abc
if you want to check whether the data is loaded correctly or not, use a Console.log() method, which is used to log the current value contained inside a specific variable. Console.log()
is a part of global window
object in the web browser. It also
prints the element in an HTML-like tree. Let’s run this command and see for ourselves.
STEP 4: DEFINE X AND Y VALUE
In our Line Chart, the y-axis (vertical) on the left comprised of Internet Usage values and x-axis (horizontal) on the bottom comprised of Bill date. To grab the correct metrics from each data point, we’ll need accessor functions. Accessor functions convert a single data point into the metric value.
const yAccessor = (d) => d.InternetUsage;
const dateParser = d3.timeParse("%d/%m/%Y");
const xAccessor = (d) => dateParser(d["Bill Date"]);
// Check the value of xAccessor function now
// console.log(xAccessor(dataset[0]));
Note: Unlike “natural language” date parsers (including JavaScript’s built-in parse), this method is strict. If the specified string does not exactly match the associated format specifier, this method returns null. For example, if the associated format is the full ISO 8601 string “%Y-%m-%dT%H:%M:%SZ”, then the string “2011–07–01T19:15:28Z” will be parsed correctly, but “2011–07–01T19:15:28”, “2011–07–01 19:15:28” and “2011–07–01” will return null, despite being valid 8601 dates.
console.log(xAccessor(dataset[0]));
STEP 5: CREATE CHART CONTAINERS
When drawing a chart, there are two containers whose dimensions we need to define: the wrapper and the bounds.
The wrapper contains the entire chart: the data elements, the axes, the labels, etc. Every SVG element will be contained inside here.
The bounds contain all of our data elements: in this case, our line.
This distinction will help us separate the amount of space we need for extraneous elements (axes, labels), and let us focus on our main task: plotting our data. One reason this is so important to define up front is the inconsistent and unfamiliar way SVG elements are sized.”
Excerpt From: Nate Murray. “Fullstack Data Visualization with D3”
// 2. Create a chart dimension by defining the size of the Wrapper and Margin let dimensions = {
width: window.innerWidth * 0.6,
height: 600,
margin: {
top: 115,
right: 20,
bottom: 40,
left: 60,
},
};
dimensions.boundedWidth =
dimensions.width - dimensions.margin.left - dimensions.margin.right;
dimensions.boundedHeight =
dimensions.height - dimensions.margin.top - dimensions.margin.bottom;// 3. Draw Canvas const wrapper = d3
.select("#wrapper")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height); //Log our new Wrapper Variable to the console to see what it looks like
//console.log(wrapper);
Here, we will select the Wrapper ID which we passed on our Index.html file. As we know, we can select all elements with an id (#id)
STEP 6:CREATE A BOUNDING CONTAINER?
We may often find ourselves needing to group elements together, allowing us to apply transformations or set attributes that are inherited by all child elements of that group. One way to achieve this is to use the container SVG element, <g>, allowing us to arrange our elements into groups. Then we will add a style with transform.translate() function to get the transformation whose translation tx1 and ty1 is equal to tx0 + tk and ty0 + tk, where tx refers to translation along the x-axis and ty refers to the translation along the y-axis.
// 4. Create a Bounding Box const bounds = wrapper
.append("g")
.style("transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
If we look at our Elements panel on our browser, we can see our new element with a <g style>. We can see that the element size is 0px by 0px — instead of taking a width or height attribute, an element will expand to fit its contents. When we start drawing our chart, we’ll see this in action
STEP 7: SETTING SCALES DOMAINS AND RANGES
To plot the max internet usage values in the correct spot, we need to convert them into pixel space. So, the idea of scaling is to take the values of data that we have and to fit them into space we have available. In a nutshell, the purpose is to ensure that the data we ingest fits onto our graph correctly. Since we have two different types of data (date and numeric values) they need to be treated separately. To examine this whole concept of scales, domains, and ranges properly, please refer to my first blog D3.js -demystified
Before going to the next section, I thoroughly recommend you read it (and plenty more of the great work by Fil) as he really does nail the description
https://observablehq.com/@d3/introduction-to-d3s-scales
To create a new scale, we need to create an instance of d3.scaleLinear().d3.scaleLinear constructs create a scale with a linear relationship between input and output. Then we will use the .domain function to let D3 know what the scope of the data will be and then passed to the scale function.
The d3-array module also has a d3.extent() method for grabbing those numbers. d3.extent() takes two parameters:
- an array of data points
- an accessor function which defaults to an identity function (d => d)
// 5. Define Domain and Range for Scales const yScale = d3
.scaleLinear()
.domain(d3.extent(dataset, yAccessor))
.range([dimensions.boundedHeight, 0]);
Here, the .extent function finds the maximum and minimum values in the array and then .domain function returns those maximum and minimum values to D3 as the range for the x-axis.
.range([dimensions.boundedHeight, 0])
SVG y-values count from top to bottom and in this case, the maximum & minimum number of pixels our point will be from the x-axis.
STEP 8: CREATE A REFERENCE BAND
Create a reference band that will act as a threshold for our data. I am keeping 100GB as a target. Let’s visualize this threshold by adding a rectangle. We just need to give it four attributes: x, y, width, and height
// console.log(yScale(100));
const referenceBandPlacement = yScale(100);
const referenceBand = bounds
.append("rect")
.attr("x", 0)
.attr("width", dimensions.boundedWidth)
.attr("y", referenceBandPlacement)
.attr("height", dimensions.boundedHeight - referenceBandPlacement)
.attr("fill", "#ffece6");
Similarly, create scales for X axis as we did for Y axis
const xScale = d3
.scaleTime()
.domain(d3.extent(dataset, xAccessor))
.range([0, dimensions.boundedWidth]);
STEP 9:CONVERT A DATAPOINT INTO X AND Y VALUE
The d3.line() method is used to constructs a new line generator with the default settings. The line generator is then used to make a line. d3.line() method will create a generator that converts data points into a d string This will transform our data points with both the Accessor function and the scale to get the Scaled value in Pixel Space
//6. Convert a datapoints into X and Y value const lineGenerator = d3
.line()
.x((d) => xScale(xAccessor(d)))
.y((d) => yScale(yAccessor(d)))
.curve(d3.curveBasis);
curveBasis() generates a curve in the line based on the cubic splines. You can omit the last line if you dont want a curvy line chart .
STEP 10: CONVERT X AND Y INTO PATH
This method is used to create a new path based on the X and Y data points.d3.path returns an object that implements the same path methods as a Canvas 2D context, but serializes them into SVG path data.
// 7. Convert X and Y into Path const line = bounds
.append("path")
.attr("d", lineGenerator(dataset))
.attr("fill", "none")
.attr("stroke", "Red")
.attr("stroke-width", 2);
STEP 11: GENERATE X AND Y AXIS
Axes can be drawn using built-in D3 functions. It is made of Lines, Ticks, and Labels.The d3.axisLeft() function in D3.js is used to create a left vertical axis. This function will construct a new left-oriented axis generator for the given scale, with empty tick arguments, a tick size of 6, and padding of 3.
When we call our axis generator, it will create a lot of elements, so it’s better to create a g element to hold all of those elements and keep our DOM organized
//8. Create X axis and Y axis
// Generate Y Axis const yAxisGenerator = d3.axisLeft().scale(yScale);
const yAxis = bounds.append("g").call(yAxisGenerator); // Generate X Axis
const xAxisGenerator = d3.axisBottom().scale(xScale);
const xAxis = bounds
.append("g")
.call(xAxisGenerator);
Why didn’t d3.axisBottom() draw the axis in the right place? d3’s axis generator functions know where to place the tick marks and tick labels relative to the axis line, but they have no idea where to place the axis itself.
To move the x-axis to the bottom, we can shift the x-axis group, similar to how we shifted our chart bounds using a CSS transform.
Excerpt From: Nate Murray. “Fullstack Data Visualization with D3”.
There are other two issues associated with X axis
- The datapoint showed “2020” instead of “Jan”
- The datapoint corresponding to “December” shows the truncated version of December
let’s fix this XAxisGenerator Code,
// Generate X Axis
const xAxisGenerator = d3.axisBottom().scale(xScale);
const xAxis = bounds
.append("g")
.call(xAxisGenerator.tickFormat(d3.timeFormat("%b,%y")))
.style("transform", `translateY(${dimensions.boundedHeight}px)`);
STEP 12: ADD A CHART TITLE
The chart caption is the first thing a user sees when they’re reading your data visualization — so it should be easy to understand. Don’t just use numbers, use words to describe those numbers.
wrapper
.append("g")
.style("transform", `translate(${50}px,${15}px)`)
.append("text")
.attr("class", "title")
.attr("x", dimensions.width / 2)
.attr("y", dimensions.margin.top / 2)
.attr("text-anchor", "middle")
.text("My 2020 Internet Usage(in GB) ")
.style("font-size", "36px")
.style("text-decoration", "underline");
CONCLUSION:
The line chart explanation mission is completed. Congratulations! We have successfully created the line chart using D3.js.From just the simple goal of creating a line chart, we have explored many core concepts core of D3:
- Selections
- Scales
- Axes
- Transformation
There is of course a lot more to D3 than that: data manipulation, layout generators, complex transitions. But, hopefully, this tutorial has given you the desire to go further.
In our next blog post, we will learn interaction and add a tooltip to this line chart.
Here is the complete code that I have used.
async function drawLineChart() {
//1. Load your Dataset
const dataset = await d3.csv("./../../Internet Usage.csv"); //Check the sample values available in the dataset
//console.table(dataset[0]); const yAccessor = (d) => d.InternetUsage;
const dateParser = d3.timeParse("%d/%m/%Y");
const xAccessor = (d) => dateParser(d["Bill Date"]); //Check the value of xAccessor function now
//console.log(xAccessor(dataset[0])); // 2. Create a chart dimension by defining the size of the Wrapper and Margin let dimensions = {
width: window.innerWidth * 0.6,
height: 600,
margin: {
top: 115,
right: 20,
bottom: 40,
left: 60,
},
};
dimensions.boundedWidth =
dimensions.width - dimensions.margin.left - dimensions.margin.right;
dimensions.boundedHeight =
dimensions.height - dimensions.margin.top - dimensions.margin.bottom; // 3. Draw Canvas const wrapper = d3
.select("#wrapper")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height); //Log our new Wrapper Variable to the console to see what it looks like
//console.log(wrapper); // 4. Create a Bounding Box const bounds = wrapper
.append("g")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
); // 5. Define Domain and Range for Scales const yScale = d3
.scaleLinear()
.domain(d3.extent(dataset, yAccessor))
.range([dimensions.boundedHeight, 0]); // console.log(yScale(100));
const referenceBandPlacement = yScale(100);
const referenceBand = bounds
.append("rect")
.attr("x", 0)
.attr("width", dimensions.boundedWidth)
.attr("y", referenceBandPlacement)
.attr("height", dimensions.boundedHeight - referenceBandPlacement)
.attr("fill", "#ffece6"); const xScale = d3
.scaleTime()
.domain(d3.extent(dataset, xAccessor))
.range([0, dimensions.boundedWidth]); //6. Convert a datapoints into X and Y value const lineGenerator = d3
.line()
.x((d) => xScale(xAccessor(d)))
.y((d) => yScale(yAccessor(d)))
.curve(d3.curveBasis); // 7. Convert X and Y into Path const line = bounds
.append("path")
.attr("d", lineGenerator(dataset))
.attr("fill", "none")
.attr("stroke", "Red")
.attr("stroke-width", 2); //8. Create X axis and Y axis
// Generate Y Axis const yAxisGenerator = d3.axisLeft().scale(yScale);
const yAxis = bounds.append("g").call(yAxisGenerator); // Generate X Axis
const xAxisGenerator = d3.axisBottom().scale(xScale);
const xAxis = bounds
.append("g")
.call(xAxisGenerator.tickFormat(d3.timeFormat("%b,%y")))
.style("transform", `translateY(${dimensions.boundedHeight}px)`); //9. Add a Chart Header wrapper
.append("g")
.style("transform", `translate(${50}px,${15}px)`)
.append("text")
.attr("class", "title")
.attr("x", dimensions.width / 2)
.attr("y", dimensions.margin.top / 2)
.attr("text-anchor", "middle")
.text("My 2020 Internet Usage(in GB) ")
.style("font-size", "36px")
.style("text-decoration", "underline");
}drawLineChart();
If you’re interested in learning more about D3.js, be sure to check Fullstack D3 course by Amelia Wattenberger
We covered a lot of ground here, but the core principles are relatively simple. In my next blog post, I’ll be covering how to add a tooltip to this simple Line chart. Once you get past the initial discomfort of learning the underpinning of D3 you can choose among thousands of chart types in D3 from D3 example galleries and modify them to your own needs.
We tried to cover as much as we could for a newbie to get started with D3.js. Hope you like it. As always, We welcome feedback and constructive criticism. If you enjoyed this blog, we’d love for you to hit the share button so others might stumble upon it. Please hit the subscribe button as well if you’d like to be added to my once-weekly email list, and don’t forget to follow Vizartpandey on Instagram!