Protovis: analysis of the Becker’s barley example sketch

We are taking a look at the Protovis Becker’s Barley example.

<html>
  <head>
    <title>Barley Yields</title>
    <link type="text/css" rel="stylesheet" href="ex.css?3.2"/>
    <script type="text/javascript" src="../protovis-r3.2.js"></script>
    <script type="text/javascript" src="barley.js"></script>
    <style type="text/css">

#fig {
  width: 350px;
  height: 833px;
}

    </style>
  </head>
  <body><div id="center"><div id="fig">
    <script type="text/javascript+protovis">

/* Compute yield medians by site and by variety. */
function median(data) pv.median(data, function(d) d.yield);
var site = pv.nest(barley).key(function(d) d.site).rollup(median);
var variety = pv.nest(barley).key(function(d) d.variety).rollup(median);

/* Nest yields data by site then year. */
barley = pv.nest(barley)
    .key(function(d) d.site)
    .sortKeys(function(a, b) site[b] - site[a])
    .key(function(d) d.year)
    .sortValues(function(a, b) variety[b.variety] - variety[a.variety])
    .entries();

/* Sizing and scales. */
var w = 242,
    h = 132,
    x = pv.Scale.linear(10, 70).range(0, w),
    c = pv.Colors.category10();

/* The root panel. */
var vis = new pv.Panel()
    .width(w)
    .height(h * pv.keys(site).length)
    .top(15)
    .left(90)
    .right(20)
    .bottom(25);

/* A panel per site-year. */
var cell = vis.add(pv.Panel)
    .data(barley)
    .height(h)
    .top(function() this.index * h)
    .strokeStyle("#999");

/* Title bar. */
cell.add(pv.Bar)
    .height(14)
    .fillStyle("bisque")
  .anchor("center").add(pv.Label)
    .text(function(site) site.key);

/* A dot showing the yield. */
var dot = cell.add(pv.Panel)
    .data(function(site) site.values)
    .top(23)
  .add(pv.Dot)
    .data(function(year) year.values)
    .left(function(d) x(d.yield))
    .top(function() this.index * 11)
    .size(12)
    .lineWidth(2)
    .strokeStyle(function(d) c(d.year));

/* A label showing the variety. */
dot.anchor("left").add(pv.Label)
    .visible(function() !this.parent.index)
    .left(-1)
    .text(function(d) d.variety);

/* X-ticks. */
vis.add(pv.Rule)
    .data(x.ticks(7))
    .left(x)
    .bottom(-5)
    .height(5)
    .strokeStyle("#999")
  .anchor("bottom").add(pv.Label);

/* A legend showing the year. */
vis.add(pv.Dot)
    .extend(dot)
    .data([{year:1931}, {year:1932}])
    .left(function(d) 170 + this.index * 40)
    .top(-8)
  .anchor("right").add(pv.Label)
    .text(function(d) d.year);

vis.render();

    </script>
  </div></div></body>
</html>

Data:

var barley = [
  { yield: 27.00000, variety: "Manchuria", year: 1931, site: "University Farm" },
  { yield: 48.86667, variety: "Manchuria", year: 1931, site: "Waseca" },
  { yield: 27.43334, variety: "Manchuria", year: 1931, site: "Morris" },
etc.

The code begins by defining a function that will be used throughout the program.

function median(data) pv.median(data, function(d) d.yield);

this function expects an array of associative arrays which have the key “yield”. What it does is that it returns the median of all the values of the key yield.
If run against barley, it will return the median yield of all observations. If run against a subset of barley, it will return the median of the yield of that subset.
The point of this function is to simplify the writing of otherwise obfuscated statements.

Now let’s see it put to good use.

var site = pv.nest(barley).key(function(d) d.site).rollup(median);

The output of this statement is easier to understand than itself. It returns an associative array, with each site as a key, and their corresponding value is the median yield for all observations for that site.

{
  Crookston: 39.03333,
  Duluth: 28.533335,
  Grand Rapids: 23.983335,
  Morris: 34.699995,
  University Farm: 31.383335,
  Waseca: 47.949995}

pv.nest(barley) means that we are going to create an associative array based on barley with a hierarchy. (tree)
.key(function(d) d.site) means that they first key in the hierarchy of that tree will be the site. So, we are going to run an operation on all the entries of “barley” with the same site.
That operation is called by .rollup(median ) at the end of the line: this crunches all the entries and replace them by the result of the function “median” applied to all those records.

var variety = pv.nest(barley).key(function(d) d.variety).rollup(median);

This does the same as above, but by variety instead of by site.

{
  Glabron: 32.4,
  Manchuria: 30.96667,
  No. 457: 33.966665,
  No. 462: 30.45,
  No. 475: 31.066665,
  Peatland: 32.383335,
  Svansota: 28.550005,
  Trebi: 39.199995,
  Velvet: 32.149995000000004,
  Wisconsin No. 38: 36.95
}
/* Nest yields data by site then year. */
  barley = pv.nest(barley)
    .key(function(d) d.site)
    .sortKeys(function(a, b) site[b] - site[a])
    .key(function(d) d.year)
    .sortValues(function(a, b) variety[b.variety] - variety[a.variety])
    .entries();

This will transform the variable barley, which is now a flat list of records, into a tree form.
pv.nest(barley) indicates we are turning barley into a tree.
.key(function(d) d.site) says that the first order of the hierarchy will be “site”.
So our tree should look like:

[{key:”Crookston”, values: {….},
  {key:”Duluth”, values:{…},
…

Well, the order of the keys may not be alphabetical, thanks to the next statement.
sortKeys sorts the keys using a comparator function: this function(a,b) thing goes through all the pairs of keys and will order them according to that function, so if for a key the value of site is higher than for another one, it will be put first.
As a result, the first key should be Waseca, then Crookston, etc.
The next order of hierarchy will be after year. No sortkeys here, so the values of keys will just be presented in natural order.
So our tree will look like:

[
{ key: “Waseca”, values: [
	{key: 1931, values: [ … ]},
	{key: 1932, values: [ … ]}
  ]
}, 
{key: “Crookston”, values: [
	{key: 1931, values: [ … ]},
	{key: 1932, values: [ … ]}
  ]
}, etc.

The next statement will rank the values.
What is in that values field (where the ellipses are) will now be ranked using another comparator function, on variety this time.
The sortKeys statement worked on keys: “Crookston”, “Duluth”, etc. so one could write directly site[a] and get a value.
But this sortValues statement works on entries (fields like: { yield: 48.86667, variety: “Manchuria”, year: 1931, site: “Waseca” } ), so one can’t write variety[a] but instead, variety[a.variety].

Finally, the last statement, entries(), says that the values thing should be filled by the actual records, as opposed to using rollup in order to crush an aggregate value from the records.

So the final tree will look like this:

[{key: “Waseca”, values: [
 {key: 1931, values: 	[{site: "Waseca", variety: "Trebi", year: 1931, yield: 63.8333},
 {site: "Waseca",variety: "Wisconsin No. 38", year: 1931, yield: 58.8},
 {site: "Waseca", variety: "No. 457", year: 1931, yield: 58.1},
…
]},
{key: 1932, values: 	[…]}
]}, 
{key: “Crookston”, values: […]}, 
…
{key: “Grand Rapids”, values: […]}]

The next block of statements is more straightforward.

/* Sizing and scales. */
var w = 242,
    h = 132,
    x = pv.Scale.linear(10, 70).range(0, w),
    c = pv.Colors.category10();

w and h are constants for the width and heights of the cells,
x is a scale to transform the yields in horizontal coordinates, so 10 will be represented at the left-most sied of the cell and 70 at the right-most side,
c is the standard color palette.

Now, we create the panels.

/* The root panel. */
var vis = new pv.Panel()
    .width(w)
    .height(h * pv.keys(site).length)
    .top(15)
    .left(90)
    .right(20)
    .bottom(25);

/* A panel per site-year. */
var cell = vis.add(pv.Panel)
    .data(barley)
    .height(h)
    .top(function() this.index * h)
    .strokeStyle("#999");

First we create the root panel. The height of the panel is determined by the number of sites.
Since site is an associative array, we first derive an array of the same length with pv.keys(site), which contains the names of the keys of that associative array (in plain English, the names of the sites). What we need is just the number of them, that’s what the length property is for.
(btw, we could have simply written barley.length)
We multiply that number by the height of each cell to get the height of the root panel.

We then create panels inside that rootpanel.
Note the simplicity with which data is pushed into those panels:

    .data(barley)

Now barley is an array of 6 objects. So we are creating 6 panels, and their data element will be of the form:

{key: “Waseca”, values: [{key: “1931”, values:  [entries]}, {key:”1932”, values: [entries]}]}

Since these cells will be positioned from the root panel, their top value is determined using a simple function of this.index, so the 1st one will be right on top, the next one will be at h pixels from the top, the next one at 2*h pixels, etc.

/* Title bar. */
cell.add(pv.Bar)
    .height(14)
    .fillStyle("bisque")
  .anchor("center").add(pv.Label)
    .text(function(site) site.key);

This just adds a title to the cells. Here, we use a bar, but it could have been a panel.
We only specify the height: the top and left value are supposed to be 0 and the width is that of the parent. We choose the svg color “bisque” as the background color and we add a label in the middle.
Here’s how we obtain the text. Yes it is the site name, but this has nothing to do with the choice of “site” as the variable name in the accessor function.
That function just takes data from the data property, which is directly inherited from the parent. This is a complex associative array, but at its first level, the key property corresponds to the site name. So, function(site) site.key returns the site name.

/* A dot showing the yield. */
var dot = cell.add(pv.Panel)
    .data(function(site) site.values)
    .top(23)
  .add(pv.Dot)
    .data(function(year) year.values)
    .left(function(d) x(d.yield))
    .top(function() this.index * 11)
    .size(12)
    .lineWidth(2)
    .strokeStyle(function(d) c(d.year));

Here’s the data representation.
We first add a group of panels called dot to the panel cell.
This is a group, and not a single panel, because of what they get through the data method: the content of the values key of their parent.
Again, the data property of their parent, cell, is of the form:

{key: site name, values: [{key: “1931”, values: […]}, {key:”1932”, values: […]}]}

So if we isolate the content of the values key, we have an array of 2 values:

[{key: “1931”, values:[…]} , 
{key: “1932”, values:[…]} ]

Therefore, this statement creates 2, not 1, panels.
But those panels are superposed: the only positioning instruction is that they are 23 pixels from the top, so they are both taking the full width of the cell panel, and go to the bottom of that panel as well.
The system will first draw the first one, then the second one on top of that.

Then, we add a dot object to those dot panels, which is a series of circles.
How many dots will there be in those series? This, again, depends on the contents of the data method.
What we get this time is:

    .data(function(year) year.values)

What this means is that we are looking at the data element of the parent, and we are taking what’s behind values.

The data element of the parent was of the form:

{key: year name, values: 
  [
   {site: site name, variety: variety name, year: year name, yield: yield value},
   {site: site name, variety: variety name, year: year name, yield: yield value}, 
   {site: site name, variety: variety name, year: year name, yield: yield value},
…
   {site: site name, variety: variety name, year: year name, yield: yield value}
  ]
}

So what we’re getting is the array of 10 entries that were behind values.

.left(function(d) x(d.yield))

This means that each mark will be positioned from the left, according to the value of its yield property, once transformed by our scale.

.top(function() this.index * 11)

And they are simply positioned vertically depending on this.index, so one on top of the other. Remember, this is relative to the dot panel, not to cell.

.size(12)
.lineWidth(2)

We are determining a size and a line width, but no fill style, so we are drawing rings, not discs. Even if they are superposed, the back one should still be visible.

.strokeStyle(function(d) c(d.year));

Finally, we are determining the color of the ring. For that, we extract the year of the current entry using d.year. It is converted to a color using the palette c.
There are other ways to get the year, but this is the simplest to write.

Now if we consider the statement in its entirety again:

/* A dot showing the yield. */
var dot = cell.add(pv.Panel)
    .data(function(site) site.values)
    .top(23)
  .add(pv.Dot)
    .data(function(year) year.values)
    .left(function(d) x(d.yield))
    .top(function() this.index * 11)
    .size(12)
    .lineWidth(2)
    .strokeStyle(function(d) c(d.year));

What is really assigned to dot is not the first item created (panels) but the dot chart proper. This is important to note that for the rest:

/* A label showing the variety. */
dot.anchor("left").add(pv.Label)
    .visible(function() !this.parent.index)
    .left(-1)
    .text(function(d) d.variety);

What we’re trying to do here is to write legends for the dot charts. We don’t want to do this for each year: the labels would be on top of each other, and less legible than if they were just one group of labels. This is why we use this visible statement. this.parent refers to a the parent panel of dot, so this.parent.index is worth 0 for the first panel, and 1 for the second. So here, !this.parent.index will be false for all values other than 0, so there will be only 1 set of labels, even if we add a year for instance.

Then, although we said we were adding the labels just to the left of the dot (this is what dot.anchor(“left”) does), what we really want is to have them on the left of the cell.
That’s why we use

.left(-1)
.text(function(d) d.variety)

runs on the data provided by the parent, in this case the dot chart. So it’s just the variety of the current entry.

/* X-ticks. */
vis.add(pv.Rule)
    .data(x.ticks(7))
    .left(x)
    .bottom(-5)
    .height(5)
    .strokeStyle("#999")
  .anchor("bottom").add(pv.Label);

This just adds a series of 7 ticks to the bottom of the chart. They are rules, but their height has been limited to 5 pixels. They start from below the chart – bottom(-5) – and we add the ticks to that below with anchor(“bottom”). Since all the values are 2-digit numbers, we can leave the default versions for the pv.Label objects, and not go into tickFormat or anything fancy.

/* A legend showing the year. */
vis.add(pv.Dot)
    .extend(dot)
    .data([{year:1931}, {year:1932}])
    .left(function(d) 170 + this.index * 40)
    .top(-8)
  .anchor("right").add(pv.Label)
    .text(function(d) d.year);

Finally, the legend of the chart, in form of a dot chart.
The first .extend(dot) statement just says that we are copying the properties of the other dot chart. So, if we decide to change the size of the rings or their thickness, it will be reflected in the legend.
Here, we just pass the data manually. Note that this is added to the root panel (vis), so top(-8) means that it appears over the main chart.

In this example, the authors have started from an unformated, flat list of 120 entries.
Then, they thought of the shape their final visualization will take and the hierarchy of objects needed to accommodate that:
• two superposed dot series, showing the various varieties for a given year/site combination (dot),
• each inside a panel, one for each year,
• which would be part of a parent panel, for each site (cell)
• which would be stacked vertically inside the root panel (vis).

So in order to do that seamlessly, they had to prepare a data variable with that hierarchy:
First by site, then by year, then by variety.

This is what the complex pv.nest command does at the beginning.
Once this is taken care of, the various objects can be added one to the other naturally.

Working with data in protovis – interlude: protovis nesting vs tableau

Protovis, like Tableau, are based on the grammar of graphics framework. In a nutshell, in both environments, a chart designer can map visual attributes (such as x or y dimension, color, shape, etc.) to dimensions of data.

The flat file which Becker’s Barley is based on can be used in Tableau public nearly as is.
Here’s a size-by-size comparison of the results:

How it’s done in Protovis

In protovis, the flat file is nested several times, so that its various elements can be called in a hierarchy from series of dots per variety, to panel per sites to a grander panel. Legend and ticks are added by hand for a perfect finish. Still, some careful planning is required to prepare the data file and to adjust the various elements (choice of colors, sizes, etc.)

How it’s done in Tableau


The data file, unsurprisingly, has 4 dimensions: site, variety, year and yield. In tableau terms, yield is a measure (a numerical dimension) while the other 3 are categorical dimensions. With a few clicks, it is possible to get a result which is similar to the original vis in Protovis. We assign yield to column (horizontal attribute), site and variety to row in that order. We also untick aggregate measures in Analysis, so we get little circles and not big bars. Here, I’ve manually sorted the sites and the varieties.

Conclusion

It is much, much easier to achieve a similar result with Tableau, however using protovis provides a finer control.

Working with data in protovis – part 3 of 5

Previous : Multi-dimensional arrays, inheritance and hierarchy

Short interlude: what can be done with arrays in javascript?

Now that we have a grasp on how arrays work and how they can be used in protovis, let’s take a step back and look at some very useful methods for working with arrays in standard javascript.

Sorting arrays

Using a method called, well, sort(), javascript arrays can be reordered in a variety of ways.

var a = [1, 1.2, 1.7, 1.5, .7, .3];
a.sort();

Without any argument, this will sort the array in ascending order: [0.3, 0.7, 1, 1.2, 1.5, 1.7].

However, we can reorder the array differently with a comparison function, such as:

a.sort(function(a,b) b-a)

Note that this is protovis notation. In traditional javascript, you’d have written a.sort(function(a,b) {return b-a;});

This would sort a in descending order ([1.7, 1.5, 1.2, 1, 0.7, 0.3]). Here is how it works.

The comparison function takes 2 elements in the array. If the first element (corresponding to the first argument, a) should appear first, the function must return a negative number. If the result is positive, the second element is appearing first. With our example, b-a is negative when the first element is greater than the second. So, this function sorts an array in descending order.

Sort also works with arrays of associative arrays. For instance:

var data = [
  {key:"a", value:1},
  {key:"b", value:1.2},
  {key:"c", value:1.7}, 
  {key:"d", value:1.5},
  {key:"e", value:.7},
  {key:"f", value:.3}
];

data.sort(function(a,b) b.value-a.value);

Here, we have to specify which criterion is used to sort the array. In this example, we sort it in descending order by value. But we could have sorted it in ascending order by key, for instance.

The method reverse() turns an array upside down.

var a = [1, 1.2, 1.7, 1.5, .7, .3];

a.reverse(); // gives [0.3, 0.7, 1.5, 1.7, 1.2, 1]

So, a.sort().reverse() will also sort that array in decreasing order.

Extracting sub-arrays

The method slice() will extract a sub-array out of an array, in other words, it will retrieve certain elements. It works with one or two arguments. If you only give one argument, it will give you the last elements from the index you specified.

For instance,

[1, 1.2, 1.7, 1.5, .7, .3].slice(3) // it's [1.5, 0.7, 0.3]

The index 3 corresponds to the 4th element of the array (0,1,2,3) so slice(3) will return the 4th, 5th and 6th elements.

With 2 arguments, you get a sub-array that starts at the first index but ends just before the 2nd one. For instance,

[1, 1.2, 1.7, 1.5, .7, .3].slice(0,3) // gives [1, 1.2, 1.7]

It is also possible to use negative arguments. Instead of counting from the beginning of the array, it means counting from the end.

[1, 1.2, 1.7, 1.5, .7, .3].slice(-1) // result: [0.3]

Turning a string into an array

This can be done via the split() method.

split() normally works with a separator, for instance “07/04/1975″.split(“/”) is ["07", "04", "1975"], but if an empty string is used, then each character becomes an element of the new array. For instance,

"ABCDEFGHIJ".split("") // result: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] 

This can be very useful to generate data on the go.

And what methods does protovis has for working with arrays and data?

Many.

Some simple, some very elaborate. In addition to visualization functions protovis has impressive data processing capabilities.

The workhorse: pv.range()

pv.range() is a method that creates an array of numbers.

pv.range(10), for instance, is [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], or the 10 first integers starting from 0.
(this is the method I referred to in the example of the previous part).

By using the other arguments, it is possible to generate arrays of numbers that go from one specific index to another, or to change the step. For instance:

pv.range(5,10) // is [5, 6, 7, 8, 9]
pv.range(5,21,5) // is [5, 10, 15, 20]

While this may not look incredible at first sight, pv.range() really shines when associated with other functions or methods such as map().

map()  is an array method that creates a new array by applying a function to each of the element of the array it’s run against. For instance,

var data = pv.range(100).map(Math.random) // creates an array of 100 random numbers.

var data = pv.range(1000).map(function(a) Math.cos(a/1000)) // stores into data 1000 values 
// of cos(x) for all the numbers between 0 and 0.999 by increment of 0.001.

In other words, pv.range() and map() can quickly create very interesting datasets for protovis to visualize.

Simple statistical functions that make life easier

pv.min(), pv.max()

Protovis has functions that extract the maximum or the minimum value of an array.

pv.min([1, 1.2, 1.7, 1.5, .7, .3]) will return the value of the smallest element of the array, in that case 0.3.

Likewise, pv.max returns the value of the greatest element of the array.

Both of these can be used with an accessor function to help retrieve what will be compared.

For instance:

var data = [
  {key:"a", value:1},
  {key:"b",value:1.2},
  {key:"c", value:1.7}, 
  {key:"d", value:1.5},
  {key:"e", value:.7},
  {key:"f", value:.3}
];

pv.max(data, function(a) a.value); // should return 1.7

pv.sum(), pv.mean(), pv.median()

Likewise, protovis has aptly-named methods that return the sum, the mean and the median of the elements of an array. Note that it’s pv.mean() and not pv.average(), though.

pv.sum([1, 1.2, 1.7, 1.5, .7, .3]) // gives  6.4.
pv.mean([1, 1.2, 1.7, 1.5, .7, .3]) // gives 1.0666666666666667
pv.median([1, 1.2, 1.7, 1.5, .7, .3]) // gives 1.1.

Similarly to pv.min() you can use an optional accessor function.

pv.median(data, function(a) a.value);

pv.normalize()

pv.normalize() is a handy method that divides all elements in an array by a factor, so that the sum of all these elements is 1.

pv.normalize([1, 1.2, 1.7, 1.5, .7, .3])
// [0.15625, 0.18749999999999997, 0.265625, 0.234375, 0.10937499999999999, 0.04687499999999999]

pv.sum(pv.normalize([1, 1.2, 1.7, 1.5, .7, .3])) // result: 1.

Combining arrays

pv.blend()

pv.blend() simply turns an array of arrays into a simpler array where all elements follow each other.

pv.blend([["a", "b", "c"], [1,2,3]]) // gives  ["a", "b", "c", 1, 2, 3]

One benefit is that it is possible to run methods on this new array that wouldn’t work on an array of arrays.

For instance:

pv.max([[1,2],[3,4],[5,6]]) // result : NaN
// protovis cannot guess how to compare [1,2], [3,4] or [5,6] without any further instruction.
pv.max(pv.blend([[1,2],[3,4],[5,6]])) // result: 6.
pv.max([[1,2],[3,4],[5,6]], function(a) pv.max(a)) // also returns 6.

pv.cross()

Given two arrays a and b, pv.cross(a,b) returns an array of all possible pairs of elements.

pv.cross(["a", "b", "c"], [1,2,3]) // returns 
//[[“a”,1],[“a”,2],[“a”,3],[“b”,1],[“b”,2],[“b”,3],[“c”,1],[“c”,2],[“c”,3]]

pv.transpose()

pv.transpose() flips an array of arrays.

In our previous example –

pv.cross(["a", "b", "c"], [1,2,3])

The result was a 9×2 array. [[“a”,1],[“a”,2],[“a”,3],[“b”,1],[“b”,2],[“b”,3],[“c”,1],[“c”,2],[“c”,3]]

If we apply pv.transpose, we get a 2×9 array:

pv.transpose(pv.cross(["a", "b", "c"], [1,2,3])); // result:
//[["a","a","a","b","b","b","c","c","c"]
//[1,2,3,1,2,3,1,2,3]]

Putting it all together

This short example will show how one can work from an unprepared dataset.
For the purpose of this example, I’m retrieving GDP per capita data of OECD countries. I have a table of 34 countries on 10 years from 2000 to 2009. The countries are in alphabetical order.
I would like to do a bar chart showing the top 12 countries ranked by their GDP per capita. How complex can this be? With the array methods, not so difficult.

var data=[
["Country",2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009],
["Australia",28045.6566279103, 29241.289462815, 30444.7839453338, 32079.5610505727, 33494.6654789274, 35091.7414600449, 37098.3968669994, 39002.0335375021, 39148.4759391917, 39660.1953738791],
["Austria",28769.5911274323, 28799.1968990511, 30231.0955219759, 31080.6122212092, 32598.0698959222, 33409.3453751649, 36268.9604225683, 37801.7320979073, 39848.9481928628, 38822.6960085478],
["Belgium",27624.4454637489, 28488.7963954409, 30013.7570016445, 30241.724380444, 31151.6315179167, 32140.9239149283, 34159.4723388042, 35597.2980660662, 36878.9710906881, 36308.4784216699],
["Canada",28484.9776553362, 29331.8223245112, 29911.3226371905, 31268.5521058006, 32845.6267954986, 35105.9892865177, 36853.5554314586, 38353.0812084648, 38883.0700787846, 37807.5259929247],
["Chile",9293.72322249492, 9712.95685376592, 9973.25946014534, 10475.6486664294, 11299.9329380664, 12194.1443507847, 13036.1285137662, 13896.6946083854, 14577.5715215544, 14346.4648487517],
["Czech Republic",14992.3582327884, 16173.6387263985, 16872.3146139754, 17991.9331910401, 19303.9208098529, 20365.7571142986, 22349.7033747705, 24578.7151794413, 25845.0252652635, 25530.2001372786],
["Denmark",28822.3343389384, 29437.5232757263, 30756.3323835463, 30427.5149228283, 32301.3394496912, 33195.8834383121, 36025.8628586246, 37730.736916861, 39494.1216134705, 37688.2516868117],
["Estonia",9861.80346448344, 10693.0362387373, 11966.6746413277, 13370.127724177, 14758.4181995435, 16530.7311083966, 19134.4130457855, 21262.1415030861, 21802.2185996096, 19880.2786070771],
["Finland",25651.0005450286, 26518.293212524, 27509.1023963486, 27592.2031438268, 29855.387224753, 30689.9753400459, 33095.030855128, 36149.32963148, 37795.237097856, 35236.9462175119],
["France",25272.4127936247, 26644.8404266553, 27776.4225007831, 27399.5459524877, 28273.6330332193, 29692.129719144, 31551.3458452695, 33300.5367942194, 34233.0770304046, 33697.9698933558],
["Germany",25949.0588445755, 26855.1296905696, 27587.1573355486, 28566.8930562768, 29900.6507502683, 31365.576121107, 33713.1689185469, 35623.4147543745, 37170.693412872, 36339.9009091421],
["Greece",18410.2161113813, 19928.6672542964, 21597.5997816753, 22701.7754699285, 24088.2202877303, 24571.6085727425, 26917.7358531616, 28059.5724529461, 29919.6385697499, 29121.8306842272],
["Hungary",12134.0140204384, 13576.4413471795, 14765.2333755651, 15424.3926636975, 16316.8885518624, 16937.9383138957, 18329.1579647528, 19187.2829138188, 20699.9056965458, 20279.5060192879],
["Iceland",28840.4899286393, 30443.907492292, 31083.7723760522, 30768.0910155965, 33697.6723843766, 35025.1338769129, 35807.880160537, 37178.9762849327, 39029.2396691797, 36789.0728215933],
["Ireland",28695.0931156375, 30524.3712319885, 33052.4517849575, 34525.4148515126, 36511.518217243, 38622.5520655621, 42267.9784043449, 45293.6482834027, 42643.6362934458, 39571.204030073],
["Israel",23496.1573531847, 23455.1048827219, 23534.8524310529, 22270.5507137525, 23650.3373360675, 23390.3809761211, 24960.0508863672, 26583.387317862, 27679.3777353666, 27661.1271269539],
["Italy",25594.3456589799, 27127.4298830414, 26803.97019843, 27137.5365317475, 27416.0961790796, 28144.0379218171, 30224.2469223694, 31897.7434207435, 33270.7464832409, 32407.5033618371],
["Japan",25607.7204586644, 26156.1615068187, 26804.9303541079, 27487.123875677, 29020.8955443678, 30311.5281317716, 31865.2733339475, 33577.1846704038, 33902.3807335349, 32476.7736535378],
["Korea",17197.0800334388, 18150.876787578, 19655.5961579558, 20180.932195274, 21630.1629926488, 22783.2288432845, 24286.1803272315, 26190.6122298674, 26876.6422899926, 27099.9481192024],
["Luxembourg",53645.7229595775, 53932.4594967082, 57559.2104243171, 60723.9861616145, 65021.6940738929, 68372.258619455, 78523.2988291034, 84577.2190776183, 89732.070876649, 84802.9716853677],
["Mexico",10046.1268819126, 10135.7265293228, 10397.8915415223, 10883.8643666506, 11535.0108530091, 12460.5385178945, 13672.6034191998, 14581.5178679935, 15290.896575069, 14336.6153896301],
["Netherlands",29405.5490140191, 30788.2508953647, 31943.4974056501, 31702.5798119046, 33208.8592768987, 35110.6577619843, 38063.6877336725, 40744.3819227526, 42887.4361825772, 40813.047575264],
["New Zealand",21113.9167562199, 22128.8483527737, 23115.0676326844, 23826.8007737809, 24775.7827578032, 25460.2942993256, 27277.7654223562, 28701.3328788203, 29231.151567007, 29097.307061683],
["Norway",36125.7036078917, 37091.7150478501, 37051.9473705873, 38298.8631304819, 42257.5943785044, 47318.7844737929, 53287.9750020125, 55042.2093246253, 60634.9819502298, 55726.7456848212],
["Poland",10567.0016558591, 10950.4739587211, 11562.6245018264, 11985.0042955849, 13014.5996003514, 13785.7656450352, 15067.4402978586, 16762.2045951617, 18062.1834289945, 18928.8205162032],
["Portugal",17748.6930918065, 18464.7472033934, 19088.1764760703, 19392.2862101711, 19796.1891048601, 21294.1660710906, 22869.9272824917, 24122.9886840593, 24962.2959136026, 24980.061318413],
["Slovak Republic",10980.1169029929, 12070.70145551, 12965.6228450353, 13597.9556337584, 14659.0096334623, 16174.4605836956, 18397.5822518716, 20916.5438777989, 23241.0972994031, 22870.8998469728],
["Slovenia",17468.5101911515, 18342.8685005009, 19702.0589885459, 20448.2040741134, 22200.7770986247, 23493.9370882038, 25432.3240768864, 27228.3376725332, 29240.5471362484, 27535.6540786883],
["Spain",21320.0690041202, 22591.3855327033, 24066.5003000439, 24748.2819467008, 25958.1017090553, 27376.7644983649, 30347.9357063518, 32251.8469529105, 33173.3334547015, 32254.0895139806],
["Sweden",27948.4749835611, 28231.3958224236, 29277.7736264795, 30418.0441004337, 32505.646687298, 32701.4327422078, 35680.1787996674, 38339.6698693908, 39321.3014501815, 36996.1394001578],
["Switzerland",31618.0958082618, 32103.4812665805, 33390.9169108411, 33266.3042144918, 34536.8476164193, 35478.0395058126, 39116.3945212067, 42755.5569054368, 45516.5601956426, 44830.3719226918],
["Turkey",9169.71780977686, 8613.21159842998, 8666.90348260238, 8790.01048446305, 10166.0963401732, 11391.3768057053, 12886.6195973915, 13897.3820310325, 14962.4944915831, 14242.7276329837],
["United Kingdom",26071.3066882302, 27578.2857038985, 28887.5850874466, 29848.7319363944, 31790.9933025905, 32724.4057819337, 34970.5193042987, 35719.2060247462, 36817.493156774, 35158.8005888247],
["United States",35050.1738557741, 35866.2624634202, 36754.5543204007, 38127.524970345, 40246.0630591955, 42466.1326203714, 44594.9199470326, 46337.2237397566, 46901.0697730874, 45673.7445647402]
];

var year=10;
var rows = data.slice(1,data.length);

var x = pv.Scale.ordinal(pv.range(12)).splitBanded(0, 240, 4/5);
var y = pv.Scale.linear(0, 80000).range(0, 170);

var vis = new pv.Panel()
    .width(250)
    .height(200)
    ;
	vis.add(pv.Bar)
		.data(rows.sort(function(a,b) b[year]-a[year]).slice(0,12))
		.bottom(10)
		.width(x.range().band)
		.height(function(d) y(d[year]))
		.left(function() 5+x(this.index))
		.anchor("bottom").add(pv.Label).textAngle(Math.PI/2).text(function(d) d[0]).textBaseline("middle").textAlign("right")
		;
vis.render();

here, in the data variable, I’ve put my 2-dimensional table as I got it. The first row contains headers which I won’t need. So, in line 40, I create rows which just removes that 1st line with a slice function. data.slice(1,data.length) effectively keeps all the lines but the first.
In the next few lines I create two scales for placing my bars, a standard vis panel and a bar chart. Now, what kind of data will I pass to the bar chart?
I want to sort the rows by the value of the latest year (which happens to be the 11th column, so 10). This is what the rows.sort(function(a,b) b[year]-a[year]) part of the statement does. I’ve simply assigned 10 to the variable year, so if I want to display other years (with an HTML form for instance) it wouldn’t be difficult to modify.
And, since I only want the top 12 values, I just write .slice(0,12) after that.

In line 55, I just add a label. pv.Label inherits the data of its parent. The data item is the whole row of my 2-dimensional table, so if I write function(d) d[0], I am referring to the left-most item which will be the country name.

So, with the use of simple array functions, I can easily rework an unprepared dataset in protovis, rather than having to tailor my dataset with manual (and error-prone) manipulations in external programs. Here is the result:

Next: reshaping complex arrays

Working with data in protovis – part 2 of 5

Previous post: simple arrays

Multi-dimensional arrays, associative arrays and protovis

Even for a simple chart, chances are that you’ll have more than a single series of numbers to represent. For instance, you could have labels to describe what they mean, several series, and so on and so forth.
So, let’s say we want to add these labels to our original examples, so we know what those bars represent.

var categories = ["a","b","c","d","e","f"];
var vis = new pv.Panel()
  .width(150)
  .height(150);

vis.add(pv.Bar)
  .data([1, 1.2, 1.7, 1.5, .7, .3])
  .width(20)
  .height(function(d) d * 80)
  .bottom(0)
  .left(function() this.index * 25)
  .anchor("bottom").add(pv.Label)
    .text(function() categories[this.index]);

vis.render();

While this did the trick, nothing guarantees that the data proper and the category name will stay coordinated. If one data point is deleted or removed and this is not replicated on the categories, they will no longer match. A more integrated way to proceed would be to group category and data information, like this:

var data = [
  {key:"a", value:1},
  {key:"b", value:1.2},
  {key:"c", value:1.7}, 
  {key:"d", value:1.5},
  {key:"e", value:.7},
  {key:"f", value:.3}
];
var vis = new pv.Panel()
  .width(150)
  .height(150);

vis.add(pv.Bar)
  .data(data)
  .width(20)
  .height(function(d) d.value * 80)
  .bottom(0)
  .left(function() this.index * 25)
  .anchor("bottom").add(pv.Label)
    .text(function(d) d.key);

vis.render();

This time, we group the values and the category names in a single variable, an array of associative arrays.
When drawing the bar chart, protovis will go through this array and retrieve an associative array for each bar.
We have to change the way the height function is written. The data element being accessed is no longer of the form 1 or 1.7, but {key:”a”, value:1} or {key:”c”, value:1.7}. So to get the number part of it, we must write d.value.

Likewise, instead of accessing an array of categories for the text part, we can use the current data element via an accessor function, and write d.key.

Hierarchy and inheritance

So we’ve seen that arrays, or associative arrays, can have several levels and can be nested one into another.
Interestingly, protovis elements, like panels, charts, mark etc. also work in a hierarchy. For instance, when you start working with protovis you create a panel object. Then, you add other objects to that panel, like a bar chart (our example), or another panel. You can add other objects to your new objects, or attach them to your first panel.
This diagram shows the hierarchy between elements in the previous example.

var categories = ["a","b","c","d","e","f"];
var vis = new pv.Panel()
  .width(150)
  .height(150)

var bar = vis.add(pv.Bar)
  .data([1, 1.2, 1.7, 1.5, .7, .3])
  .width(20)
  .height(function(d) d * 80)
  .bottom(0)
  .left(function() this.index * 25)
  .anchor("bottom").add(pv.Label)
  .text(function() categories[this.index]);

vis.render();

The bar object is considered to be the child of vis, who is its parent.

You may know that in protovis, children objects inherit properties of their parents.

For instance, if width wasn’t specified for the bar object, it would have the width of its parent, 150. Each mark would cover the whole screen.

For data, when a new object is added, data is either specified at that level, or obtained from the parent element of this object.

Let’s take our example and tweak it a bit.

var vis = new pv.Panel().width(150).height(150);
var bar = vis.add(pv.Bar)
  .data([1, 1.2, 1.7, 1.5, .7, .3]).width(20) .bottom(0)
  .height(function(d) d * 80).left(function() this.index * 25)
  .anchor("top").add(pv.Label)
vis.render();

Here, I didn’t specify a data or a text value for the labels I added. They just took the value of its parent element – the marks of the pv.Bar object.
Here’s another variation:

var vis = new pv.Panel().width(150).height(150);
vis.add(pv.Panel)
  .data([1, 1.2, 1.7, 1.5, .7, .3]) .left(function() this.index * 25)
  .add(pv.Bar).width(20) .bottom(0)
  .height(function(d) d * 80)
  .anchor("top").add(pv.Label)
vis.render();

Here, I’m adding panels, then a bar in each panel.
From the root panel, I’m adding a group of panel with this data: [1, 1.2, 1.7, 1.5, .7, .3].
Since there are 6 elements here, I’m adding 6 panels.
Here, the left method applies to each of the panel. The first one is to the left, the next one is 25 pixels further, etc.
I’m then adding a bar object to each panel. Is that one group of bar? Technically yes, but each has only one element! Each pv.Bar implicitly gets the data element of its parent, so the first bar gets [1], the next one gets [1.2], etc. The height of each bar is determined by multiplying the value of that element by 80.
Note that since the fillStyle properties are not defined for the bars, they get the ones which are attributed by default, which explains the color changes.

Further refinement: accessing the data of its parent!

var vis = new pv.Panel().width(150).height(150);
vis.add(pv.Panel)
  .data([1, 1.2, 1.7, 1.5, .7, .3]) .left(function() this.index * 25)
  .add(pv.Bar).width(20) .bottom(0)
  .height(function(a,b) b * 80)
  .anchor("top").add(pv.Label)
vis.render();

Well, the output is exactly the same, but how I obtained the data is different. Instead of getting the data using the standard accessor function, I passed two arguments: function(a, b).
What this does is that the first argument corresponds to the current data element of this object, and the second, to that of its parent.

In this example, they happen to be the same, but this is how you can access the data of the parent objects.

Putting it all together

Let’s see how we can use protovis and the properties of hierarchy! This example is less trivial than the ones we’ve seen so far but with what we’ve seen it is quite accessible.
The challenge: re-create square pie charts.
How it’s done:

var data=[36,78,63,24],  // arbitrary numbers
cellSize = 16,
cellPadding = 1,
squarePadding=5,
colors=pv.Colors.category20().range()
;

var vis=new pv.Panel()
    .width(4*(10*cellSize+squarePadding))
    .height(10*cellSize)
    ;

var square = vis.add(pv.Panel)
    .data(data)
    .width(10*cellSize)
    .height(10*cellSize)
    .left(function() this.index*(cellSize*10+squarePadding))
    ;

var row = square.add(pv.Panel)
	.data([0,1,2,3,4,5,6,7,8,9])
	.height(cellSize)
	.bottom(function(d) d*cellSize)
	;

var cell = row.add(pv.Panel)
    .data([0,1,2,3,4,5,6,7,8,9])
    .height(cellSize-2*cellPadding)
    .width(cellSize-2*cellPadding)
    .top(cellPadding)
    .left(function(d) d*cellSize+cellPadding)
    .fillStyle(function(a,b,c) (b*10+a)<c?colors[this.parent.parent.index].color:"lightGrey")
;

square.anchor("center")
  .add(pv.Label)
    .text(function(d) d)
    .textStyle("rgba(0,0,0,.1)")
    .font("100px sans-serif");

vis.render();

First, we initiate the data (4 arbitrary numbers from 1 to 100), and various parameters which will help size the square pie – size of the cells, space between them, space between the square pie charts. We also initiate a color palette.
Then, we are going to create 4 panels or groups of panels, each a child of the previous one.
First comes the vis panel, which groups everything,
Then the square panels, which correspond to each square pie. This is to this panel that our data is assigned.
Then come the row panels, and, finally, the cell panels.
The numbers which we want to represent are assigned to the square panel. So, what data are we passing to the row and the cell panels? The only thing we want is to have 10 rows and 10 cells per row. So, we can use an array with 10 items. We are going to use [0,1,2,3,4,5,6,7,8,9] so the data value of the row and that of the cell will correspond to the coordinate of the row and the cell, respectively. In other words, the 5th row will be assigned the data value of 4, and the 7th cell in that row will get the data value of 6. We could retrieve the same numbers using “this.index” but this can lead to obfuscated formulas.

Note that in the next part of the tutorial, we’ll see that in Protovis, there is a more elegant way to write [0,1,2,3,4,5,6,7,8,9] or similar series of numbers. But, we’ll leave this more explicit form for now.

Back to our row panel. We position it with bottom(function(d) d*cellSize). Here, again, d represents the rank of the row, so the 1st row will get 0, and its bottom value will be 0, the next row will get 1, and its bottom value will be 1*cellSize or 16, etc.

Likewise, in the cell panel, the cells are positioned with left(function(d) d*cellSize+cellPadding). This is the same principle. (here, cellPadding is just used to fine-tune the position of the cell).

This is in the final line that we are really going to use hierarchy.

.fillStyle(function(a,b,c) (b*10+a)<c?colors[this.parent.parent.index].color:"lightGrey")

here, a represents the data value of the cell – in other words, the column number.
b, the data value of the cell’s parent, the row. This is the row number.
and c is the data value of the parent of the parent of the row, the square – this is the number that we are trying to represent.

so, what we are going to determine is whether b*10+a<c. If that’s the case, we color the cell, else, we leave it in pale grey. To get the color we need, we go back to the palette that we defined at the beginning, and take the color corresponding to the square number (0 to 3 from left to right).
The square number can be obtained by this.parent.parent.index.

Finally, we add the numbers in large transparent gray digits on top of the squares.

Here is the result:

Next: Javascript and protovis array functions

Working with data in protovis – part 1 of 5

When I started using protovis I had only a very basic knowledge of javascript, which in theory isn’t a problem as protovis is meant to be learned by example, and as it has its own logic and structure which is different from typical javascript code. So I started by looking and modifying examples which was enough to do basic stuff.
But I soon felt limited by what hid behind a single property: data. I knew that protovis had lots of features to manipulate and process data but they were not obvious from the examples.

I mean,

var vis = new pv.Panel()
.width(150)
.height(150);

vis.add(pv.Bar)
.data([1, 1.2, 1.7, 1.5, .7, .3])
.width(20)
.height(function(d) d * 80)

vis.render();

Here, it’s pretty obvious that the bars represent the values 1, 1.2, 1.7, 1.5, 0.7 and 0.3 respectively. One can infer that the sizes of bars are 25 pixels wide and 80 times their value long.

But protovis doesn’t usually look like this “hello world” kind of example, but rather like this:

/* Compute yield medians by site and by variety. */
function median(data) pv.median(data, function(d) d.yield);
var site = pv.nest(barley).key(function(d) d.site).rollup(median);
var variety = pv.nest(barley).key(function(d) d.variety).rollup(median);
/* Nest yields data by site then year. */
barley = pv.nest(barley)
    .key(function(d) d.site)
    .sortKeys(function(a, b) site[b] - site[a])
    .key(function(d) d.year)
    .sortValues(function(a, b) variety[b.variety] - variety[a.variety])
    .entries();
[. . .]
/* A panel per site-year. */
var cell = vis.add(pv.Panel)
    .data(barley)
    .height(h)
    .top(function() this.index * h)
    .strokeStyle("#999");

What just happened? pv.nest, key, rollup, sortKeys, entries – what could that do?

To go beyond merely touching up examples, and do your own visualizations from scratch, it is important to get a good grip on how to feed protovis with data. In order to do so, you need a few javascript notions.

Arrays, arrays, how do they work?

In javascript, an array is an ordered list of stuff.

In our initial example, we had one such list:

[1, 1.2, 1.7, 1.5, .7, .3]

Anything can be put in an array: numbers, strings, Booleans (true/false values), objects … including other arrays. All elements of an array don’t have to be of the same type. Arrays can be assigned to a variable.

var a = [1, 1.2, 1.7, 1.5, .7, .3];

Elements of the array can be accessed using the [] notation. In javascript, indices start at 0, so the first element of an array can be obtained so:

a[0];

This returns 1. Javascript has many functions to create and manipulate arrays, which we will talk about later. For the time being, let’s look at arrays of arrays. If we wrote instead:

var a = [[1, 1.2], [1.7, 1.5], [.7, .3]];

a is now an array of arrays, or “multi-dimensional array”.

a[0] is now worth [1, 1.2]. To access the first number of the array, one has to write a[0][0], which will return the first element (1) of the first element ([1, 1.2]) of a.

Javascript also has another type of array called associative arrays, where values are assigned to keys instead of an index. For instance,

var a = {yield: 27.00000, variety: "Manchuria", year: 1931, site: "University Farm"};

is an associative array. To access a value, one can use a . operator:

a.yield

will retun 27.

a["yield"]

also works.

Like other variable types, it is possible to have an array of associative arrays. In fact, this is used quite often in protovis.

Protovis and arrays – deconstructing the first example

The reason why I introduced javascript arrays is that the data property requires an array. Protovis then loops through that array, performing operations on each of its elements. To that end, it uses things such as accessor functions and properties of an object called this.

To explain all of this let’s go back to the first example and analyse it line by line.

var vis = new pv.Panel()
  .width(150)
  .height(150);
vis.add(pv.Bar)
  .data([1, 1.2, 1.7, 1.5, .7, .3])
  .width(20)
  .bottom(0)
  .height(function(d) d * 80)
  .left(function() this.index * 25);
vis.render();

The first 3 lines create a panel, which is like the sheet of paper on which protovis will draw the chart. Its width and height properties must be filled, as they are 0 by default which would make the whole visualization invisible.

The next line adds a bar chart to this panel we’ve just created.

The line after specifies the data on which to work: here comes our array. Here, we have written the array literally in the data property, but nothing prevents us to assign it to a variable first and to pass the variable instead.

The next line, and the line with the bottom property, assign constant numbers to these properties. It means that all the bars will have a width of 20 pixels, and they will all be aligned with the bottom of the panel – that’s what

bottom(0)

does.

Now let’s look at the two remaining lines:

.height(function(d) d * 80)
.left(function() this.index * 25);

The first line uses an accessor function. What this does is that it looks at the current element, and perform an operation on it, the result of which will be the height of that element.

In proper javascript, we would have written:

function(d) {return d*80;}

but protovis uses a shorthand notation that allows us to omit curly braces and the return statement. By the way, d in the function is completely arbitrary, and could be any variable name –

function(a) a*80

also works. It’s just that the name of the variable between parentheses will represent the value of the current element.

The second line uses the this object. this represents what protovis is working on at the moment, and it has properties that can be used. The most commonly used is index: this.index returns the position of the current element in its array, so it is going to be: 0 for the first bar, 1 for the next one, etc.

So this line specifies that each new bar should start every 25 pixels from the left border of the panel.

You may wonder, why not write

.left(this.index * 25);

and omit the function()? Well, function() means that the content of the property gets re-evaluated. If we had omitted it, this.index * 25 would have been computed once (for a result of 0) and that value would have been used for all the bars.

By the way, instead of writing the height property as it is, we could have written:

.height(function()[1, 1.2, 1.7, 1.5, .7, .3][this.index] * 80)

Using an accessor function is shorter and clearer.

Next: Multi-dimensional arrays, inheritance and hierarchy