Blog

Jan 15, 2017  mode_comment

Drawing a bar chart with d3.js for JS beginners (Part III)

Back

D3.js is a powerful data visualizing JavaScript (JS) library developed by Mike Bostock.
In the previous posts, I went over some JS/D3 basics (part 1) and started drawing a bar chart with SVG elements (part 2). This post will cover how to tidy up our bar chart by creating axes and labels, where the finished bar chart will look like the chart above.



1. Canvas and margins


When we made our bar chart in the previous post, we didn't really think too carefully about the design and layout of our svg canvas. Now that we want to add axes and labels, we need to make some room. Thus we will have to re-design our canvas and the structure of our code.

Let's start by defining some variables:

var dataset;
var engineers, engMajors;
var svg_w = 960;
var svg_h = 700;

d3.csv("../files/recent-grads.csv", function(error,data){
    // insert code here
});

Here I declared variables to store our data that we want to plot, and declared and defined our svg canvas size to be 960x700. Now, let's use the concept of the html box model to set up margins so that we can add axes and labels.

var margin = {top: 30, right: 30, bottom: 300, left: 40};

This will create an object called margin with 4 properties: top, right, bottom, left. Now let's define our chart size using our svg size and margins.

var width = svg_w - margin.left - margin.right;
var height = svg_h - margin.top - margin.bottom;

Here, we define our chart's width and height so that it is the svg canvas size with the margins subtracted off (The . notation is used to access object properties).

Now we are ready to read our .csv file and manipulate our data.

d3.csv("../files/recent-grads.csv", function(error, data){
    if (error){ console.log(error); }

    dataset = data;
    engineers = dataset.filter(function(d){ return d.Major_category == "Engineering"; }); // select only the Engineering major rows
    engMajors = engineers.map(function(d){ return d.Major; }); // return the name of the Engineering major

    for (i=0; i<engMajors.length; i++){
        engMajors[i] = engMajors[i].replace("ENGINEERING", "ENG"); // shorten the string by replacing 'Engineering' to 'Eng'
    }
});

Let's go over what I did in the above code. First, I read the .csv file using the d3.csv() method and stored the data into our variable dataset. Then, I filtered the data using the d3.filter() method to select only the rows where the Major_category column was "Engineering" and stored these rows into the variable engineers. Since we want to label our bar chart according to their majors, I then used the JS .map() method on our variable engineers to retrieve information for the "Major" column.

The map method transforms an array by applying a function to all of its elements and building a new array from the returned values. The new array will have the same length as the input array, but its content will have been “mapped” to a new form by the function. -Eloquent Javascript

The next few lines of code were written to shorten our major names. I realized the labels were slightly long so I used the JS for loop to iterate through the variable engMajor (using engMajor.length) and used the JS .replace() method to replace the string "ENGINEERING" to "ENG". JS arrays come with a built-in property called .length() that returns the size of the array. Code 1 shows what we have so far:

Code 1. Preparing to draw our bar chart by setting up canvas and organizing data

var dataset;
var engineers, engMajors;
var svg_w = 960;
var svg_h = 700;
var margin = {top: 30, right: 30, bottom: 300, left: 40};

var width = svg_w - margin.left - margin.right;
var height = svg_h - margin.top - margin.bottom;

d3.csv("../files/recent-grads.csv", function(error,data){
    if (error){ console.log(error); }

    dataset = data;
    engineers = dataset.filter(function(d){ return d.Major_category == "Engineering"; });
    engMajors = engineers.map(function(d){ return d.Major; });

    for (i=0; i<engMajors.length; i++){
        engMajors[i] = engMajors[i].replace("ENGINEERING", "ENG");
    }
});


Now let's draw our svg canvas.

var svg = d3.select("body")
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g")
    .attr("transform", "translate("+ margin.left + "," + margin.top + ")");

We introduced some new syntax while making our svg canvas. First, we appended the g element, which stands for group. The g svg element is a group element that has no visual effects. It's purely used to group elements that are appended afterwards so transforming them is easy.
That said, we immediately gave our g element the transform attribute. The transform attribute can do things like translate or rotate elements that are appended afterwards. Since we want to make room for our axes, we gave the transform attribute a value of translate(x,y) where the x and y are (margin.left, margin.top) so our top-left corner of the bar chart starts at this x, y coordinate. The + is used to concatenate strings (JS will automatically convert our margin numerical values to a string in this case).

Now let's draw our bars:

var barWidth = width / (engineers.length + 1);
svg.selectAll("div")
    .data(engineers)
    .enter()
    .append("rect")
    .attr("x", function(d,i { return i*(barWidth+1); }))
    .attr("y", function(d){ return height-d.ShareWomen*height; })
    .attr("width", barWidth)
    .attr("height", function(d, i){ return d.ShareWomen * height; })
    .attr("fill", "teal");

First, we create the variable barWidth and define it so that it is equal to width/(engineers.length+1). 1 is added because we are defining the x-coordinate to be the bottom left corner of our bars. The rest of the code is similar to what we wrote in part 2.


Output of code 2: our svg canvas has the bar chart with margins

Code 2
var dataset;
var engineers, engMajors;
var svg_w = 960;
var svg_h = 700;
var margin = {top: 30, right: 30, bottom: 300, left: 40};

var width = svg_w - margin.left - margin.right;
var height = svg_h - margin.top - margin.bottom;

d3.csv("../files/recent-grads.csv", function(error,data){
    if (error){ console.log(error); }

    dataset = data;
    engineers = dataset.filter(function(d){ return d.Major_category == "Engineering"; });
    engMajors = engineers.map(function(d){ return d.Major; });

    for (i=0; i<engMajors.length; i++){
        engMajors[i] = engMajors[i].replace("ENGINEERING", "ENG");
    }

    // draw svg canvas
    var svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate("+ margin.left + "," + margin.top + ")");

    // draw bar chart
    var barWidth = width / (engineers.length + 1);
    svg.selectAll("div")
        .data(engineers)
        .enter()
        .append("rect")
        .attr("x", function(d,i { return i*(barWidth+1); }))
        .attr("y", function(d){ return height-d.ShareWomen*height; })
        .attr("width", barWidth)
        .attr("height", function(d, i){ return d.ShareWomen * height; })
        .attr("fill", "teal");
});




2. Scales, axes, and labels


Now it's time to add axes! To do so, we can utilize d3.scale() and d3.svg.axis() methods.

Scales are functions that map from an input domain to an output range. -Mike Bostock, creator of d3.js

Before we can set up our x/y axis, we want to define their scales first.

var xScale = d3.scale.linear()
    .range([0, width])
    .domain([0, engineers.length]);

var yScale = d3.scale.linear()
    .range([height, 0])
    .domain([0, d3.max(dataset, function(d){ return d.ShareWomen; })]);

Here, we create two variables, xScale and yScale. Each scale is defined to be d3.scale.linear(), which is a d3 method of creating a linearly spaced scale. The command is then followed by .range() and .domain(). The method .range() is used to set the range of the scale and .domain() is used to map our y-axis data values onto the scale. Note that for our yScale, we give it a .range() of [height, 0] because of how the JavaScript graphical coordinates are set up (0,0 is the top left corner and we want our y-axis to start from y=height and end at y=0).

Now let's define our axes using the d3.svg.axis() method.

var xAxis = d3.svg.axis()
    .scale(xScale)
    .orient("bottom")
    .ticks(engineers.length)
    .tickFormat(function(d,i){ return engMajors[i]; });

var yAxis = d3.svg.axis()
    .scale(yScale)
    .orient("left")
    .ticks(10, "%")

We can chain several commands with the d3.svg.axis() method.
First, we set our scales by passing the variable xScale that we defined earlier into the .scale() method. Then, we determine where the axis will sit with the .orient() method, where it takes the arguments top, right, bottom, left.
We can also set the number of ticks we want to display with the .tick() method. .tick() can take two arguments: the first argument is the number of ticks we want and the 2nd argument is the display format of the ticks. Only the y-axis has a 2nd argument passed into .ticks() so only the y-labels will take on the format of the 2nd argument (i.e. %).
Finally, for the x-axis, we chain an additional method .tickFormat(). This method is used to format the tick labels as a string. We pass in the anonymous JS function function(d,i){return engMajors[i];} to return each major in engMajors as the label for each bar (if we simply wrote .tickFormat(engMajors), the label will be a concatenated string of all the majors in the array engMajors).

We are done setting up our axis properties and all we have to do now is to draw the axes onto our chart. To draw the axes, we set up our standard d3 code for appending elements:

svg.append("g")
    .attr("class", "axis")
    .call(yAxis)
    .append("text")
        .attr("y", 6)
        .attr("dx", ".7em")
        .style("text-anchor","front")
        .style("font-size", 20)
        .text("Percentage of Women in Engineering Majors");

svg.append("g")
    .attr("class", "axis")
    .call(xAxis)
    .append("text")
        .attr("dx", "-0.8em")
        .attr("dy", ".15em")
        .style("text-anchor","end")
        .style("font-size", 13)
        .attr("transform","rotate(-80)");

To draw an axis on our chart, we need to call the .call(axis-name) method. I also gave our axes a class name of "axis" so that we can target our axis with CSS later. After we call our axes, we need to append our labels. This is done by appending the text element to our axes.
The lines following .append("text") are code to specify where the labels will be at and how they will be anchored to the axes. .attr(x/y) specify the x and y coordinates and .attr(dx/dy) specify how far away the labels will sit from the axes. Then .style("text-anchor", location) determines how the labels will be anchored. Notice how front and end behave. We can see that the front of the text "Percentage of Women .." is anchored to our y-axis while the end of our "Majors" are anchored to the x-axis. Finally, we use the transform attribute, introduced earlier, to rotate our x-axis labels so that they are rotated by 80 degrees counter-clockwise.


Output of code 4: our bar chart with axes

Code 4
var dataset;
var engineers, engMajors;
var svg_w = 960;
var svg_h = 700;
var margin = {top: 30, right: 30, bottom: 300, left: 40};

var width = svg_w - margin.left - margin.right;
var height = svg_h - margin.top - margin.bottom;

d3.csv("../files/recent-grads.csv", function(error,data){
    if (error){ console.log(error); }

    dataset = data;
    engineers = dataset.filter(function(d){ return d.Major_category == "Engineering"; });
    engMajors = engineers.map(function(d){ return d.Major; });

    for (i=0; i<engMajors.length; i++){
        engMajors[i] = engMajors[i].replace("ENGINEERING", "ENG");
    }

    // draw svg canvas
    var svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate("+ margin.left + "," + margin.top + ")");

    // draw bar chart
    var barWidth = width / (engineers.length + 1);
    svg.selectAll("div")
        .data(engineers)
        .enter()
        .append("rect")
        .attr("x", function(d,i { return i*(barWidth+1); }))
        .attr("y", function(d){ return height-d.ShareWomen*height; })
        .attr("width", barWidth)
        .attr("height", function(d, i){ return d.ShareWomen * height; })
        .attr("fill", "teal");

    // set x-axis scale
    var xScale = d3.scale.linear()
        .range([0, width])
        .domain([0, engineers.length]);

    // set y-axis scale
    var yScale = d3.scale.linear()
        .range([height, 0])
        .domain([0, d3.max(dataset, function(d){ return d.ShareWomen; })]);

    // define x-axis using xScale with appropriate tick #/labels
    var xAxis = d3.svg.axis()
        .scale(xScale)
        .orient("bottom")
        .ticks(engineers.length)
        .tickFormat(function(d,i){ return engMajors[i]; });

    // define x-yxis using yScale with appropriate tick #/labels
    var yAxis = d3.svg.axis()
        .scale(yScale)
        .orient("left")
        .ticks(10, "%")

    // draw the y-axis on our svg chart
    svg.append("g")
        .attr("class", "axis")
        .call(yAxis)
        .append("text")
            .attr("y", 6)
            .attr("dx", ".7em")
            .style("text-anchor","front")
            .style("font-size", 20)
            .text("Percentage of Women in Engineering Majors");

    // draw the x-axis on our svg chart
    svg.append("g")
        .attr("class", "axis")
        .call(xAxis)
        .append("text")
            .attr("dx", "-0.8em")
            .attr("dy", ".15em")
            .style("text-anchor","end")
            .style("font-size", 13)
            .attr("transform","rotate(-80)");
}); //end of d3.csv()


// CSS styling for axes
//  can be contained in a separate .css file 
//  or <head><style>"css code"</style></head>
.axis path, .axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}
.axis text{
    font-family: sans-serif;
    font-size:11px;
}



That's it! Here is a summary of what we did:

  1. Set up the svg canvas considering the chart size and margins
  2. Manipulate data to get it in the form that we want to work with
  3. Draw the svg canvas
  4. Draw the bar chart
    • translate the bar chart by margin values
  5. Set up the axes scales
    • set range (scale of the axis itself)
    • set domain (data values that will be mapped onto the axis)
  6. Set up the axes
    • pass in the scales defined in step 5
    • set orientation of axes
    • set numbers of ticks
    • set tick format
  7. Draw the axes on the canvas
    • call the axis variables
    • append text labels
      • set location of label
      • set how label is anchored
      • style the label
      • write the label (if necessary)
  8. Style the axes using css



3. It's the end



Awesome work! We now have a neat looking bar chart! Thanks for following this far. I hope this series of posts was helpful. I'm also pretty new to JS/D3 so please don't hesitate to correct me if I made any errors.

D3.js is a sophisticated library where you could accomplish a specific task in many different ways. For this reason, I want to encourage everyone to look at these awesome resources and good luck on your d3/JS journey!

Resources :

- Eloquent Javascript (e-Book)
- Interactive Data Visualization for the Web (e-Book)
- Mike Bostock (d3 creator's website)
- D3 API Reference (web)


About

I am a computational scientist finishing my PhD at U of Penn. I picked up programming coming into graduate school and after years of computational research, I'm amazed by what data can do. I love to use data analytics to find trends, which when exposed, empower people to make informed decisions about the world they live in. I'm also the co-founder of Penn Data Science Group.