Simple Json example?

12,355 views
Skip to first unread message

sspboyd

unread,
May 12, 2011, 6:16:09 PM5/12/11
to d3-js
Hi,
I'm still trying to wrap my head around how D3 works and I'm not
getting too far. My Processing experience isn't helping much. I've
worked through some of the examples on Jan Willem Tulp's blog and the
"A Bar Chart" tutorial on the site.

Currently I'm trying to figure out how to modify the "A Bar Chart,
Part 1" example to use a json feed of some hockey stats. I'm Canadian
and the playoffs are on so... I'd like to modify the bars to represent
SOG (shots on goal) and Time on ice (TOI) for the home team skaters.
The actual data being used isn't relevant, I'm mainly looking for some
very basic examples that will help me frame an understanding of D3.
The examples on the site are fantastic at showing what can eventually
be done with D3 but it's difficult to parse out the fundamentals (for
me anyway).

So far I've tried things like adding d3.json("fileName.json") and then
attempting to figure out how to reference that data. I'm guessing I'll
need to add a callback function and then shuffle around methods to
work within the callback function?? The Bar Chart example has the data
declared as a var which is apart from several other scale methods. I'm
struggling to understand the appropriate location and flow of methods
and data.

Any simple examples of D3 functionality people have are much
appreciated.

If anyone's interested in the data I'm trying to work with:
http://www.cbc.ca/data/statsfeed/sports/hockey/boxscore/boxscore_2011051005.json

Thanks,
Stephen

Mike Bostock

unread,
May 12, 2011, 6:29:19 PM5/12/11
to d3...@googlegroups.com
> So far I've tried things like adding d3.json("fileName.json") and then
> attempting to figure out how to reference that data.

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

[1] http://www.w3.org/TR/cors/

sspboyd

unread,
May 14, 2011, 3:33:52 AM5/14/11
to d3-js
Thanks for the reply Mike,
I'm looking at the calendar example you have on the site. I'm trying
to figure out what I need to repurpose and how to do it but I don't
have a clear idea of what things like nest() or key() or rollup() or
map() actually do aside from inference.

My attempts so far are amounting to stabs in the dark at the moment.
For example:

var chart = d3.select("body")
.append("div")
.attr("class", "chart");

// read in json data - not sure if I need .entries()???
d3.json("boxscore_2011032825.json", function(json) {
chart.data(d3.entries(json)).selectAll("div")
.enter().append("div")
.text(function(d) { return d.data.key;}); // using 'd' instead of
'json' make any diff?
});


It's throwing an error saying enter isn't a method which clearly
indicates I'm using it wrong. I don't know what entries does but I've
seen it used elsewhere. I'm guessing it lets me iterate through the
different nodes in the json??

I'm clinging to the hope that if I keep trying different things
something will eventually work!
> >http://www.cbc.ca/data/statsfeed/sports/hockey/boxscore/boxscore_2011...

Mike Bostock

unread,
May 14, 2011, 2:07:36 PM5/14/11
to d3...@googlegroups.com
> I'm looking at the calendar example you have on the site. I'm trying
> to figure out what I need to repurpose and how to do it but I don't
> have a clear idea of what things like nest() or key() or rollup() or
> map() actually do aside from inference.

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

sspboyd

unread,
May 19, 2011, 4:30:30 PM5/19/11
to d3...@googlegroups.com
Hi Mike, 
I've gotten a bit farther in playing with the json data. The current state can be seen here:

I've put my questions and stumbling blocks into the code as comments. They're also listed out below:
1) I'm not sure how to reference the shift count (SHF) data in the homeskaters' array. I want to pass that data to the d3.max method in order to scale the length of the bar correctly. I'm hard coding it now.
2) How to figure out the number of items in homeskaters node. I've tried using length, but again, not quite working.
3) There seems to be an issue with using the dx attribute for svg:text in FF4.
4) I haven't figured out how to avoid hard coding the data being referenced (json.homeskaters.SHF). What if I want to add the ability to switch to showing SOG?

Any pointers are much appreciated.

Thanks,
Stephen 

var rowHeight = 20;

var chart = d3.select("body")
.append("svg:svg")
.attr("class", "chart")
.attr("width", 600)
.attr("height", 400)
.append("svg:g")
.attr("transform", "translate(10,15)");


// read in the json feed
d3.json("boxscore_2011032825.json", function(json){ 

/* 

// I am not sure how to reference the Shift count (SHF) in the json data. A few unsuccessful attempts listed below
var w = d3.scale.linear()
.domain([0, d3.max(d.homeskaters.SHF)]) // this doesn't work
.range([0,250])

var w = d3.scale.linear()
.domain([0, d3.max(function(d) { return d.homeskaters.SHF })]) // This doesn't work either

.range([0,250])
*/

// hard coding works :(
var w = d3.scale.linear()
.domain([0, 27])
.range([0,250])

chart.selectAll("line")
.data(w.ticks(5))
.enter().append("svg:line")
.attr("x1", w)
.attr("x2", w)
.attr("y1", 0)
// .attr("y2", json.homeskaters.SHF.length) // doesn't work...
.attr("y2", 400) // ...so more hard coding it is for now
.attr("stroke", "#ccc");

chart.selectAll("text.rule")
.data(w.ticks(5))
.enter().append("svg:text")
.attr("class", "ruleVal")
.attr("x", w)
.attr("y", 0)
.attr("dy", -3)
.attr("text-anchor", "middle")
.text(String);

chart.selectAll("rect")
.data(json.homeskaters)
.enter().append("svg:rect")
.attr("x", 0 )
.attr("y", function(d,i){ return i * rowHeight; })
// .attr("width", function(d) { return d.SHF * 10; })
.attr("width", function(d) { return w(d.SHF); }) // works, but, what if I want to change SHF to SOG or something else?
.attr("height", 19);

chart.selectAll("text.SHFCount")
.data(json.homeskaters)
.enter().append("svg:text")
// .attr("x", function(d) { return d.SHF * 10 - 3; })
.attr("x", function(d) { return w(d.SHF); })
.attr("y", function(d, i){ return i * rowHeight + rowHeight;})
.attr("text-anchor", "end")
.attr("dy", "-.35em") //padding top --- works
// .attr("dx", "-5px") //padding right --- doesnt seem to do anything in FF4
.attr("dx", "-1.5em") //padding right --- doesnt seem to do anything in FF4
.attr("class", "SHFCount")
.text(function(d) { return d.SHF; });
chart.selectAll("text.playerName")
.data(json.homeskaters)
.enter().append("svg:text")
.attr("class", "playerName")
.attr("x", 270)
.attr("y", function(d, i) { return i * rowHeight + rowHeight - 5; })
.attr("font-family", "verdana")
.text(function (d) {return d.name;});

});


Matthew Smillie

unread,
May 20, 2011, 1:12:48 PM5/20/11
to d3-js

> 1) I'm not sure how to reference the shift count (SHF) data in the
> homeskaters' array. I want to pass that data to the d3.max method in order
> to scale the length of the bar correctly. I'm hard coding it now.

The homeskaters array contains player data, and each player has a
shift count. How you access this depends on exactly what you want,

In your first stumbling block, you're looking for the maximum shift
count:

var w = d3.scale.linear()
.domain([0, d3.max(d.homeskaters.SHF)])

There are two basic mistakes here:
1) there is no 'd' defined at this point in your code. The data is in
the json variable, you probably want json.homeskaters
2) json.homeskaters is an array, each element has a SHF property.
json.homeskaters.SHF is undefined. You can fix this by providing a
function to max() so that it can compare the correct property of each:

d3.max(json.homeskaters, function(skater) { return skater.SHF })

You might have guessed from the above, but the length of the
homeskaters array is available via json.homeskaters.length


> 4) I haven't figured out how to avoid hard coding the data being referenced
> (json.homeskaters.SHF). What if I want to add the ability to switch to
> showing SOG?

This is less d3 and more straight-up Javascript. Let's take the
function we used in max() above as an example:

d3.max(json.homeskaters, function(skater) { return skater.SHF })

There's two aspects to doing this:
1) functions can be held by variables. So instead of writing out
"function(skater) { return skater.SHF }" every time you need the SHF
stat for a skater, you can do this:

var shfStat = function(skater) { return skater.SHF }
d3.max(json.homeskaters, shfStat)

2) Properties can be accessed in two ways; the dot-notation above, and
via the [] operator, using a string key. This is actually the only
way to access properties with names that aren't valid Javascript
symbols on their own (e.g., anything with a hyphen, which JS treats as
subtraction). The following version is the same as the above:

var shfStat = function(skater) { return skater["SHF"] }
d3.max(json.homeskaters, shfStat)

You can use this same function anywhere you need to get the SHF stat
from a player, for example:

chart.selectAll("rect")
.data(json.homeskaters)
.enter().append("svg:rect")
.attr("x", 0 )
.attr("y", function(d,i){ return i * rowHeight; })
.attr("width", function(d) { return shfStat(d) * 10; })

It works here because the 'd' passed to the function is a single
element of whatever data you bound using data(). In this case it's
json.homeskaters.

There's more you can do, but this is definitely a "most of the way
there" solution, and going much further is probably a bit out of
scope; I'd recommend digging into Javascript a bit more.

regards,
m.

sspboyd

unread,
May 29, 2011, 10:27:01 PM5/29/11
to d3...@googlegroups.com
Hi Mike,
I said I'd email you my next questions but your address isn't available anywhere so, sorry, but I'm posting a question here that might be more a javascript question than D3. I don't know.

I am still playing with that one json file.

I'm trying to implement the ability to switch the chart between displaying two types of data, SOG and SHF. I'm staring with SOG.

I am assuming what I need to do is:
- select the chart
- select the rects
- transition each rect to a new width based on the specified data point.

Here's my first attempt
// Control switching between SOG and SHF stats display
function transitionSOG(){
var bars = d3.select(".chart");
bars.selectAll("rect")
.transition()
.duration(750)
.attr("width", function(d) { return w(d.SOG); });
}

The problem I've run into is that w isn't inside the scope of the function. The function w is in the scope of the function that grabs the json data and renders out the rects when the page loads.

So I copied it over to the transistionSOG. I know there must be a better way to access or use that w function but whatever, I renamed the function to transW and copied it. I just want this to work first, then I'll worry about making it better.  

My problem now is I don't know how to access the SOG data. I _think_ that the json data file I loaded has been bound to the chart although I don't even know that for sure. If I'm right then how much of the data is bound to the chart? Just SHF? Or the whole thing??. I've tried peaking at the DOM via firebug but can't see the json data embeded in the chart or d3 objects.

Here's what I tried to run and it doesn't work:

// Control switching between SOG and SHF stats display
function transitionSOG(){
var bars = d3.select(".chart");
var transW = d3.scale.linear()
// .domain([ 0, d3.max(d.homeskaters, function(skater){ return skater.SOG; }) ])
.domain([ 0, d3.max(skater, function(skater){ return skater.SOG; }) ])
.range([0,250]);
bars.selectAll("rect")
.transition()
.duration(750)
.attr("width", function(d) { return transW(d.SOG); });
}


I'm stuck and I don't know where to start. I'm not sure if this a result of my lack of knowledge of D3 or javascript. It's probably both.

Based on Matthew Smillie's suggestion I've spent some time reading and watching javascript articles and videos such as the Mozilla docs, Doug Crockford's YUI Theatre videos and read his book The Good Parts. I'm still figuring it out but I have a much better sense of how different it is from Java. 

Here's a link to page with this code:

Thanks,
Stephen

Mike Bostock

unread,
May 29, 2011, 10:45:32 PM5/29/11
to d3...@googlegroups.com
Yep, this is a JavaScript question. Specifically regarding the scope
of variables; if you declare a variable inside a function, that
variable only exists within the scope of the function, and then it
goes away. So the problem is that, when you load the JSON feed, you
need to save the loaded data into a global so that you can access it
later. Here's the simplest way to do that:

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

sspboyd

unread,
May 29, 2011, 11:12:17 PM5/29/11
to d3...@googlegroups.com
Thanks again Mike. I'm trying this out now.

sspboyd

unread,
May 30, 2011, 5:33:55 PM5/30/11
to d3...@googlegroups.com
I've got the non-closure version of your code working and switching between SOG and SHF data. Great to have something that works!
http://sspboyd.ca/d3/d3.learning.animate.SHF-SOG.working.html

What I'd like to do now is sort it. To start, I've tried to get an array just the SHF data to run the sort on. I'm finding I can only grab the homeskaters data but not the SHF.

>>> d3.selectAll(data.homeskaters);
>>> d3.selectAll(data.homeskaters.SHF);
TypeError: can't convert undefined to object
d3_arraySlice(undefined="(void 0)")d3.js (line 19)
(undefined="(void 0)")d3.js (line 1067)

Should I be just iterating through the homeskaters array somehow and feeding that to the sort function??

Mike Bostock

unread,
May 30, 2011, 5:52:03 PM5/30/11
to d3...@googlegroups.com
> d3.selectAll(data.homeskaters);

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

Reply all
Reply to author
Forward
0 new messages