The `d3.json` method is for loading a JSON file asynchronously
(otherwise known as "XHR" or "AJAX"). The first argument is the path
to the JSON file, and the second argument is the callback function
that will be invoked, at an indeterminate future time, when the data
is loaded. A simple example:
d3.json("fileName.json", function(json) {
alert("Hello, " + json);
});
You'll probably want to put your visualization code inside the
callback function, as the code will likely depend on the data being
loaded. Another way to do it is to put your code inside another
function, say:
function update(json) {
… do visualization stuff here …
}
Then assign that function as the d3.json callback:
d3.json("fileName.json", update);
You can see a working example of using d3.csv here. It's the same as
d3.json, except it loads a CSV file rather than a JSON file. Both
d3.csv and d3.json use the same underlying method (d3.xhr) for loading
files:
http://mbostock.github.com/d3/ex/calendar.html
One important note:
> http://www.cbc.ca/data/statsfeed/sports/hockey/boxscore/boxscore_2011051005.json
You can't load this file directly, because CBC isn't setting the
appropriate CORS [1] header, Access-Control-Allow-Origin: *. This file
is only accessible if you are making a request from cbc.ca. However,
you can download the file and load it locally if you don't need it
loaded dynamically from the server.
Mike
My general recommendation is to not use code if you don't know what it
does ;) , but that's not a fair thing to say if I haven't documented
everything yet. I don't think you need to use the nest operator in
this case. D3's nest operator is a port from Protovis, which is
documented here:
http://vis.stanford.edu/protovis/jsdoc/symbols/pv.Nest.html
You'd use the nest operator if the data you loaded were in some flat,
tabular structure, and you wanted to organize it into a hierarchical
structure to make it easier to visualize. For example, if I had
tabular data on Barley yields:
{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" },
{ yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" },
{ yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...
I might want to group this data by variety and then by year in order
to construct a small-multiples visualization. However, if I didn't
need to regroup my data into a hierarchy, then there's no reason to
use the nest operator.
Similarly, the d3.entries method is a port from Protovis:
http://vis.stanford.edu/protovis/jsdoc/symbols/pv.html#.entries
You probably pulled that code from one of the hierarchical examples,
like the treemap:
http://mbostock.github.com/d3/ex/treemap.html
I use the `entries` operator in the treemap example because I need to
convert a JSON object representing the hierarchy into a structure that
I can recursively traverse using the treemap layout's `children`
function:
function(d) { return isNaN(d.value) ? d3.entries(d.value) : null; }
Basically, I need to distinguish when one of the nodes in the tree has
a numeric value (in which case it is a leaf node, and there are no
more children to traverse), and a non-numeric value (in which case the
value is another node in the tree, with children to traverse).
So again, you probably don't need to use d3.entries. When in doubt,
leave it out!
Let's take a look at your code:
chart.data(d3.entries(json)).selectAll("div")
.enter().append("div")
.text(function(d) { return d.data.key;});
It looks like you're being mislead by the treemap example. I would
start by reading some of the introductory documentation to learn the
basis. Specifically, these two tutorials:
http://mbostock.github.com/d3/tutorial/bar-1.html
http://mbostock.github.com/d3/tutorial/bar-2.html
There's also a good thread here where I explain in detail how
selectAll + data + enter + append works:
http://groups.google.com/group/d3-js/browse_thread/thread/c4393e37f923a3f5#msg_4f5975565c9066f7
To address some specific issues with your code snippet:
1. What aspect of your data do you want to display? The JSON file you
are loading has the following structure:
{
"awaygoaltenders": […],
"awayskaters": […],
"dt": "May 10, 2011 …",
"game": … game object containing additional fields …,
"homegoaltenders": […],
"homeskaters": […],
"results": […],
"threestars": […]
}
The first thing you need to do is to give D3 an array of data to
display. In your first email, you write, "I'd like to modify the bars
to represent
SOG (shots on goal) and Time on ice (TOI) for the home team skaters."
So probably your data then is the `homeskaters`, which is the array of
objects that represent the home team skaters. Each of these objects
has a `SOG` and `TOI` attribute that you can display, say as a scatter
plot. Speaking of which, take a look at this example:
https://github.com/mbostock/d3/blob/master/examples/dot/dot.html
So, instead of `data(d3.entries(json))`, you want `data(json.homeskaters)`.
2. To which elements do you want to bind your data?
Your current code binds the data to the parent chart element. But you
want the cardinality of your data to match the cardinality of the
selection—which is to say, each data object representing a home skater
should have a corresponding DOM element that represents the skater,
correct? So you'll want to bind the data to the `selectAll("div")`
rather than `chart`.
This also fixes the error message you're seeing: you can't call
`enter()` on a selection until you bind data to it, because it's only
through the process of binding data to a selection that the entering
nodes are defined. That's discussed in the tutorials I linked and the
overview:
http://mbostock.github.com/d3/#enter_and_exit
3. Lastly, what attributes of the data do you want to access?
You're currently setting the text content of each div to `d.data.key`.
With the fixes above in #1 and #2, the argument `d` to your function
represents the home skater data object. That is, an element in the
array `json.homeskaters`. These objects have attributes such as SOG
for shots-on-goal and TOI for time-on-ice. If you want to display
these values, you could say:
.text(function(d) { return d.SOG + " / " + d.TOI; })
Putting these three things together, you want something like this:
chart.selectAll("div")
.data(json.homeskaters)
.enter().append("div")
.text(function(d) { return d.SOG + "/" + d.TOI;});
Let me know if I can offer further clarification.
Mike
var data; // a global!
d3.json("boxscore.json", function(json) {
data = json;
… display your chart here, using data …
});
If you don't save the data (the `json` argument) somewhere, it falls
out of scope when your callback function returns, and you can't access
it anymore. An equivalent method of making the `json` data global is
to assign it as a property of `self`, which is the current window (and
the global scope).
Next, you should make the scales global, too. In your current code,
you're declaring `w` inside the callback. This means that the scale
will also fall out of scope and cease to exist when your function
returns! The data's not available when the page initially loads, but
that's fine—we'll declare the scale, and then set the domain later
when the data is loaded:
var w = d3.scale.linear()
.range([0, 250]);
d3.json("boxscore.json", function(json) {
data = json;
w.domain([0, d3.max(data.homeskaters, function(d) { return d.SHF; })]);
… etc. …
});
Now you have access to both `w` and `data` when you later want to transition.
If you want to get fancy (and I'm guessing you don't… but bear with me
anyway, because it's a useful thing to know), there's another
technique which you can use called closures. A closure is a function
that captures a reference to a variable in the enclosing scope. So,
another way that we can do this is what to define the transition
function until we're inside the JSON callback. This way, we avoid
creating a transitionSOG function, and just assign an event handler on
the button when the data loads:
d3.json("boxscore.json", function(json) {
// initialize the x-scale
var w = d3.scale.linear()
.domain([0, d3.max(data.homeskaters, function(d) { return d.SHF; })])
.range([0, 250]);
… then display your initial chart and stuff …
d3.select("#SOGButton").on("click", function() {
// change the domain of `w` to use SOG rather than SHF
w.domain([0, d3.max(json.homeskaters, function(d) { return d.SOG; })]);
d3.selectAll(".chart rect").transition()
.duration(750)
.attr("width", function(d) { return w(d.SOG); });
});
});
So, in this case I didn't have to make `json` or `w` global, because
I'm only accessing them within the JSON callback function. That's the
principle of closures. On the other hand, making things global is nice
because then you can debug things more easily from the developer
console.
Cheers,
Mike
You want to select DOM elements, not data. I would start by grouping
the elements for a given skater together into an svg:g element.
Currently, you're appending each separately, so it looks like this:
<rect y="0" …
<rect y="20" …
<rect y="40" …
<rect y="60" …
<text class="SHFCount" y="0" …
<text class="SHFCount" y="20" …
<text class="SHFCount" y="40" …
<text class="SHFCount" y="60" …
<text class="playerName" y="0" …
<text class="playerName" y="20" …
<text class="playerName" y="40" …
<text class="playerName" y="60" …
Instead, it's easier if you have something more structured, like this:
<g transform="translate(0,0)">
<rect …
<text class="SHFCount" …
<text class="playerName" …
</g>
<g transform="translate(0,20)">
<rect …
<text class="SHFCount" …
<text class="playerName" …
</g>
…
You can do that like so:
var g = chart.selectAll("g.homeskater")
.data(data.homeskaters)
.enter().append("svg:g")
.attr("class", "homeskater")
.attr("transform", function(d, i) { return "translate(0," + i *
rowHeight + ")"; });
g.append("svg:rect")
.attr("width", function(d) { return w(d.SHF); })
.attr("height", 19);
g.append("svg:text")
.attr("x", function(d) { return w(d.SHF); })
.attr("y", rowHeight) // note: relative to containing svg:g
.attr("text-anchor", "end")
.attr("dy", "-.35em")
.attr("class", "SHFCount")
.text(function(d) { return d.SHF; });
g.append("svg:text")
.attr("class", "playerName")
.attr("x", 270)
.attr("y", rowHeight - 5)
.text(function (d) { return d.name; });
And then, when you want to resort them, you select the g elements again:
d3.selectAll("g.homeskater")
.data(data.homeskaters.sort(function(a, b) { return b.SHF -
a.SHF; }), function(d) { return d.name; })
.transition()
.delay(function(d, i) { return i * 50; })
.attr("transform", function(d, i) { return "translate(0," + i *
rowHeight + ")"; });
In fact, you can do this now by selecting "rect" rather than
"g.homeskater", but using the grouping will make it easier for you to
move the bars and labels simultaneously. Try pasting this into your
console to see:
d3.selectAll("rect")
.data(data.homeskaters.sort(function(a, b) { return b.SHF -
a.SHF; }), function(d) { return d.name; })
.transition()
.delay(function(d, i) { return i * 50; })
.attr("y", function(d, i) { return i * rowHeight; })
Mike