Basic Chart - Grouped Bar Chart
FREE     Duration: 20:41
Part of Course: Introductory D3 Course
 

Takeaways:

  • You will use the CSV data from the D3js.org website Grouped Bar Chart Example to see how a full D3 Grouped Bar Chart Example data visualization is built
  • Notice that the styling is done in the <style> </style> section of the HTML document
  • Notice the D3 Margin Convention
  • Notice the D3 Ordinal Scale for the X-Axis and the rangeRoundBands
  • Notice the D3 Ordinal Scale for the secondary X-Axis (for each specific grouping of bar charts)
  • Notice the D3 Linear Scale for the Y-Axis
  • Notice the D3 Ordinal Scale for the 7 HTML colors used to encode attributes of the data
  • Notice the D3 SVG Axis component creation and definition for the X-Axis and Y-Axis
  • Notice the D3 .tickFormat to format the ticks on the Y-Axis
  • Notice the D3 Type-Specific d3.csv AJAX call
  • Notice the use of the d3.keys operator
  • Notice the use the of the .filter, .map, and .forEach array functions
  • Notice the .domain being set for the primary X-Axis, secondary X-Axis, and Y-Axis
  • Notice the D3 Data Join
  • Notice the secondary D3 Data Join with a function used to pull out data to bind to the SVG Rectangle DOM Elements
  • Notice the Third D3 Data Join for the Grouped Bar Chart Legend
  • Notice how D3 helps create visual representations of the data, grouping of data, and the legend

Transcript:

Basic Chart - Grouped Bar Chart

Visual Code Walk Through


[ Image: Grouped Bar Chart Example ]

We will use the Data from the D3js.org website Grouped Bar Chart example.


This chart example will demonstrate two main ideas:

The first idea is to nest an array of JavaScript objects inside of a Javascript Object to achieve a nesting of data inside of other data.

The second idea is to use this nesting to do two bindings of data to DOM elements.

The first binding will be the big data objects which represented the states.

The second binding will be the interior array of JavaScript objects with each one creating a rectangle to represent the data.


Text on Screen [ Image: Data for Grouped Bar Chart Example ]

We will save the data from the example into a file called data.csv

This file is the one that will be loaded asynchronously using the D3.csv request functionality.


The document starts with the DocType, Meta Character Set and CSS styling.

BROWSER HIGHLIGHT First section

<!-DOCTYPE ....</style>

This is the standard HTML Doctype, meta character set definition and CSS Styling for the D3 Data Visualization we are making.


Next we go into the JavaScript Sections of the document.


First, we load the D3.js JavaScript Library from the web.

BROWSER Highlight <script src....></script>

This uses the D3 code hosted by the d3js.org website.


Next, we go into the D3 code that will create the Grouped Bar Chart.


The D3 Margin convention sets up the SVG container size as well as defines the Inner Drawing Space.

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

The SVG Container will be 500 pixels tall by 960 pixels wide.


Next we have the first of two ordinal scaling functions for the x-axis data.

var x0 = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

This code is used to create the ordinal scale for the big grouping elements - which are the states.

This code creates an ordinal scaling function where the range goes from 0 to the width of the Inner Drawing Space.

This ordinal scaling function is using the Range Round Bands to set the bands.

Also, the point 1 is the the padding added to offset the bands from the edge of the interval.


Next we have the second of two ordinal scaling functions for the x-axis data.

var x1 = d3.scale.ordinal();

This code is used to create the ordinal scale for the small elements inside of the states.

The small elements are the age groups.

We do not set the domain or range for now.

These will be defined later after we have fully defined the x0 domain.


Next we have a scale linear function for the y-axis data.

var y = d3.scale.linear()
    .range([height, 0]);

This code will create a scaling function where the range goes from the height of the Inner Drawing Space to 0.

The height and the number zero are reversed so that numbers passed into this scaling function will be converted to numbers that act as if the SVG Coordinate Space has been inverted along the Y-Axis.


Next we setup an ordinal scaling function for the colors that are going to be used for the different age ranges.

var color = d3.scale.ordinal()
    .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);

The range is a list of 7 html colors.

Note that the domain is not yet set.

The domain will be set as the actual rectangles for each range are being defined.

As each age range name is passed into the color, it will be added to the domain and mapped to one of the range colors.

We've seen this previously when we did the Scatterplot example.


Next we create the X-Axis function

var xAxis = d3.svg.axis()
    .scale(x0)
    .orient("bottom");

We pass in the x0-scale function we created earlier and then give the axis an orientation of bottom.

We pass in the x0-scaling function because we want the big grouping to be based on the states.


Then we create the Y-Axis the same way

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .tickFormat(d3.format(".2s"));

We pass in the y-scaling function we created earlier and give the axis an orientation of left.

We also give the numbers a formatting using the d3.formatting function.

This specific type of formatting will make sure the numbers show 1 decimal place and then have the letter M append to the end of the number on the right.

This is called SI prefix formatting.

The webpage gives the example of converting a number of 10 million written out with it's 7 zeros to a number written as 1 0 and then the letter M.


The next code creates the SVG Container and the Inner Drawing Space.

var svg = d3.select("body").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 + ")");


The next code is where the D3 does the asynchronous call to the server to get the data and then builds the chart.

BROWSER HIGHLIGHT all d3.csv

In this case, the callback function is an anonymous function.

Note - this example uses d3.csv for comma separated values not d3.tsv for tab separated values.


Let's go through the callback function section by section.


First, we have code that looks at the first data element and gets the keys.

Once it gets the keys, it applies a function to the array of keys that returns an array of all the elements that are not equal to the string "State".

It then assigns this array to the variable ageNames. var ageNames = d3.keys(data[0]).filter(function(key) { return key !== "State"; });

This is constructed in a series of steps.

First data[0] returns the first JavaScript object.

Then the d3.keys() functionality looks at each key,value pair in the object and returns only the keys.

This list of keys is returned as an array.

Next, the JavaScript Array Filter method is used to applied an anonymous function to each element of this keys array.

The anonymous function returns the element as long as it is not equal to the string "State".

The reason this is done is because of the data.


If we scroll down to the data and look at it, you can see that it's a two dimensional array.

all the data

From left to right we have the state and then all the age groups

From top to bottom we have all the states in the first column.

In all the other columns we have the population of that state in that age group.

So when we want the column names that are the age groups, we have to ignore the word "state".


Back in the code, next, we have code that iterates through the array of JavaScript objects

data.forEach(function(d) {
    d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});

This code is going to add a new key,value pair to the JavaScript Object Associative Array.

For each JavaScript object, it adds a new key called ages.

The value that this key gets is an array of objects where each object has two key value pairs.


Inside of the object, the first key value pair is the key "name" and the value of the name of the age group range.

data.forEach(function(d) {
    d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});


Inside of the object, the second key value pair is the key "value" and the value of the numerical amount of population for that age group.

data.forEach(function(d) {
    d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});


Overall, this code goes through each of the JavaScript Objects and for each object generates a new key,value pair where the key is the string "ages" and the value is an array of JavaScript objects each which has an age range and a numerical population value for that age range.

data.forEach(function(d) {
    d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});

The reason this is constructed in this way is because we are doing a grouped bar chart.

First, the data must be grouped by states.

Then inside of each of those state data objects, the data must then be grouped by age groups.

We will explore this further in the JavaScript console.


Next, we set the domain for the x0 ordinal scale function

x0.domain(data.map(function(d) { return d.State; }));

This sets the domain of x0 as the list of all of the states found in the data set.


Next, we set the domain and range for the x1 ordinal scale function

x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);

This sets the domain of x1 as the list of all of the age group names found in the data set for each state.

This is why the domain wasn't set earlier - because we didn't have the data to tell us what all of the age group names were going to be.

Then the range for the x1 ordinal scale function is set as going from 0 to the x0 rangeBand.

This x1 range is how the data stays grouped together in the chart.


Next, we set the domain for the y-scale function

y.domain([0, d3.max(data, function(d) { return d3.max(d.ages, function(d) { return d.value; }); })]);

The domain of the Y scale function is a continuous linear function, so the domain will be defined as going from 0 to the max age group population for any one of the states in the data.


To set the domain, we have to use two nested anonymous functions.

y.domain([0, d3.max(data, function(d) { return d3.max(d.ages, function(d) { return d.value; }); }) ]);

The outer most anonymous function goes through all of the elements in the data and passes each JavaScript object to the inner most anonymous function.

The inner most anonymous function goes through the array of values found in the key ages and figures out which is the max value.

It then returns this value.

So for each JavaScript Object, which is a different state, is figures out which is the largest population amount for the age group of that state.

Then once is has the largest number for each state, it then figures out what the largest number over all is for all the states provided.

This is why it's done in two nested anonymous functions.


Next, we call the D3 dot axis operator for the x-axis.

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

This creates the full X Axis.


Then, we create the y-axis and a y-axis text label.

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
  .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Population");

This creates the full Y Axis.


This code uses the D3 pattern to bind the JavaScript Objects to SVG Group Elements.

var state = svg.selectAll(".state")
    .data(data)
  .enter().append("g")
    .attr("class", "g")
    .attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });

Each JavaScript Object represents one state

So 6 SVG Group Elements will created and each one will have bound to it the data for each state.

Note that each SVG Group Element is transform translated based on the x0 ordinal function for each specific state.

This ensures that when the rectangles for each age group are constructed, that they are constructed in the right place on the graph.

This collection of SVG Group Elements is then assigned to a variable named state.


This code then uses the D3 pattern to bind the age group data to SVG rectangles for each of the age group segment found in each state.

state.selectAll("rect")
    .data(function(d) { return d.ages; })
  .enter().append("rect")
     .attr("width", x1.rangeBand())
     .attr("x", function(d) { return x1(d.name); })
     .attr("y", function(d) { return y(d.value); })
     .attr("height", function(d) { return height - y(d.value); })
     .style("fill", function(d) { return color(d.name); });

First, we selectAll the rectangles.

Then use the array of JavaScript Objects with the age group name and age group value found in each state's JavaScript Object.

So for each SVG Group Element, this code will find the array of JavaScript Objects found at the Associative Array key "age" and construct a rectangle for each of those JavaScript Objects.

Once we have all of this data, the enter selection is selected.

Then the rectangles are constructed.

The width is based on the x1 ordinal scale function range band

The x coordinate is based on the age group name passed through the x1 ordinal scale function

The y coordinate is based on the age group value passed through the y linear scale function

The height is based on the height of the Inner Container minus the age group value passed through the y linear scale function.

This is because rectangles are built using the point at the top left.

Finally, the fill of the rectangle is chosen based on the color ordinal scale we defined earlier.

Remember that this color code does 2 things.

One - it returns a color for a given value you pass into it.

Two - for every value you pass into it, it adds it to the domain.

Which means that at the end of the code the domain of the color ordinal scale function will contain all of the age group names.


Next the legend for the chart is constructed.


First, SVG Group elements are added based on the D3 pattern.

var legend = svg.selectAll(".legend")
    .data(ageNames.slice().reverse())
  .enter().append("g")
    .attr("class", "legend")
    .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

SVG Group Elements are added based on the data of the age group names.

The age group names array is sliced and reversed so that the array has the oldest name of an age group "65 Years and Over" come first.

The SVG Group Elements are transform translated according to the index element so that they line up from top to bottom on top of each other.

This ensures that the sliced up and reversed array of names has the oldest age group on top going to the youngest age group on the bottom.


Then rectangles are added to the Legend SVG Group Elements based on the width of the Inner Drawing Space.

legend.append("rect")
    .attr("x", width - 18)
    .attr("width", 18)
    .attr("height", 18)
    .style("fill", color);

The color of the rectangle is dictated by the data that was bound to each legend SVG Group Element.

Because the domain has already been set for the color ordinal scaling function, it will match up exactly to the bars that were constructed for the grouped bar chart.


Lastly, the age group name is added to each Legend SVG Group Element.

legend.append("text")
    .attr("x", width - 24)
    .attr("y", 9)
    .attr("dy", ".35em")
    .style("text-anchor", "end")
    .text(function(d) { return d; });

The text of each legend is set according to the data bound to each of the Legend SVG Group Elements.


And that is the end of the callback function and the end of the d3.csv function.

});

When this is done, the graph will have been fully generated.


Let's now build this part by part in JavaScript.



JavaScript Code Build


Because in the example the building of the chart happens inside of the callback function, we will use a more simple anonymous function in the JavaScript Console.

d3.csv("data.csv", function(error, data){...});

// =>

var callbackError, callbackData;

d3.csv("data.csv", function(error, data){
    callbackError = error;
    callbackData = data;
});


Alright, to the JavaScript Console.


CLEAR CHROME BROWSER CACHE


From now on, we'll assume that you understand and are able to save down the data in the correct format with the correct file extension, start the Python SimpleHTTP Server, as well as check to make sure D3 is loaded.


We open the Chrome Developer Tools and go step by step building the visualization.


We start by defining the callbackError and callbackData variables which will be used to house the data we get back from the d3.csv function.

var callbackError, callbackData;


The first step from the example is defining the margins and the width and height of the Inner Drawing Space.

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;


Next - define the x0 ordinal scale function and it's range round bands.

var x0 = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);


Next - define the x1 ordinal scale function

var x1 = d3.scale.ordinal();


Next - define the y linear scale function and it's range.

Remember to pay attention to the fact that the range has height first and then 0.

var y = d3.scale.linear()
    .range([height, 0]);


Next - define the color ordinal scale function and it's range which will be used to color the different age groups.

var color = d3.scale.ordinal()
    .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);


Next - define the xAxis function and provide it with a scale and orientation.

var xAxis = d3.svg.axis()
    .scale(x0)
    .orient("bottom");


Next - define the yAxis function and provide it with a scale, an orientation and tick formatting.

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .tickFormat(d3.format(".2s"));


Let's test out the D3 formatting function.

d3.format(".2s")(10000000);

You can see that it takes in the 10 million number as a 1 and 7 zeros and converts it to the number 10 with the letter M as the right most element.


Next - define the SVG Container and the Inner Drawing Space

var svg = d3.select("body").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 + ")");

BROWSER - Open the Body element, the SVG element and show the inner SVG Group Element.


Next is where we are going to differ a bit from the code of the example.

d3.csv("data.csv", function(error,data) {
    callbackError = error;
    callbackData  = data;
    console.log([callbackError, callbackData.length]);
});

Instead of defining an anonymous callback function that does all the generating of the chart in one go, we'll define a callback function that assigns the data and error to variables.

We'll then use the callbackError and callbackData variables to build the Grouped Bar Chart.

Inside of the callback function, we have a console log of an array of the callbackError and callbackData dot length so we can see what is in each one.

We can see that the callbackError is null and that the length of the callbackData array is 6 elements.


Once we have the data, we have to extract the age group names from the callbackData array.

var ageNames = d3.keys(callbackData[0]).filter(function(key) { return key !== "State"; });

ageNames;

You can see all the age group names.


Let's build this step by step to make sure we understand how it works.


First we look at the first JavaScript Object

callbackData[0];

The first JavaScript Object is for the state of California.


Next, let's use the d3.keys functionality to get the keys from all of the key, value pairs.

d3.keys(callbackData[0]);

This returns an array of all the keys for the first JavaScript Object.


Next, let's use the JavaScript Array Filter Method with an anonymous function to generate a new array of elements where every element does not equal the string "State".

d3.keys(callbackData[0]).filter(function(key) { return key !== "State"; });

There we go, these are all the age group names.


Before we do the Array forEach iterator, let's take another look at the first element.

callbackData[0];

BROWSER - expand the object in the console.

When we expand the object you can see that it has all the age groups and their respective values as well as the state.


Next - use the Array forEach iterator to go through the 6 element array and create a new key,value pair inside each JavaScript object where the key is the string "ages" and the value is an array of objects where each of these new objects has the age group name and age group population size.

callbackData.forEach(function(d) {
    d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});


Now we look at the first element again.

callbackData[0];

BROWSER - expand the object in the console.

This time you can see that a new key,value pair has been added.

The key name is ages.

The value is an array of 7 objects.

BROWSER - expand the array

When we expand the array we can see that it is an array of 7 objects.

BROWSER - expand the first two object

When we expand the first two objects inside of the array inside of the outer object, we see that each one has the keys of "name" and "value" and the values correspond to the data found in the outer object.

This nested data structure will be useful for building the Grouped Bar Chart.


Next - define the domain of the x0 ordinal scale function and then check to see what the domain is.

x0.domain(callbackData.map(function(d) { return d.State; }));

x0.domain();

The domain of the x0 ordinal scale function is the two letter abbreviation of each American state present in the data.


Next - define the domain and range of the x1 ordinal scale function and then check to see what the domain is.

x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);

x1.domain();

The domain of the x1 ordinal scale function is the array of the strings of the 7 different types of age groups.


Next - define the domain for the y linear scale function and then check to see what the domain is.

y.domain([0, d3.max(callbackData, function(d) { return d3.max(d.ages, function(d) { return d.value; }); })]);

y.domain();

The Y linear scale domain goes from 0 to 10,604,510.

Remember that the anonymous function inside of the anonymous function looks inside of each state object and then look through the array of objects that has the age group names and values.


Next - the x-axis is created.

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

BROWSER - click into the G element

If we click into the SVG Group Element for the Inner Drawing Space

You can see the SVG Group element with the class "x axis".

This is the X axis.


Next - the y-axis is created with the text label.

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
  .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Population");


Next - create the SVG Group Elements to which the JavaScript Objects which represent each state will be bound to.

var state = svg.selectAll(".state")
    .data(callbackData)
  .enter().append("g")
    .attr("class", "g")
    .attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });

Note that each SVG Group Element is transform translated to the corresponding place on the chart based on the x0 ordinal scale function.


Let's look at the data that was bound to the first two elements in the state variable.

state.data()[0];

state.data()[1];

These two elements correspond to the California and Texas Data.

So the first two SVG Group elements will be California and Texas.


Next - for each of the SVG Group Elements just created, let's create the rectangles based on the age group breakdown and population for each age group.

state.selectAll("rect")
    .data(function(d) { return d.ages; })
  .enter().append("rect")
    .attr("width", x1.rangeBand())
    .attr("x", function(d) { return x1(d.name); })
    .attr("y", function(d) { return y(d.value); })
    .attr("height", function(d) { return height - y(d.value); })
    .style("fill", function(d) { return color(d.name); });

The rectangles now appear.

Remember that rectangles are constructed from the top left most point.


Next - the legend for the chart is created.


First - the SVG Group Elements for the legend are created based on the data of the age group names.

var legend = svg.selectAll(".legend")
    .data(ageNames.slice().reverse())
  .enter().append("g")
    .attr("class", "legend")
    .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

If we scroll to the bottom of the Chrome Developer Tools Elements section, we can see the 7 SVG Group Elements with class of legend.


Next - create the rectangles that are colored based on the age group.

legend.append("rect")
    .attr("x", width - 18)
    .attr("width", 18)
    .attr("height", 18)
    .style("fill", color);

BROWSER - move chrome developer tools to the right and then back.

If we move the Chrome Developer Tools to the right, we can see the 7 rectangles that have been crated.

BROWSER - scroll down the elements until we can see the legends, then click into one of the SVG Group elements.

If we click into the SVG Group Elements with class of legend, we can see the rectangles.


Finally - create the text that goes along with each legend and it's colored rectangle.

legend.append("text")
    .attr("x", width - 24)
    .attr("y", 9)
    .attr("dy", ".35em")
    .style("text-anchor", "end")
    .text(function(d) { return d; });

This created the text for the legend.


And there we go, we have the chart.


Let's close the Chrome Developer tools to get a better look.

BROWSER - close the Chrome Developer Tools.

BROWSER - zoom out.

You can see the full picture.


And with that we built the Basic Chart - Grouped Bar Chart.


The chart was created using two main ideas.

The first idea was to nest an array of JavaScript objects inside of a javascript object to achieve a nesting of data inside of other data.

The second idea was to use this nesting to do two bindings of data to DOM elements.

The first binding was the big data objects which represented the states.

The second binding was the interior array of JavaScript objects with each one creating a rectangle to represent the data.

<< Back To D3 Screencast and Written Tutorials Index