DEV Community

Cover image for How to visualize data with a bar chart using d3
Ifeoluwa isaiah
Ifeoluwa isaiah

Posted on • Updated on

How to visualize data with a bar chart using d3

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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; 
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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)
   }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
 });

Enter fullscreen mode Exit fullscreen mode


javascript
The code above simply gets the data and stores them in the values array, if you check your console you should see this

data from an api

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()
Enter fullscreen mode Exit fullscreen mode

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()
 });

Enter fullscreen mode Exit fullscreen mode

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]) 
}
Enter fullscreen mode Exit fullscreen mode


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];

Enter fullscreen mode Exit fullscreen mode

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, ])
Enter fullscreen mode Exit fullscreen mode

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]
})
Enter fullscreen mode Exit fullscreen mode

That's how we have this

.domain([0, d3.max(values, (item) => {
            return item[1]
})])
Enter fullscreen mode Exit fullscreen mode

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];

Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)])

Enter fullscreen mode Exit fullscreen mode

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]) 
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Now in order to have this bottom axis paired with dates below

x axis of a bar chart

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

data from an api

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]);
  })  
Enter fullscreen mode Exit fullscreen mode

If you console.log(datesArr) you'll find our array containing the newly converted dates

an array of 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)`)
    }

Enter fullscreen mode Exit fullscreen mode

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})`);
Enter fullscreen mode Exit fullscreen mode

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

a bar chart containing the x axis only

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)`)
Enter fullscreen mode Exit fullscreen mode

So in total we have something like this

a bar chart with both x and y axis

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])
            })

Enter fullscreen mode Exit fullscreen mode

Breaking it down

  svg.selectAll('rect')
            .data(values)
            .enter()
            .append('rect')
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

You should have something similar to this now

A bar chart displaying data

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')

Enter fullscreen mode Exit fullscreen mode

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')
   })

Enter fullscreen mode Exit fullscreen mode

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])

  })
Enter fullscreen mode Exit fullscreen mode

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')
   })
Enter fullscreen mode Exit fullscreen mode

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()
 });
Enter fullscreen mode Exit fullscreen mode

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)