On Github brandomr / better_barchart
This walk through covers how to go from zero to interactive bar chart as quickly as possible. Along the way we'll learn a bit about HTML5, CSS, and javascript.
HyperText Markup Language (HTML) is the standard markup language used to create webpages. An HTML document is a plain text document with an .html extension that consists of elements enclosed in <>. Here is what a very standard site might look like:
<div> <p>Hello world!</p> </div>Hello world!
One of the most basic building blocks of HTML pages is the div. A div is essentially a rectangular block. Here's an example of one:
<div style="display: inline-block; width: 50px; height: 200px; background-color: #3690c0;"></div>
With just a set of div elements, we can actually make a bar chart appear on a page.
<div class="example_bar" style="height: 20px"></div> <div class="example_bar" style="height: 50px"></div> <div class="example_bar" style="height: 75px"></div> <div class="example_bar" style="height: 100px"></div> <div class="example_bar" style="height: 150px"></div>
Cascading Style Sheets (CSS) enable you to apply styles to elements of the same type or of the same class or ID. They are intended to enable the separation of document content from document presentation. Here's how I applied styling to all the bars in the bar chart at once.
<style> .example_bar { display: inline-block; width: 50px; background-color: #3690c0; } </style>
With the advent of HTML5 came the Scalable Vector Graphic (SVG). The SVG is a canvas for creating graphics like lines/paths, rectangles, and circles. D3.js relies on the SVG to create graphics that can scale to the size of our data. Here's an example.
<svg width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="grey" stroke-width="4" fill="steelblue"></circle> </svg>
HTML documents are stored in local memory on a client's computer and are interpreted by the web browser. The interpretation of the HTML is rendered as the
Document Object Model (DOM)
The DOM need not be static! In fact, the beauty of D3.js is that it allows us to easily use data to manipulate the DOM.
Data-Driven Documents (D3) is a javascript library designed for the creation of web-based charting. A web page is just an HTML document and D3 let's us drive the HTML using data.
D3.js was created in 2011 by Mike Bostock, Vadim Ogievetsky, and Jeffrey Heer while they were part of Stanford's Vis Group. D3.js is open source and has become the standard for interactive visualizations for the web.
D3 is used by the New York Times (who now employs Mike Bostock) and many others. It has been leveraged for building many of the other commonly used web charting libraries.
I never do. And nor should you. Leverage the many examples available at D3js.org and elsewhere.
Javascript is considered the language of the web. It has been around for a while (it was first developed at Netscape in the 90's) and is now used on essentially any web page you might visit. For example, this presentation was made with Reveal.js. To load some javascript include a reference to the script file in your HTML document:
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
Keep in mind that like a HTML, a javascript document is simply a plain text document written in javascript with a .js extension.
here is the data the we will use to build the chart
country favorability description Bahrain 27 Our favorability with Bahrain is at an all time low Bangladesh 35 We are doing better and better with Bangladesh Brunei 48 Could be better with Brunei all things considered Bermuda 56 Bermuda is such a nice place. A really nice place Barbados 69 Great island and they less than three us Bolivia 74 Surprising right? Who would have thought it? Brandon 87 He is our biggest fan thats for sureA .csv file is just a comma separated text file. Recognize a theme here? We will be using a lot of plain text files.
You have probably opened a .csv or two using Excel or another spreadsheet software.
Bet you didn't notice the difference.
First, you'll need to create a blank web page and load D3.
<!DOCTYPE html> <meta charset="utf-8"> <body> <div>Behold, a bar chart!</div> <div class="barchart"></div> <script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> <script type="text/javascript" src="barchart.js"></script> </body> </html>
You can copy and paste that into a blank file using a plain text editor. I like Text Wrangler since it's free but you could just as easily use something as basic as Notepad or something with more functionality like Sublime Text.
Make sure you save your file with a .html extension.
<!DOCTYPE html> <meta charset="utf-8"> <body> <div>Behold, a bar chart!</div> <div class="barchart"></div> <script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> <script type="text/javascript" src="barchart.js"></script> </body> </html>
Open up another blank plain text file and save it in the same directory as your .html file. Call it barchart.js
We'll use this file to write out our D3 script.
First, let's define the margins of our chart.
var margin = {top: 50, right: 20, bottom: 30, left: 60} var width = 660 - margin.left - margin.right var height = 400 - margin.top - margin.bottom;
Next, let's scale our x and y axes to fit the canvas:
var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var y = d3.scale.linear() .range([height, 0]);
Let's append a SVG element to our <div class="barchart"> container:
var svg = d3.select(".barchart").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 + ")");Remember, the SVG is a container for our chart. Specifically, we append a SVG and then append to that a G element which lets us group the building blocks together.
First, define a function called type that ensures favorability, a continuous variable, is read in as a number instead of a string.
function type(d) { d.favorability = +d.favorability; return d; }
Now we can read in our data, passing the name of the data file, the function type, and asking the browser to log the data to the console.
d3.csv("data.csv", type, function(error, data) { console.log(data);
Your javascript file should now look like:
var margin = {top: 50, right: 20, bottom: 30, left: 60} var width = 660 - margin.left - margin.right var height = 400 - margin.top - margin.bottom; var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var y = d3.scale.linear() .range([height, 0]); var svg = d3.select(".barchart").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 + ")"); function type(d) { d.favorability = +d.favorability; return d; } d3.csv("data.csv", type, function(error, data) { console.log(data);
Now that we have read in our data, we map our data to the x and y domain. We use the variables x and y which we defined earlier and we call them using the x and y ranges within our data set.
x.domain(data.map(function(d) { return d.country; })); y.domain([0, 100]);
This ensures that when we use our x or y variable on an x or y data point, it scales to the appropriate number for our chart.
I set the y domain (favorability) as between 0 and 100 since we know this is a percent out of 100. The x domain is mapped to the number of unique data points within the country variable.
Add the following code block to barchart.js
svg.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.country); }) .attr("width", x.rangeBand()) .attr("y", function(d) { return y(d.favorability); }) .attr("height", function(d) { return height - y(d.favorability); })We just selected the SVG variable and bound the data to the SVG while appending a new rect for each row in data.csv.
We set the x and y position using the scales we defined earlier and set the width and the height using our x scale and the the favorability variable in the dataset.
With a bit of styling back in our HTML document, we can style the color of the bar chart.
body { background-color: black; font: 10px sans-serif; margin-left: 10%; } .bar { fill: #3690c0; } .bar:hover { fill: #fc4e2a; }
We just set the color of the bar by styling anything where class="bar". We also added a second color for when someone hovers over the bar.
Now, we need to define the axes
var xAxis = d3.svg.axis() .scale(x) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left");
We can call d3.svg.axis() to succinctly create axes. We then scale them using our x and y variables.
Now we can add our x and y axes to the SVG canvas:
svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis);<p></p>
svg.append("g") .attr("class", "y axis") .call(yAxis)
Append a new g element to the variable svg and call the xAxis or yAxis variable to appropriately scale and locate the axes.Let's add some styling to the axes back in the HTML document.
.axis { font: 12px sans-serif; fill: #fff; } .axis path, .axis line { fill: none; stroke: #969696; shape-rendering: crispEdges; } .x.axis path { display: none; }
Note that we changed the .x.axis path to display: none which turns off the x axis line.
Let's add to our D3 code to improve the tick marks and labels.
var xAxis = d3.svg.axis() .scale(x) .orient("bottom") .tickSize(0) .tickPadding(5); var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(4) .tickSize(3);
We remove the ticks completely from the x axis and add padding between the bars and the country labels. Also, we limit the number of labels (between the min/max) on the y axis to 4 and we shrink the ticks.
Our javascript file barchart.js should look like
var margin = {top: 50, right: 20, bottom: 30, left: 60}; var width = 660 - margin.left - margin.right; var height = 400 - margin.top - margin.bottom; var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient("bottom") .tickSize(0) .tickPadding(5); var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(4) .tickSize(3); var svg = d3.select(".barchart").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 + ")"); function type(d) { d.favorability = +d.favorability; return d; } d3.csv("data.csv", type, function(error, data) { console.log(data); x.domain(data.map(function(d) { return d.country; })); y.domain([0, 100]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis); svg.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.country); }) .attr("width", x.rangeBand()) .attr("y", function(d) { return y(d.favorability); }) .attr("height", function(d) { return height - y(d.favorability); }) });
<!DOCTYPE html> <meta charset="utf-8"> <style> body { background-color: black; font: 10px sans-serif; margin-left: 10%; } .bar { fill: #3690c0; } .bar:hover { fill: #fc4e2a; } .axis { font: 12px sans-serif; fill: #fff; } .axis path, .axis line { fill: none; stroke: #969696; shape-rendering: crispEdges; } .x.axis path { display: none; } </style> <body> <div>Behold, a bar chart!</div> <div class="barchart_step1"></div> <script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> <script type="text/javascript" src="js/barchart_step1.js"></script> </body> </html>
Let's add a tooltip when we hover over the bar. This will reveal the favorability data point underlying the bar.
First, let's define a variable called tooltip
var tooltip = svg.append("text") .style("position", "absolute") .style("z-index", "1000") .style("visibility", "hidden") .style("fill", "#ffffff");
Let's have our tooltip begin as hidden.
Let's look back at the code that executes the bars themselves.
svg.selectAll(".bar") .data(data) .enter().append("rect")Right after
.attr("height", function(d) { return height - y(d.favorability); })we will add a mouseover event:
.on("mouseover", function(d){ tooltip.html(d.favorability ) .style("visibility", "visible") .attr("x", function() { return x(d.country) + x.rangeBand()/2 - 10; }) .attr("y", function() { return y(d.favorability) + 25; }) ;})
Let's also add a mouseout event right after that:
.on("mouseout", function(d){ tooltip.style("visibility", "hidden") ;});
Now let's add the variable for the notes on mousover:
var notes = d3.select(".barchart").append("div") .attr("class", "notes") .html("Hover over a country to see the percent of its population with" + "<span style="color:white"> favorable views of Shangri La</span>") .style("margin-bottom", "-50px") .style("margin-top", "20px") .style("margin-left", "10%") .style("font-size", "16px") .style("color", "#fc4e2a") .style("width", "400px") .style("line-height", "1em") .style("text-align", "left");
Note that when we define this variable we can define the html to show on load (Hover over a country to see...)
Find the mouseover. Right after
.attr("y", function() { return y(d.favorability) + 25; })we need to add
d3.select(".notes") .html(d.country + ": <span style="color:white">" + d.favorability + "% favorability. </span><br>" + d.description )
This indicates that we need to grab the div with class="notes" on mouseover and change the text to the variables of our choice.
Our javascript file barchart.js should look like
var margin = {top: 50, right: 20, bottom: 30, left: 60}; var width = 660 - margin.left - margin.right; var height = 400 - margin.top - margin.bottom; var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient("bottom") .tickSize(0) .tickPadding(5); var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(4) .tickSize(3); var notes = d3.select(".barchart").append("div") .attr("class", "notes") .html("Hover over a country to see the percent of its population with" + "<span style="color:white"> favorable views of Shangri La</span>") .style("margin-bottom", "-50px") .style("margin-top", "20px") .style("margin-left", "10%") .style("font-size", "16px") .style("color", "#fc4e2a") .style("width", "400px") .style("line-height", "1em") .style("text-align", "left"); var svg = d3.select(".barchart").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 + ")"); function type(d) { d.favorability = +d.favorability; return d; } d3.csv("data.csv", type, function(error, data) { console.log(data); x.domain(data.map(function(d) { return d.country; })); y.domain([0, 100]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", -45) .attr("dy", ".21em") .style("text-anchor", "end") .text("Percent with favorable view"); svg.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.country); }) .attr("width", x.rangeBand()) .attr("y", function(d) { return y(d.favorability); }) .attr("height", function(d) { return height - y(d.favorability); }) .on("mouseover", function(d){ tooltip.html(d.favorability ) .style("visibility", "visible") .attr("x", function() { return x(d.country) + x.rangeBand()/2 - 10; }) .attr("y", function() { return y(d.favorability) + 25; }) d3.select(".notes") .html(d.country + ": <span style="color:white">" + d.favorability + "% favorability. </span><br>" + d.description ) ;}) .on("mouseout", function(d){ tooltip.style("visibility", "hidden") ;}); var tooltip = svg.append("text") .style("position", "absolute") .style("z-index", "1000") .style("visibility", "hidden") .style("fill", "#ffffff"); });
<!DOCTYPE html> <meta charset="utf-8"> <style> body { background-color: black; font: 10px sans-serif; margin-left: 10%; } .bar { fill: #3690c0; } .bar:hover { fill: #fc4e2a; } .axis { font: 12px sans-serif; fill: #fff; } .axis path, .axis line { fill: none; stroke: #969696; shape-rendering: crispEdges; } .x.axis path { display: none; } </style> <body> <div>Behold, a bar chart!</div> <div class="barchart_step1"></div> <script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> <script type="text/javascript" src="js/barchart_step1.js"></script> </body> </html>