Saturday, September 1, 2012

Using Multi-Column Data with D3 Part 2

Okay, so after reading my previous tutorial we are now capable of loading in a CSV file with multi-column data and completely reconstructing it.

That's great! but... what about being able to categorize, filter, (somewhat) transition, and color the representation of that data? That's what this tutorial is all about! :)


To save some of you a little time deciding whether or not to read this tutorial, this is what our final product is going to be:

http://thecodingwebsite.com/tutorials/d3/multicolumn/d3multicolumn.html

Several things you should notice:
  1. It starts out showing all of the data.
  2. If you click on a category at the top, only the data that is in that category will be showed.
  3. Changing categories will continue to work (without refreshing) as long as your computer remains intact.
  4. Any data in "Section 3" (of any category) has blue text and any data in "Section 5" (of any category) has green text, while the rest of the text is black.
  5. The colors of the rectangles for "Data 1", "Data 2", and "Data 3" are blue, green, and red, respectively.
  6. The current filter category is being displayed to the right of the list of categories at the top (e.g. "Currently showing: all").
  7. The rectangles are animated - whenever the page loads and/or a category is selected, all of the rectangles gradually expand to their proper width (starting at 0) and change to their proper color (starting at black).

Due to my previous tutorial, you already know how to reconstruct the data. So, all I have to do now is explain how and why I did what I did.

Here's the source code of the page - don't worry, I'll break it into smaller pieces of code as I explain it:

<html>

<head>

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

<script type="text/javascript">

 var graphHeight = 5000;
 
 var xOffset = 95;
 var yOffset = 15;
 var rightPadding = 50;
 var bottomPadding = 5;
 var width = window.innerWidth - xOffset - rightPadding;
 var verticalSpacing = 10;
 var height = graphHeight - yOffset - bottomPadding;
 
 var unfilteredData;
 
 var svg;
 var maxValue, hMultiplier, barHeight, barYMultiplier;
 
 window.onload = function()
 {
  //Create the SVG graph.
  svg = d3.select("body").append("svg").attr("width", "100%").attr("height", graphHeight);
  
  d3.csv("data.csv", function(d)
  {
   unfilteredData = d;
   
   maxValue = d3.max(unfilteredData, function(d)
   {
    return Math.max(d["Data 1"], d["Data 2"], d["Data 3"]);
   });
   
   hMultiplier = width / maxValue;
   barHeight = height / (unfilteredData.length * 3) - verticalSpacing;
   barYMultiplier = barHeight + verticalSpacing;
   
   refilter("all");
  });
 };
 
 function refilter(filterCategory)
 {  
  if (filterCategory == "all")
  {
   data = unfilteredData;
  }
  else
  {
   data = unfilteredData.filter(function(d)
   {
    return (d["Site Type"] == filterCategory);
   });
  }
  
  d3.select("#showing").text(filterCategory);
  
  
  
  svg.selectAll("g").remove();
  
  
  
  //Add data to the graph.
  var dataAdd = svg.selectAll("g").data(data);
  
  var dataEnter = dataAdd.enter().append("g");
  
  for (var dataCount = 1; dataCount <= 3; ++dataCount)
  {
   dataEnter.append("rect").attr("x", xOffset).attr("num", function(d, i)
   {
    return i;
   });
   
   dataEnter.append("text").attr("font-size", 10).attr("num", function(d, i)
   {
    return i;
   });
  }
  
  
  dataAdd.selectAll("rect").attr("y", function(d, i)
  {
   return d3.select(this).attr("num") * barYMultiplier * 3 + i * barYMultiplier + yOffset;
  }).attr("height", barHeight).transition().duration(1000).attr("width", function(d, i)
  {
   return d["Data " + (i + 1)] * hMultiplier + 5;
  }).attr("stroke", "gray").attr("fill", function(d, i)
  {
   if (i == 0)
   {
    return "blue";
   }
   else if (i == 1)
   {
    return "green";
   }
   else
   {
    return "red";
   }
  });
  
  dataAdd.selectAll("text").text(function(d, i)
  {
   return d["Site Type"] + ", " + d["Media Type"];
  }).attr("x", 0).attr("y", function(d, i)
  {
   return d3.select(this).attr("num") * barYMultiplier * 3 + i * barYMultiplier + yOffset + 6;
  }).attr("fill", function(d)
  {
   if (d["Media Type"] == "Section 3")
   {
    return "blue";
   }
   else if (d["Media Type"] == "Section 5")
   {
    return "green";
   }
   else
   {
    return "black";
   }
  });
 }

</script>

</head>

<body>

<a href="javascript:refilter('all');">All Categories</a>   <a href="javascript:refilter('Category 1');">Category 1</a>   <a href="javascript:refilter('Category 2');">Category 2</a>   <a href="javascript:refilter('Category 3');">Category 3</a>   <a href="javascript:refilter('Category 4');">Category 4</a>   <a href="javascript:refilter('Category 5');">Category 5</a>   <a href="javascript:refilter('Category 6');">Category 6</a>   

Currently showing: <span id="showing"></span>

</body>

</html>

The JavaScript starts out with a bunch of variables, and there are some various calculations throughout the code - both of these will be skipped in my explanation process as they are self explanatory and they would turn this tutorial into a torture device rather than having it be somewhat useful.

I did start out with an interesting piece of code that uses D3's "max" functionality:

   maxValue = d3.max(unfilteredData, function(d)
   {
    return Math.max(d["Data 1"], d["Data 2"], d["Data 3"]);
   });

Basically, I told D3 to evaluate every single row of data, use JavaScript's "Math.max" function to determine which of the 3 pieces of data in each row has the highest value, return it, and determine the highest value out of all of the data. This value (now stored in "maxValue") can now be used to scale the entire graph horizontally.

Next, I have a function called "refilter", which starts out with this code:

 function refilter(filterCategory)
 {  
  if (filterCategory == "all")
  {
   data = unfilteredData;
  }
  else
  {
   data = unfilteredData.filter(function(d)
   {
    return (d["Site Type"] == filterCategory);
   });
  }
  
  d3.select("#showing").text(filterCategory);

The category to filter out is passed as "filterCategory" to the function. I call "refilter" once when the window loads (using "all" as the filter), and then I call "refilter" again every time the user clicks on a category link (you can see this by looking at the HTML code near the bottom).

Notice that I made it so that if its value is equal to "all" it will not perform any filtering. If it isn't set to "all", however, it will use D3's "selection.filter" function. In doing this it will go through each row of data and determine if the "Site Type" attribute is equal to the "filterCategory" value. If it is, then it will return true; otherwise, it will return false. Any row of data that returns true is kept in the selection, and vice versa.

I then have it display the "filterCategory" value, which you can see to the right of the list of categories ("Currently showing: all", or etc.).

Then:

  svg.selectAll("g").remove();
  
  
  
  //Add data to the graph.
  var dataAdd = svg.selectAll("g").data(data);
  
  var dataEnter = dataAdd.enter().append("g");
  
  for (var dataCount = 1; dataCount <= 3; ++dataCount)
  {
   dataEnter.append("rect").attr("x", xOffset).attr("num", function(d, i)
   {
    return i;
   });
   
   dataEnter.append("text").attr("font-size", 10).attr("num", function(d, i)
   {
    return i;
   });
  }

I make sure to clear the graph entirely before adding any new elements. I also perform the standard "selectAll", "data", "enter", and "append" calls that we should be used to by now.

Then I loop through each row of data 3 times (with "dataCount" equaling "1", "2", and "3"). For each 3 pieces of data in each row, I add a rectangle and some text to represent it visually. Notice that not all of the properties have been set with the "dataEnter" - these are only the values that I want the rectangles and text to have as soon as they are added to the page.

Pay attention to the "num" property here. It is a custom attribute that I have created. All it does is keep track of the current piece of data (either "1", "2", or "3" to represent "Data 1", "Data 2", and "Data 3") in the row that is being set up.

Finally, I set the properties of all of the rectangles and text:

  dataAdd.selectAll("rect").attr("y", function(d, i)
  {
   return d3.select(this).attr("num") * barYMultiplier * 3 + i * barYMultiplier + yOffset;
  }).attr("height", barHeight).transition().duration(1000).attr("width", function(d, i)
  {
   return d["Data " + (i + 1)] * hMultiplier + 5;
  }).attr("stroke", "gray").attr("fill", function(d, i)
  {
   if (i == 0)
   {
    return "blue";
   }
   else if (i == 1)
   {
    return "green";
   }
   else
   {
    return "red";
   }
  });
  
  dataAdd.selectAll("text").text(function(d, i)
  {
   return d["Site Type"] + ", " + d["Media Type"];
  }).attr("x", 0).attr("y", function(d, i)
  {
   return d3.select(this).attr("num") * barYMultiplier * 3 + i * barYMultiplier + yOffset + 6;
  }).attr("fill", function(d)
  {
   if (d["Media Type"] == "Section 3")
   {
    return "blue";
   }
   else if (d["Media Type"] == "Section 5")
   {
    return "green";
   }
   else
   {
    return "black";
   }
  });

As I said before, I am not going to go through each line of code, as you should be able to understand most of it by now. Notice that I made the rectangle's width transition in by calling "transition", calling "duration" (with a value of 1000 milliseconds, or 1 second), and then setting the width, stroke, and fill attributes afterwards. All 3 of these attributes (width, stroke, and fill) are transitioned in over the course of 1 second because they are set after "transition" is called.

Lastly, I reuse the "num" property that was set beforehand to determine which piece of data is being represented and to color the rectangle properly.


That's it! Using the techniques I've shown you above, you should be capable of categorizing, filtering, (basic) transitioning, and coloring the data represented in your graph. Good luck! :)

4 comments:

  1. One more time, Andrew : Thank you very much for these excelent tutorials. I learned a lot about D3, with them.

    ReplyDelete
  2. Thanks a lot for the brilliant tutorial Andrew. Hats off!

    Keep it up :)

    ReplyDelete