Thursday, July 26, 2012

Introduction to D3

This tutorial is in response to a request by someone who wanted a basic introduction to D3. I've never used D3 before, but I've learned a bit about it so far and this is just an introduction to get you started anyways.

Basically, what I've found is that D3 is a lot like JQuery in many regards. You can select existing (and not-yet-existing) elements, manipulate them graphically, perform animations, etc. The best aspect of D3 seems to be that although the learning curve is rather steep for anyone who's a beginning coder, it actually ends up making most tasks of displaying/arranging data on a web page very simple and easy - you just have to know how. :)

With that said, let's get started! Firstly, I recommend that you at least skim through two beginner JQuery tutorials I've made in the past:

http://www.thecodingtutorials.blogspot.com/2012/01/importing-jquery-into-your-website.html
http://www.thecodingtutorials.blogspot.com/2012/01/changing-page-content-with-jquery.html

Before being able to easily read and follow this tutorial, you'll at least need to know how to import a JavaScript file and write JavaScript code in your web page. It would help a lot if you knew generally how element selection and such works in JQuery as well.


The first thing I like to do when learning a new language/framework is to find helpful links to its documentation, tutorials, etc. Here's what I've found:

(Download D3 here - I'm using the production version for this tutorial)
http://d3js.org/

https://github.com/mbostock/d3/wiki
https://github.com/mbostock/d3/wiki/API-Reference
http://christopheviau.com/d3_tutorial/
https://github.com/mbostock/d3/wiki/Selections
http://bost.ocks.org/mike/join/


Alright, enough talk - let's see an example of D3 working! Here is a pretty basic one that I've come up with... It's more to prove that D3 is working and that you get the gist of how it's used:

http://thecodingwebsite.com/tutorials/d3/intro/d3test.html

Here is the code:

<html>

<head>

<script type="text/javascript" src="d3.v2.min.js"></script>

<script type="text/javascript">

 var test = d3.select("body").data([1, 2, 3]);
 
 test.enter().append("p").append("test").text(function(d)
 {
  return "Hey there... #" + d;
 });

</script>

</head>

<body>

</body>

</html>

You'll notice that I don't have anything in the body of the page. I simply include D3 in the head (note: it must be in the same directory as the web page if you use the same "src" value that I used above), and then I have one little snippet of JavaScript code:

var test = d3.select("body").data([1, 2, 3]);
 
 test.enter().append("p").append("test").text(function(d)
 {
  return "Hey there... #" + d;
 });

First, I start off with "d3". You don't have to instantiate (create) d3 or anything, you just have to include the d3 script in your page and it will work.

Then, from d3, I select the body of the page. Simple enough - just make sure you wrap what you're selecting in quotes or it won't work.

After selecting the body of the page, I add some data. This data is in the form of an array which has 3 values: 1, 2, and 3. Notice how I put it in square brackets and separated each value by a comma. That's one of the standard ways of placing an array with the values already in the page's code in JavaScript. I store the result of this in a variable called "test".


Next, I call the "enter" function (using the "test" variable I created earlier). This is where it starts to get confusing. :) In D3, there are 3 selections that you can use after adding data:
  1. Update - this is automatic, e.g. you don't write "data([1, 2, 3]).update()" - it doesn't exist, because it doesn't need to.

  2. Enter

  3. Exit
Right now, we're just going to use enter. You should perform tasks after enter whenever new data is being added. You would use update to update existing data (e.g. when a value changes), and you would use exit when data is being removed.


Lastly, I append a child "p" (paragraph) element, append a child "test" (a custom element that I made up) element onto that, and then add text inside that says "Hey there... #" with each data number at the end. This is what the resulting HTML code looks like:

<p><test>Hey there... #1</test></p>
<p><test>Hey there... #2</test></p>
<p><test>Hey there... #3</test></p>

Of course, you can't actually see this HTML code when you view the source code or anything, as it's dynamically generated. Here's a picture of the result:










Ooh. Pretty, isn't it? :)

No, not really. :/ If we just wanted to display text, we would use plain HTML! We want to make graphs, so let's do it!

If you've tried using D3 before, it may not surprise you that it took me a long time to figure out the necessary code to make just a simple graph, and I've been writing code for years! It gets tricky because there are several factors involved:
  1. You have to write working JavaScript code. If it's somehow invalid, then whenever your code gets an error it will stop running immediately!
  2. You have to know your way around SVG (Scalable Vector Graphics, what we're going to use to draw the graph) - this will help you get started:

    http://www.w3schools.com/svg/default.asp

    If your SVG code has a spelling error or you're missing a fundamental attribute (such as setting the width of a rectangle), it won't work.
  3. You have to use D3 properly.
My goal here is to help you with #3, and you can pick up on #'s 1 and 2 through my examples and on your own. With that said, let's look at my graph example:

http://thecodingwebsite.com/tutorials/d3/intro/d3graphtest.html

<html>

<head>

<script type="text/javascript" src="d3.v2.min.js"></script>

<script type="text/javascript">

 window.onload = function()
 {
  //Create the SVG graph.
  var svg = d3.select("body").append("svg").attr("width", "100%").attr("height", "100%");
  
  
  
  //Add data to the graph and call enter.
  var dataEnter = svg.selectAll("rect").data([8, 22, 31, 36, 48, 17, 25]).enter();
  
  
  
  //The height of the graph (without text).
  var graphHeight = 450;
  
  //The width of each bar.
  var barWidth = 80;
  
  //The distance between each bar.
  var barSeparation = 10;
  
  //The maximum value of the data.
  var maxData = 50;
  
  
  
  //The actual horizontal distance from drawing one bar rectangle to drawing the next.
  var horizontalBarDistance = barWidth + barSeparation;
  
  
  //The horizontal and vertical offsets of the text that displays each bar's value.
  var textXOffset = horizontalBarDistance / 2 - 12;
  var textYOffset = 20;
  
  
  //The value to multiply each bar's value by to get its height.
  var barHeightMultiplier = graphHeight / maxData;
  
  //The actual Y position of every piece of text.
  var textYPosition = graphHeight + textYOffset;
  
  
  
  //Draw the bars.
  dataEnter.append("rect").attr("x", function(d, i)
  {
   return i * horizontalBarDistance;
  }).attr("y", function(d)
  {
   return graphHeight - d * barHeightMultiplier;
  }).attr("width", function(d)
  {
   return barWidth;
  }).attr("height", function(d)
  {
   return d * barHeightMultiplier;
  });
  
  
  
  //Draw the text.
  dataEnter.append("text").text(function(d)
  {
   return d;
  }).attr("x", function(d, i)
  {
   return i * horizontalBarDistance + textXOffset;
  }).attr("y", textYPosition);
 }

</script>

</head>

<body>

</body>

</html>

Here's a screenshot of what it looks like:





























Okay, so this bar graph is a lot prettier than what we had before! It could use some improvements, but it gets the job done: with not too much code, it displays our data as a graph using D3 and SVG.

Let's take a look at pieces of the code:

 window.onload = function()
 {
  //Create the SVG graph.
  var svg = d3.select("body").append("svg").attr("width", "100%").attr("height", "100%");
  
  
  
  //Add data to the graph and call enter.
  var dataEnter = svg.selectAll("rect").data([8, 22, 31, 36, 48, 17, 25]).enter();

First, I had all of my code run once the window is loaded. For some reason, dynamic SVG code doesn't seem to work otherwise. To me, that's all you and I need to know about it. If you want to know why, you can do your own research. :)

Once the window has loaded, I select the body of the page and append "svg" onto it (which is what tells the browser that we're using SVG, basically). I also set the width and height properties of "svg" to "100%", meaning that our SVG graph should take up the entire page.

Note: this does not mean that we specifically have to draw something on the entire page - it only means that we can. If your width and height are not set (in my browser they default to a few hundred pixels or so in both dimensions) or they are set too small, your graph may end up being cut off!

I then have it select all "rect" elements. Notice that as of now there aren't any! Basically, you're supposed to tell D3 that you're going to want to have rectangles. I believe they do this for the enter, update, and exit functionality, if I'm not mistaken.

Then I add some bogus data and call the "enter" function. Notice how I store the result of "enter" in the new "dataEnter" variable. This is primarily because I will be using "enter" twice later on.


Then I do some math and variable setup... Feel free to read through that section, read the comments, try to figure out what I did and why I did it, etc. Basically, you can customize a few simple aspects of the bar graph without having to read through any intense code. It also makes the code more efficient and easier to read when you make it like I did.

Moving on...

  //Draw the bars.
  dataEnter.append("rect").attr("x", function(d, i)
  {
   return i * horizontalBarDistance;
  }).attr("y", function(d)
  {
   return graphHeight - d * barHeightMultiplier;
  }).attr("width", function(d)
  {
   return barWidth;
  }).attr("height", function(d)
  {
   return d * barHeightMultiplier;
  });
  
  
  
  //Draw the text.
  dataEnter.append("text").text(function(d)
  {
   return d;
  }).attr("x", function(d, i)
  {
   return i * horizontalBarDistance + textXOffset;
  }).attr("y", textYPosition);
 }

Alright, this is where it gets interesting! Now we're actually drawing the graph (the bars and the text) based off of the D3 setup of SVG, the "enter" function (stored in "dataEnter"), and the math and variables from earlier.

This code should be relatively self explanatory (for the most part), so I'll go through it briefly and explain some of the not-so-well-known details along the way.

I add an SVG rectangle, position it, and size it for every piece of data that I added earlier. D3 automatically does this for every piece of data. If we were to reduce the code a little bit (and only care about drawing the rectangle, not the text or anything else), it would look like this:

  d3.select("body").append("svg").attr("width", "100%").attr("height", "100%")
.selectAll("rect").data([8, 22, 31, 36, 48, 17, 25]).enter()
.append("rect").attr("x", function(d, i)
  {
   return i * horizontalBarDistance;
  }).attr("y", function(d)
  {
   return graphHeight - d * barHeightMultiplier;
  }).attr("width", function(d)
  {
   return barWidth;
  }).attr("height", function(d)
  {
   return d * barHeightMultiplier;
  });

Just to make sure I wasn't misinforming you, I tried making the code like this (with the text part commented out) and the bars displayed the same as before.

Now let's take a look at how to use functions for D3 graphs... Here are the first two that I used (for positioning the rectangle):

dataEnter.append("rect").attr("x", function(d, i)
  {
   return i * horizontalBarDistance;
  }).attr("y", function(d)
  {
   return graphHeight - d * barHeightMultiplier;
  })

I declared an anonymous function (also called a "lambda function" sometimes) as the second parameter of the "attr" (attribute) for "x" (the X position of the rectangle). I also told it that there are two parameters, "d" and "i", that will need to be passed to the function. If you look at the function for "y" (the Y position of the rectangle), however, you'll see that I only told it to use one parameter, "d".

So why did I do this? Well, let's take a look at the D3 API's "attr" function:

https://github.com/mbostock/d3/wiki/Selections#wiki-attr

which says this: 
selection.attr(name[, value])

...If value is a constant, then all elements are given the same attribute value; otherwise, if value is a function, then the function is evaluated for each selected element (in order), being passed the current datum d and the current index i, with the this context as the current DOM element. The function's return value is then used to set each element's attribute. A null value will remove the specified attribute...
So we could just make the code something like this:

.attr("x", 5)

but this would give the same X position (5) to every rectangle - we don't want that! :) So instead, we use a function, which allows us to set the X position of every rectangle based off of the data value (stored in "d") and/or the index of each piece of data (stored in "i").

This was the data array that I used: [8, 22, 31, 36, 48, 17, 25]. This means that for the first piece of data, "d" will equal "8", then for the second, "d" will equal "22", etc.

So what is the index, then? It's the position in the array. For the first piece of data, "i" will equal "0". You might be wondering why it would equal "0" and not "1" - the reason is that in almost every programming language in use nowadays, arrays start counting at 0 rather than 1. So the first element in the array will be "0", the second will be "1", etc. - the same goes for the value of "i".

This is why I used "d" and "i" as parameters for setting the X position of each rectangle. Even though I didn't use "d", I had to put something there as a placeholder so that I could access the index. Then, I multiplied the index by an unchanging value to space out each of the bars in the graph:

.attr("x", function(d, i)
  {
   return i * horizontalBarDistance;
  })



However, for the Y position, I only needed the value of each piece of data. Every rectangle in SVG is positioned by its top (Y) left (X) corner, regardless of its width and height. In case you don't know, the coordinates (0, 0) are the top left of the page - this is standard for computer graphics.

So, in order to find the Y position of each bar, I found what the height of each bar is going to be (d * barHeightMultiplier) and then subtracted it from the graph's height:

.attr("y", function(d)
  {
   return graphHeight - d * barHeightMultiplier;
  })



Then finally I sized each bar (with the "width" and "height" attributes), added some text (with the text's contents being the value of each piece of data), and positioned the text.



It's rather simple code - for me at least, because I've been doing this kind of stuff for years. With just a little bit of math/thinking and all of the information I've given you so far, you're on your way to creating very pretty, intricate graphs using D3 and SVG. Good luck! :)

25 comments:

  1. Replies
    1. Thank you for the feedback. I appreciate it!

      - Andrew

      Delete
  2. I've been looking at this page for two days...I'm wondering, how can you read data from a text file (with no commas..) and input it as the data to create these graphs?

    I've been kind of stuck, =/

    Also, if you wanted to add animation to your graphs, would you place the commands for animation after the section you "draw bars"?

    All in all I love the tutorials. =)

    ReplyDelete
    Replies
    1. Right now, my buyer is having me make a tutorial using the CSV (Comma Separated Values) file format (commonly used in spreadsheets). D3 actually has some functions to make this task very easy with CSV files. Wait a couple of days or so and I should have this tutorial finished.

      If you don't want to do this, I believe you'll have to load the text file in (from a server) in JavaScript (or PHP if you like) and store it in a JavaScript array that you pass to the "data" function.

      I'm fairly certain an animation tutorial will come soon afterwards, but basically I think you'll want to use the "selection.on" function to listen for events and respond with a function:

      https://github.com/mbostock/d3/wiki/Selections#wiki-on

      More details with more accuracy should come at a later time.

      Thank you!

      - Andrew

      Delete
    2. Update: good news! I just finished my CSV tutorial, and it turns out that I was wrong and you can just use a text file. It's fairly simple, too! :)

      Here you go - it's pretty self explanatory:

      http://thecodingtutorials.blogspot.com/2012/07/using-csv-and-text-files-with-d3.html

      Enjoy!

      - Andrew

      Delete
  3. AWESOME thank u!!! I'll check it out!

    ReplyDelete
  4. Thanks for this incredible introduction! It helped me a lot :)

    ReplyDelete
  5. Thank you Andrew! =DD

    ReplyDelete
  6. Cheers mate, this was a nice intro. Thanks for putting the time in. I've used SVG via Raphaël and jQuery for a while but after checking the examples out I find I must know more. Will this addiction ever end... :-)

    ReplyDelete
    Replies
    1. Thanks!

      Yeah, I've used all 3 before as well.

      Yes, it will end. Do you want to know why? When I found out that SVG graphics are essentially "addition-only", it ended for me. What I mean by this is that you either have to clear the entire canvas or you have to erase over top of individual elements manually - there's no "automatic erase/delete" like there is in the "real" programming languages.

      - Andrew

      Delete
    2. Hi Andrew,
      I think I mis-understand "automatic erase/delete". Are you referring to e.g. Java's memory clean up when objects/variables are no longer being referenced?
      I forked an old fiddle to demonstrate how I delete paths when I don't need them any more http://jsfiddle.net/chrisloughnane/AwMfq/1/

      C.

      Delete
    3. Java is not the same thing as JavaScript.

      Ahh, so Raphael has a Remove function. I remember not liking some limitation about Raphael before, although it's been too long since I've used it.

      - Andrew

      Delete
  7. Thanks! It is very helpful to try D3.

    ReplyDelete
    Replies
    1. Thanks! I'm glad I could help.

      - Andrew

      Delete
  8. Great job Andrew! Thanks for explaining it piece by piece.

    ReplyDelete
  9. I like how your writing is in ACTUAL ENGLISH, rather than some professor speak IT nerd bs. I understood it, as did my cat. Cheers.

    ReplyDelete
    Replies
    1. Ahh yes, the ever puzzling dilemma of one's ability to teach a cat how to code. Thanks! ;)

      - Andrew

      Delete
  10. In the first portion, you selected the wrong element. Tripped me up for a bit, since I'm such a newbie at this haha.
    // correct
    var test = d3.select("p").data([1, 2, 3]);
    // incorrect
    var test = d3.select("body").data([1, 2, 3]);

    Couldn't get the first element to print when I tried it with the latter version. Thanks for the tutorial!

    ReplyDelete
    Replies
    1. The code that I wrote worked perfectly for the scenario that I had presented and the version of D3 that I had used. You can check the source code in the example page and verify this.

      So you're either not matched to my scenario or my version of D3 at the time that I had created this tutorial, and it seems here like it would probably be the former case.

      - Andrew

      Delete
    2. It may also depend on the browser/version you are using as well.

      - Andrew

      Delete
  11. Thanks for giving best example with better explanation

    ReplyDelete