This lesson covers:
D3.js is a graphing library that makes building awesome graphs and visualizations much easier. It also has a different way of thinking about updating the DOM that is more in tune with animations. You will find that updating the products page to add animation is difficult. JQuery has a nice animation function, and there are always css3 transistions, but it will get crazy pretty quick. You will find that d3 has a harder learning curve to get started, but adding on additions or more complex data sets will be easier.
Here is a graph from the admin page.
// calorie graph
var titleHeight = 30;
chart = d3.select("#calorieGraph").append("svg")
.attr("class", "chart")
.attr("width", 200)
.attr("height", (20 * calData.length) + titleHeight );
x = d3.scale.linear()
.domain(d3.extent(calData, function(d) { return d.key; }))
.range([0, 200]);
var y = d3.scale.ordinal()
.domain([0,10])
.rangeBands([0, 200]);
var groups = chart.selectAll("g")
.data(calData)
.enter().append('g');
// create bars
groups.append('rect')
.attr("y", function(d,i) { return (i * 20) + titleHeight; })
.attr('width', function(d) { return (y(d.value)) + 25; })
.attr('height', 20);
It is a very basic bar graph but it introduces a couple things that are central to d3. I have linked the graph to ‘calories’ which is one of the inputs on my create page. If you have used something different, and I highly advise that you try something different here, remember to change the names to make sense.
First we create the element in the DOM.
chart = d3.select("#calorieGraph").append("svg")
.attr("class", "chart")
.attr("width", 200)
.attr("height", (20 * calData.length) + titleHeight );
we are setting the width and the height, and a class here.
x = d3.scale.linear()
.domain(d3.extent(calData, function(d) { return d.key; }))
.range([0, 200]);
var y = d3.scale.ordinal()
.domain([0,10])
.rangeBands([0, 200]);
These are helper functions that d3 lets us create. They normalize widths and heights of the bar graph. But we will get into that later.
var groups = chart.selectAll("g")
.data(calData).enter();
This is actually where the magic happens. What we are doing is taking what we defined in chart, telling d3 to add a ‘g’ element for each object in calData, and selecting the elements we are adding (elements that are ‘entering’ the grapth). When we add data d3 seperates the data into three piles: enter, update, and exit. The enter method works like an each loop, everything afterwards is applied to each element in that group. This allows you to animate each group seperately.
// create bars
groups.append('rect')
.attr("y", function(d,i) { return (i * 20) + titleHeight; })
.attr('width', function(d) { return (y(d.value)) + 25; })
.attr('height', 20);
// create keys
groups.append('text')
.attr("dy", "1em")
.attr('class','text')
.attr('dy', function(d,i) { return (i * 20) + (titleHeight + 15); })
.attr('dx', 5)
.attr("text-anchor", "end") // text-align: right
.text(function(d) { return d.key; });
chart.append('text')
.attr("dy", "1em")
.attr('class','title')
.attr('dy', 20)
.attr('dx', 2)
.attr("text-anchor", "start") // text-align: right
.text("Calories");
And finally we get down to business. We are appending elements (either ’rect’s or ’text’s) and then setting attributes on each of them.
There are three things that are different:
1. We are using svg instead of divs.
2. D3 does not have a model. Data is attached to the DOM element
3. Updating is a three step process. Find elements / find data, combine and process, flush out un-needed elements.
TODO
TODO
d3.json('/products',function(d) {});
With d3, it is easy to pull data from the server, although you do not have many options. They work similar to $.json(), except there is also, .text(), .html(), .xml(), .tsv()… You can also use the native XHR object with d3.xhr.
Inside the callback you want to put all of your graph code. The next step is setting up the data for the graph. We will be using crossfilter this time, and go deeper into it later.
calorie = cf.dimension(function(d) { return +d.calories; });
calories = calorie.group();
var calData = calories.all();
Basically, we are creating key value pairs for the calorie attributes of each data element. calData ends up looking like this:
[
{
key : "500",
value : "2"
},
{
key : "600",
value : "7"
}
]
Key is equal to all the d.calories, and value is how many times they appear. You will want to play around with this, of course, for other graphs, but the important thing is that one value in each object can be used as a key, or a unique identifier.
The whole thing together looks like this:
d3.json('/products',function(d) {
cf = crossfilter(d);
calorie = cf.dimension(function(d) { return d.calories; });
calories = calorie.group();
var calData = calories.all();
category = cf.dimension(function(d) { return d.category; });
categories = category.group();
var catData = categories.all();
// calorie graph
var titleHeight = 30;
chart = d3.select("#calorieGraph").append("svg")
.attr("class", "chart")
.attr("width", 200)
.attr("height", (20 * calData.length) + titleHeight );
x = d3.scale.linear()
.domain(d3.extent(calData, function(d) { return d.key; }))
.range([0, 200]);
var y = d3.scale.ordinal()
.domain([0,10])
.rangeBands([0, 200]);
var groups = chart.selectAll("g")
.data(calData)
.enter();
// create bars
groups.append('rect')
.attr("y", function(d,i) { return (i * 20) + titleHeight; })
.attr('width', function(d) { return (y(d.value)) + 25; })
.attr('height', 20);
// create keys
groups.append('text')
.attr("dy", "1em")
.attr('class','text')
.attr('dy', function(d,i) { return (i * 20) + (titleHeight + 15); })
.attr('dx', 5)
.attr("text-anchor", "end") // text-align: right
.text(function(d) { return d.key; });
chart.append('text')
.attr("dy", "1em")
.attr('class','title')
.attr('dy', 20)
.attr('dx', 2)
.attr("text-anchor", "start") // text-align: right
.text("Calories");
});
This is where you (and I) say: “But wait! We are updating the DOM like a crazy person. There must be a better way!”
I will agree with you here. It looks like d3 is updatind each individual element seperately. There must be some magic going on behind the scenes or else this would get very slow. It must be doing something to take care of this problem because the examples on their website, are awe inspiring, and none of them feel slow.
“But they are attaching data to the DOM. That can’t be good.”
It seems inefficient to attach data to the DOM, since reading from the DOM is one of the most expensive actions you can do. Again, the examples speak for themselves. TODO: research this topic more.
I think this is a good time to bring something up. Although it is great to practice the craft of creating javascript with graphs like these, it is important to remember the art. What do your users want to see? That is the graph you should build, not the one that looks the best, or is the easiest.
There is no such thing as a great website about nothing.