As for nested arrays, I'd take a look at the calendar and scatterplot
matrix examples:
http://mbostock.github.com/d3/ex/calendar.html
http://mbostock.github.com/d3/ex/splom.html
The calendar example has an array of years so that `vis` is a
selection of svg:svg elements, with one per year. Within each year,
there's one rect per day for that year. Similarly, in the scatterplot
matrix, there's one svg:g element per row, and then one svg:g element
per column for that row (making it a cell). The scatterplot matrix is
made a bit more complicated since I wanted to access the parent data
from the child cells, so I used a `cross` helper to copy the parent
data into the child data.
As a more general example, here's how you might construct a standard
HTML table from your data:
var table = d3.select("body").append("table");
var tr = table.selectAll("tr")
.data(data)
.enter().append("tr");
var td = tr.selectAll("td")
.data(function(d) { return d; }) // #1
.enter().append("td")
.text(function(d) { return d; }); // #2
This assumes that data is a two-dimensional array of numbers. Note
that the identity function, function(d) { return d; }), is used to
dereference each element in the array. In the first case (#1), the
data operator is evaluated on each element in `data`. For the first
row, this returns the array of numbers that we want for the cells:
[55, 44, 147, 161]. For the second row, it returns the second element
in data: [182, 174, 109, 20]. Then, the operators on the td are
evaluated per element. For the first cell in the first row, the text
operator returns 55. For the second cell in the first row, 44. And so
on.
If you want, you can run this whole thing as a single statement, and
make clever use of some JavaScript built-ins for the identity function
(#1 == Object, #2 == String):
d3.select("body").append("table")
.selectAll("tr").data(data).enter().append("tr")
.selectAll("td").data(Object).enter().append("td")
.text(String);
Mike
Sure. Let's start by looking at the simpler 1-dimensional case, say a
ul element with a variable number of child li elements. If we want to
handle the general case where we have a different number of items
across updates, then we'll need to use the `data` operator in
conjunction with the enter, update and exit selections. For example,
maybe the background-color style is a constant and only needs to be
set on enter, but the text content is derived from data and needs to
be set both on enter and update.
Let's start by doing the initial selection and data join:
var li = d3.select("ul").selectAll("li")
.data(data);
Breaking this down:
1. d3.select("ul") - select the (existing) ul element that will be the
parent node for the li elements.
2. selectAll("li") - select all the (existing) child li elements. The
first time this code is run, this selection will be empty. Subsequent
times, it will contain the previously-added elements.
3. data(data) - join the li elements with the specified array of data,
returning the update selection. This is set as the variable `li`.
Note that `li` refers to the updating selection. This means if we
previously-added li elements, then the updating selection will contain
the previously-added elements joined to the new data. So we can
immediately update those existing elements using the new data. In this
case, that might just be the next content:
// update
li.text(function(d) { return d; });
We may also want to access the entering nodes, if there are more
elements in the data array than exist in the document. We can access
placeholder nodes for these elements using the `enter()` operator, and
then immediately create and append li elements for them using
`append("li")`. From there, we can specify the desired operators.
Generally, this is a superset of what we would do on update, because
it includes both the constants and the data-driven ones:
// enter
li.enter().append("li")
.style("background-color", "red")
.text(function(d) { return d; });
Similarly, we may want to access the exiting surplus nodes and remove them:
// exit
li.exit().remove();
If you want to get fancy, you can also define separate transitions for
these selections. You can see another example of this in the source of
the chart templates:
https://github.com/mbostock/d3/blob/master/src/chart/bullet.js
So, now if we want to return to the more elaborate nested case, we can
deal with nested enter, update and exit. However, we can simplify by
making some safe assumptions. First, if we are entering new table
rows, they won't yet have any child cells. Second, if we are exiting
table rows, we can just remove the rows and not deal with exiting the
children. This results in something like this:
var tr = table.selectAll("tr")
.data(data);
// enter
tr.enter().append("tr")
.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.text(function(d) { return d; });
// update
var td = tr.selectAll("td")
.data(function(d) { return d; });
// update / enter
td.enter().append("td")
.text(function(d) { return d; });
// update / update
td.text(function(d) { return d; });
// update / exit
td.exit().remove();
// exit
tr.exit().remove();
Of course, this is the most general way of structuring your code. You
can reduce some of the code duplication by extracting your functions
and naming them, rather than using strictly anonymous functions. You
can also group your code into JavaScript functions (optionally using
the `call` operator for method chaining, if desired).
Another common case is that you have separate code paths for
initializing your visualization, and then you just want to run
updates. Even simpler if your updates never change the cardinality of
your data, so you don't need to deal with enter & exit on update. In
this case, your initialization code only needs to handle enter:
function enter() {
table.selectAll("tr")
.data(data)
.enter().append("tr")
.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.text(function(d) { return d; });
}
And your update code only needs to handle update:
function update() {
table.selectAll("tr")
.data(data)
.selectAll("td")
.data(function(d) { return d; })
.text(function(d) { return d; });
}
You can make the update code even simpler if you just update the
attributes of existing objects in-place. This way, you don't have to
rebind the data:
function update() {
table.selectAll("tr td").text(function(d) { return d.name; });
}
Mike
I thought of a couple optimizations… not that you need them, but if
you're curious:
You can improve the performance of interaction by using two different
layers (svg:g elements) in the scatterplot. The background layer
contains your dots with a fill, and the foreground layer contains the
same dots with an optional stroke. That stroke color can be "none" (or
equivalently, null) until you mouseover. This way, you don't remove
and re-append elements on interaction, or change the radius, which
causes more expensive redraws. You can even render the background
layer statically (say, as an image), since it doesn't change on
mouseover. I used that trick first in the Protovis parallel
coordinates example:
http://vis.stanford.edu/protovis/ex/cars.html
Another technique is to provide more explicit binding between related
elements, so you don't have to reselect based on data. For example, on
cell mouseover, you could have each cell's data store the associated
point selection in the scatterplot, so you don't have to reselect. You
can do that using the `each` operator. When you create the
scatterplot, you can store the reference to the circle element in the
data:
circle.each(function(d) {
d.element = this;
});
Then when you create the heatchart, collect those elements into a selection:
rect.each(function(d) {
d.elements = d3.selectAll(d.points.map(function(d) { return d.element; }));
});
Then, your onCellOver looks something like this:
d.elements.attr("r", 4).attr("stroke", "#f00").attr("stroke-width", 3);
Lastly, one JavaScript tip: if you declare named functions, rather
than binding anonymous functions to a var, JavaScript "hoists" the
function definitions to the front, so you can call them out of order.
This gives you a little more flexibility in how you structure your
code. For example, this code causes an error because `foo` is not yet
defined:
var a = foo();
var foo = function() { return 42; };
Whereas this code works fine, because the definition of `foo` is hoisted:
var a = foo();
function foo() { return 42; }
MDC says: "Another unusual thing about variables in JavaScript is that
you can refer to a variable declared later, without getting an
exception. This concept is known as hoisting; variables in JavaScript
are in a sense 'hoisted' or lifted to the top of the function or
statement. However, variables that aren't initialized yet will return
a value of undefined."
Mike