Maybe it just occurred to you that you haven't visualized any data before since the start of your coding journey, or perhaps you haven't found a good blog explaining how data can be visualized.
Say no more.
In this blog post, we are going through the process of displaying data with a bar-chart using my freeCodeCamp data visualization project as a real life demo.
We would also be using a javaScript library called D3.js, for those unfamiliar with D3 it is a JavaScript library for producing dynamic, interactive data visualizations in web browsers.
The first step is to add the D3 script tag in the head of your html
.
<script src="https://d3js.org/d3.v7.min.js"></script>
With that added, congrats we can now visualize data.
I am keeping in mind that d3 can be a very confusing thing to grasp at first sight, which is why I am going to break it down into much smaller bit so you understand why we do what we do.
The HTML
1. Add an svg element in your html:
This can be done manually by adding the following line of code
<svg id="chart"></svg>
Your svg id
be anything you like as long as it relates to what is being done.
Now, adding an svg doesn't do anything but hold the rest of the element we are going to put in it later. Before moving on to the javascript aspect, our bar chart needs an heading.
2. Add a text element:
we can again just include this with a text
tag directly in our svg
, add the line below in the svg
tag.
<text id="title" x="280" y="40">United States GDP</text>
Not a fan of Maths? Don't be confused the x
and y
above only helps with the positioning as it doesn't behave like a normal html element, x is the left and y is the top think of it as a margin left and margin top.
In summary our html file should look similar to this
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bar chart</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<svg id="canvas">
<text id="title" x="280" y="40">United States GDP</text>
</svg>
</body>
</html>
Some CSS
If you aren't ready to give your bar chart some styling, I suggest giving the svg
at least a background-color
so you can see the changes that will be made, for this project I have a few styles added, you can customize this to suit your taste.
body {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #4a4e69;
font-family: Arial, Helvetica, sans-serif;
}
svg {
background-color: white;
border-radius: 10px;
padding: 10px;
}
#title {
font-size: 20px;
}
#tooltip {
margin-top: 20px;
font-size: 18px;
color: white;
}
rect {
fill: #4a4e69;
}
rect:hover {
fill: #f2e9e4
}
With html
and css
out of the way, lets get to why you are here.
The JavaScript
1. Defining some variables:
In your script
we are going to define the variables that are going to be used later in the creation of our bar chart, add the code below in your script
let values = [];
let heightScale;
let xScale;
let xAxisScale;
let yAxisScale;
let width = 800;
let height = 600;
let padding = 40;
The values
array is empty because later on it's going to contain the data we are going to get from an api.
The heightScale
is what will determine the height of each of the bars as it cannot have say a constant value like 50 or 60, if that were so then all bars will have the same height irrespective of the value it represents.
The xScale
will determine where the bars are placed horizontally in the chart.
The xAxisScale
will be use to create the xAxis
at the bottom while the yAxisScale
will be used to create to the yAxis
on the left, you can think of it as the x and y axis on a graph.
The width
, height
and padding
are attributes that are going to be added to the svg
.
2. Select the svg
:
To select an element in normal javascript we usually go through the route of document.getElementById()
or document.querySelector()
or whatnot but because we are using d3 here we can just do this
let svg = d3.select('svg')
and that's it, you have selected the svg
element in your html and saved it in a variable called svg
, if there are more than one svg
in the document then it will return the first element, you can also select by the id
or class
of the element for example
let svg = d3.select('#chart')
both are valid ways of selecting an element in d3.
3. Create functions:
Now that we have our variables set, we need to create some functions that will be called to do one thing or another in order to create our bar chart.
I. Let's drawChart()
:
The svg
is where all other element are going to sit like the xAxis
, yAxis
and the bars
. For that we need to define the width
and height
of it using a function.
Add the code below after selecting your svg
let drawChart = () => {
svg.attr('width', width)
.attr('height', height)
}
The above code is a simple arrow function that takes the svg
variable and adds an attribute to it. The d3 .attr()
takes two argument first is the attribute we want to add which in this case is the width
and height
and second is the value.
II. Let's fetch()
some data:
Before we proceed creating the bar chart, we need to fetch the data
that the bar chart will be created with, now freeCodeCamp gave us an api to work with.
https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json
You can open this up in a new tab to see how the data is structured.
To fetch this data I am going simply use javaScript's own fetch
method, add the below code after the drawChart()
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json') //get the data
.then(res => res.json()) //convert the data to json
.then(data => {
values = data.data //save the data in the values array
console.log(values) //just so you can see that values now contains the data
});
javascript
The code above simply gets the data and stores them in the values
array, if you check your console you should see this
Lastly within the fetch
method we want to call the functions we'll be creating later, add the code below to your fetch
drawChart()
generateScales()
drawBars()
generateAxes()
You should have something like this
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then(res => res.json())
.then(data => {
values = data.data;
console.log(values)
drawChart()
generateScales()
drawBars()
generateAxes()
});
III. Let's generateScales()
:
We need to create the scales that the axis are going to use, add the below code after your drawChart()
let generateScales = () => {
heightScale = d3.scaleLinear()
.domain([0, d3.max(values, (item) => {
return item[1]
})])
.range([0, height - (2 * padding)])
xScale = d3.scaleLinear()
.domain([0, values.length - 1])
.range([padding, width - padding])
}
javascript
First, we defined the heightScale
by calling d3.scaleLinear()
function, after that we had to call a domain
, a domain
is simply telling it that the values
we are expecting here have a maximum and minimum, take the code below for an example
const arr = [100, 200, 300, 400, 500, 600];
In the array above the minimum number present is 100 and the maximum is 600. That is essentially what the domain does, it gets the min and max of the data.
But the domain can't magically know the min and max of the data, we have to tell it by opening an array in it and specifying the lowest value as the first item and highest value as the second.
.domain([0, ])
Since we are working with GDP data we know for sure that the lowest can only be 0 else we'd have worked with d3.min()
to find the lowest value in the array.
Now that the min of the domain is set we have to find the max as the second item in the domain array, now in real life one cannot always know the exact figure and it maybe subject to change in the future, that's why the below code is added as the second item in the array, it takes the values array and maps over each item before returning the highest value of the second index.
d3.max(values, (item) => {
return item[1]
})
That's how we have this
.domain([0, d3.max(values, (item) => {
return item[1]
})])
Now to the range
, it also takes in an array with two values, the first is the min then the max, take the code below for an example
// our data
const arr = [100, 200, 300, 400, 500, 600];
If you think about it how can one represent a huge value like 600 should the height then be 600px tall? Then what happens when we work in thousands or millions how to be present the height of that bar representing the value? That is what the range
does
const arr = [100, 200, 300, 400, 500, 600];
// we can say, to represent this data the range can be;
.range([10, 100])
What this does is that the value of 100 will be represented at the height of 10 because it is the min and 600 will be represented at the height of 100 because it is the max, likewise the rest of the value's height will be calculated within the range we have set for it.
I hope this code now makes more sense to you
.range([0, height - (2 * padding)])
It sets the min range to 0 and the max to the height of the svg take away the padding on both sides so there's a bit of a space.
Then we defined our xScale similarly
xScale = d3.scaleLinear()
.domain([0, values.length - 1])
.range([padding, width - padding])
This scale is used to position the xAxis
which would sit horizontally at the bottom.
Moving on, in the generateScale()
right after the xScale
add the following code.
let datesArr = values.map(item => {
return new Date(item[0]);
})
console.log(datesArr) // so you can see
xAxisScale = d3.scaleTime()
.domain([d3.min(datesArr), d3.max(datesArr)])
.range([padding, width - padding])
yAxisScale = d3.scaleLinear()
.domain([0, d3.max(values, (item) => {
return item[1]
})])
.range([height - padding, padding])
Now in order to have this bottom axis paired with dates below
We need to convert the string date from our data and change it to an actual date, if you studied the data you will see that the date for each value is at the index of 0
For this conversion to happen we created a datesArr
to hold the newly converted dates
let datesArr = values.map(item => {
return new Date(item[0]);
})
If you console.log(datesArr)
you'll find our array containing the newly converted dates
After that, we defined the xAxisScale
by calling the d3.scaleTime()
because the values we are working with are dates, then we called the domain
and range
, likewise for the yAxisScale
.
IV. Let's generateAxes()
:
Now that the hard part which is generateScales()
is out of the way it's time to start with our both of our axis, add this code below the generateScales()
function.
let generateAxes = () => {
let xAxis = d3.axisBottom(xAxisScale)
svg.append('g')
.call(xAxis)
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height - padding})`);
let yAxis = d3.axisLeft(yAxisScale);
svg.append('g')
.call(yAxis)
.attr('id', 'y-axis')
.attr('transform', `translate(${padding}, 0)`)
}
Within the function above we defined an xAxis
representing the x
of our graph, before calling it with the d3.axisBottom(xAxisScale)
method and giving it the xAxisScale
Now to display this on screen, a g
element is appended the already selected svg
then we called the xAxis
which is essentially telling it to draw the xAxis
within the g
let xAxis = d3.axisBottom(xAxisScale)
svg.append('g')
.call(xAxis)
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height - padding})`);
An id
attribute is added to the g
and a transform
attribute which if not added would naturally sit at the top of the svg, try taking away the transform
attribute and you will have something like this
After that the yScale
is defined and called with the d3.axisLeft()
, just like we did for the xAxis
, another g
is appended to the svg
then we told it to draw the yAxis
within it, then an id
attribute is given to it and a transform
attribute to position it better within the svg
, try taking the transform
out to see its natural position.
let yAxis = d3.axisLeft(yAxisScale);
svg.append('g')
.call(yAxis)
.attr('id', 'y-axis')
.attr('transform', `translate(${padding}, 0)`)
So in total we have something like this
V. Let's drawBars()
:
Finally we are going to be filling the graph with bars representing its data, add the code below after the generateScales()
let drawBars = () => {
svg.selectAll('rect')
.data(values)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('width', (width - (2 * padding)) / values.length)
.attr('data-date', (item) => {
return item[0];
})
.attr('data-gdp', (item) => {
return item[1];
})
.attr('height', (item) => {
return heightScale(item[1])
})
.attr('x', (item, index) => {
return xScale(index)
})
.attr('y', (item) => {
return (height - padding) - heightScale(item[1])
})
Breaking it down
svg.selectAll('rect')
.data(values)
.enter()
.append('rect')
First we selected all rect
or rectangle within the svg whether they exist or not then we bind the rect
with values
with .data(values)
to associate each rect
with each value
, then the .enter()
is called to tell it what to do if no rectangles are found and right after that the .append(rect)
is called which would create new rectangle for each value.
Then we added some attributes like width
, height
, class
, 'x' and 'y' for proper display and positioning of each bar, other attributes like data-date
and data-gdp
are for the fulfillment of freeCodeCamp's test but it's good practice to add them too.
Again try taking away the x
or y
attribute to see how they would be positioned by default.
.attr('class', 'bar')
.attr('width', (width - (2 * padding)) / values.length)
.attr('data-date', (item) => {
return item[0];
})
.attr('data-gdp', (item) => {
return item[1];
})
.attr('height', (item) => {
return heightScale(item[1])
})
.attr('x', (item, index) => {
return xScale(index)
})
.attr('y', (item) => {
return (height - padding) - heightScale(item[1])
}
You should have something similar to this now
VI. The Tooltip:
Now that we have our bar chart displayed the last thing is to add a tooltip to display some information about the particular bar hovered on, now for this I have created a very simple tooltip, add the code below at the top of your drawBars()
before the rect
selection
let tooltip = d3.select('body')
.append('div')
.attr('id', 'tooltip')
.style('visibility', 'hidden')
.style('width', 'auto')
.style('height', 'auto')
What this does is to select the body
add a div
to it and give it an attribute of id
which is set to tooltip
, right after that, we gave a few styles to it like setting the visibility
to hidden by default, setting the width
and height
to auto
.
Now that we have our tooltip
the next thing is displaying the necessary information when hovered upon, for this, d3 has a method for us which is the .on()
, add the code below right after the y
attribute of the svg
, think of it as adding an event listener to the bars.
.on('mouseover', (item, index) => {
tooltip.style('visibility', 'visible')
.html(`Date: ${index[0]} Data: <b>${index[1]}</b>`)
.attr('data-date', index[0])
})
.on('mouseout', (item) => {
tooltip.style('visibility', 'hidden')
})
Using the .on()
method the first argument takes the name of the the event while the second is the function that should run when the event happens, this function takes the item
and the index
of the item
. Inside the function the tooltip visibility
style is set back to visible
when the mouseover happens and is given some html
which include the date and value of the particular bar hovered upon. a data-date
attribute is also given to the tooltip whose value is the date of the bar hovered upon.
.on('mouseover', (item, index) => {
tooltip.style('visibility', 'visible')
.html(`Date: ${index[0]} Data: <b>${index[1]}</b>`)
.attr('data-date', index[0])
})
Now for when the mouse leaves we have this line of code below, which sets the visibility
of the tooltip back to hidden
that way it toggles on and off based on the event happening.
.on('mouseout', (item) => {
tooltip.style('visibility', 'hidden')
})
The Full code
let values = [];
let heightScale;
let xScale;
let xAxisScale;
let yAxisScale;
let width = 800;
let height = 600;
let padding = 40;
let svg = d3.select('#canvas');
// the drawChart function
let drawChart = () => {
svg.attr('width', width)
.attr('height', height)
}
// the generateScales function
let generateScales = () => {
heightScale = d3.scaleLinear()
.domain([0, d3.max(values, (item) => {
return item[1]
})])
.range([0, height - (2 * padding)])
xScale = d3.scaleLinear()
.domain([0, values.length - 1])
.range([padding, width - padding])
let datesArr = values.map(item => {
return new Date(item[0]);
})
console.log(datesArr)
xAxisScale = d3.scaleTime()
.domain([d3.min(datesArr), d3.max(datesArr)])
.range([padding, width - padding])
yAxisScale = d3.scaleLinear()
.domain([0, d3.max(values, (item) => {
return item[1]
})])
.range([height - padding, padding])
}
// the drawBars function
let drawBars = () => {
let tooltip = d3.select('body')
.append('div')
.attr('id', 'tooltip')
.style('visibility', 'hidden')
.style('width', 'auto')
.style('height', 'auto')
svg.selectAll('rect')
.data(values)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('width', (width - (2 * padding)) / values.length)
.attr('data-date', (item) => {
return item[0];
})
.attr('data-gdp', (item) => {
return item[1];
})
.attr('height', (item) => {
return heightScale(item[1])
})
.attr('x', (item, index) => {
return xScale(index)
})
.attr('y', (item) => {
return (height - padding) - heightScale(item[1])
})
.on('mouseover', (item, index) => {
tooltip.style('visibility', 'visible')
.html(`Date: ${index[0]} Data: <b>${index[1]}</b>`)
.attr('data-date', index[0])
})
.on('mouseout', (item) => {
tooltip.style('visibility', 'hidden')
})
}
// the generateAxes function
let generateAxes = () => {
let xAxis = d3.axisBottom(xAxisScale)
svg.append('g')
.call(xAxis)
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height - padding})`);
let yAxis = d3.axisLeft(yAxisScale);
svg.append('g')
.call(yAxis)
.attr('id', 'y-axis')
.attr('transform', `translate(${padding}, 0)`)
}
// fetching the data
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then(res => res.json())
.then(data => {
values = data.data
console.log(values)
drawChart()
generateScales()
drawBars()
generateAxes()
});
Conclusion
Congratulations! You have just created a bar chart. The code above shows how HTML, SVG, CSS, and JavaScript can work together to create a stunning data graph. Understanding how each of the different elements works and their functions is crucial for developing more complex data graphs, if you want to know more about d3 there are many tutorials and online resources that can have you have better understanding of it.
This can be a lot to grasp but don't be too discouraged, if you want me to make a detailed tutorial about d3 all you need to do is comment down below.
Thanks for reading!
Top comments (0)