stack-presentation



stack-presentation

1 1


stack-presentation

Using the d3 stack layout

On Github Boulder-Denver-d3-vis / stack-presentation

Boulder/Denver D3 And Data Vis

November 21, 2013

http://bit.ly/1h3yQSN

http://boulder-denver-d3-vis.github.io/stack-example/

The Presenters:

Erik Cunningham

@trinary

http://github.com/trinary

Brian Lehman

@wordcrank

http://github.com/blehman

Today's Topics

  • Workflow:
    • Quick ways to see your work
  • Layouts:
    • Helpers in D3 that augment your data for display

Workflow

D3 lives in the browser, so having a good browser worflow is important

Lots of options

  • file:// urls for everything
  • python SimpleHTTPServer
  • node (yeoman, livereload)
  • Tributary
  • XAMPP, middleman, etc

File URLs

The simplest possible approach, just link everything as file URLs, e.g. file:///home/user/project/file.js

Advantages:

  • Easy as can be

Disadvantages:

  • Will break on external resources
  • No LiveReload, minification, test runners

Python Static Files

Use Python's built-in SimpleHTTPServer to serve static files

If you have Python and a shell, this gets you a web server:

python -m SimpleHTTPServer

Advantages:

  • Load resources the same way as a production server, GitHub Pages, etc.

Disadvantages:

  • No livereload, minification, test runners

Tributary

Live D3 coding environment

Advantages:

  • instantaneous feedback
  • Saves as Gist
  • Linked data
  • Helpers for animation

Disadvantages:

  • Need to port to use in other apps/sites
  • In-browser editor

Node and Yeoman

Use the yeoman node module to generate a static site.

  • Install node.js
  • npm install -g yo grunt-cli bower
  • yo, pick static site generator, bower install, grunt server

Advantages:

  • Lots of generators, livereload, tests, pretty fast

Disadvantages:

  • Lots of prerequisites, disk space, complexity

Others

There are tons of options:

  • Middleman, ruby static site generator
  • XAMPP, Apache server stack
  • Probably tons of others

Find something that works well with your editor, browser, and allows quick reloading of your site.

Today's Example

Stacked area chart, adapted from Scott Murray's D3 book:

http://github.com/boulder-denver-d3-vis/stack-example

Clone it, use python to serve it, or just open index.html up and follow along.

Starting Point

http://chimera.labs.oreilly.com/books/1230000000345/ch11.html#_stack_layout

The Stack Layout

D3's layouts produce a function that modifies or adds properties to your data so that it is convenient to display in a particular way.

The stack layout calculates a a 'baseline' for each element in a stack, based on the values before it.

If your stack is along the y axis (as ours is), this results in a new y0 attribute on our data.

Organizing Data

D3's stack layout expects an array of arrays.

The points for each color go into an array together.

All of these arrays (housed in an array) make our dataset.

This confused me initially when applying it to stacked bars.

Organizing Data

To provide the data that D3 expects:

If I have apples, bananas, and grapes types, and a value for each type for 5 time periods:

var dataset = [
  [ // apples
    { y: 5 }, { y: 4 }, { y: 2 }, { y: 7 }, { y: 23 }
  ],
  [ // bananas
    { y: 10 }, { y: 12 }, { y: 19 }, { y: 23 }, { y: 17 }
  ],
  [ // grapes
    { y: 22 }, { y: 28 }, { y: 32 }, { y: 35 }, { y: 43 }
  ]
];

Take note, 3 series of 5 items each, not the other way around.

Applying Stack

First, create our stack function: (API Docs)

var stack = d3.layout.stack();

And then apply it to our data:

stack(dataset);

The stack function we got back from D3 will add baseline values for our stacks, using D3's defaults.

Result of Stack

Our data has been annotated with y0 values representing the baseline for each part of the stack.

data => 
[ // Apples. Bottom of the stack, so all baselines are 0!
  [{y: 5, y0: 0}, {y: 4, y0: 0} ...  ],
  // Bananas. Second on the stack, so all 
  // baselines are the corresponding apple value.
  [{y: 10, y0: 5}, {y: 12, y0: 4} ...  ],
  // Grapes. Top of the stack, so all baselines 
  // are the sum of apple and orange values.
  [{y: 22, y0: 15}, {y: 28, y0: 16} ...  ],
]

If the first apples value is 5, the first bananas baseline should be 5.

Drawing our stacks

A two level binding is required.

First, we bind a group element to each series

Then, inside each group, we bind rectangles to each data value

We use y0 as our baseline, and y as our height, with a linear scale applied.

Outer Binding

var groups = svg.selectAll("g").data(dataset)
    .enter()
    .append("g")
    .attr("transform","translate(" + [margin.left, margin.top] + ")")
    .style("fill", function(d, i) { return colors(i); });

Inner Binding

var rects = groups.selectAll("rect") // Creating a new sub-selection inside groups
  .data(function(d) { return d; })   // Our data is the value of the data bound to each group.
  .enter()
  .append("rect")
  .attr("x",      function(d, i) { return xScale(i);   })
  .attr("y",      function(d)    { return yScale(d.y0);})
  .attr("height", function(d)    { return yScale(d.y); })
  .attr("width", xScale.rangeBand());

Starting Point

http://chimera.labs.oreilly.com/books/1230000000345/ch11.html#_stack_layout

SVG's Coordinate System

Scaling the Y Axis

The original example uses SVG's default y axis, from the top.

var yScale = d3.scale.linear()
    .domain([0, max])
    .range([0, height]);
  • The domain is the input to a scale
  • The range is the output of a scale

The output of this scale starts at 0 (the top of the SVG).

Inverting Y

Instead, let's change the output of the y scale to start at the height and go to 0:

var invertedYScale = d3.scale.linear()
    .domain([0,max])
    .range([height, 0]);

But how does this affect our y and height attributes?

Fixing Attributes

Before:

var rects = groups.selectAll("rect")
  .data(function(d) { return d; })
  .enter()
  .append("rect")
  .attr("x", function(d, i)   { return xScale(i); })
  .attr("y", function(d)      { return yScale(d.y0); })
  .attr("height", function(d) { return yScale(d.y); })
  .attr("width", xScale.rangeBand());

After:

var rects = groups.selectAll("rect")
  .data(function(d) { return d; })
  .enter()
  .append("rect")
  .attr("x", function(d, i)   { return xScale(i);                    })
  .attr("y", function(d)      { return invertedYScale(d.y0 + d.y);   })
  .attr("height", function(d) { return height - invertedYScale(d.y); })
  .attr("width", xScale.rangeBand());

And we're done!

To Review:

Find a workflow that you enjoy

  • Favor tools that provide immediate feedback

Make fat stacks

  • 2-dimensional data requires 2-dimensional binding.
  • Scales and coordinate systems need to work in harmony.

Questions?

Exercises

Beginner:

  • Add a new series to the data, and predict its baseline values
  • Work through the attr values using your browser's debug tools

Advanced

  • Change the data to this format:
[
  { name: "apples", values: [{y: 5}, {y:4}...etc]},
  { name: "bananas", values: [{y: 5}, {y:4}...etc]},
]

Here's a starting point

Fix the d3.layout.stack() call, data() bindings, scales, legend generation, etc.