Data Visualization with D3.js

Ben Clinkinbeard

Who?

  • Principal Architect @ Universal Mind
  • Swiz Framework
  • FlexMDI
  • Data visualization with Flare

D3.js background

  • Open source - BSD
  • http://d3js.org/
  • Hosted on GitHub
  • Mike Bostock
  • Academic pedigree

D3.js lineage

  • Prefuse - Java
  • Flare - AS3
  • Protovis - JavaScript

But is it COOL?!?!

Streamgraph

Credit: Mike Bostock

Rounded Rectangles

Credit: Mike Bostock

Collision Detection

Credit: Mike Bostock

Data-Driven Documents

  • Fairly low level
  • DOM manipulation based on data
  • SVG (for the good stuff)
  • HTML and CSS... natch

Be warned

  • SVG means IE8 and below is out

SVG element types


<rect x="0" y="0" width="20" height="100" />

<circle cx="50" y="50" r="20" />

<ellipse cx="250" cy="25" rx="100" ry="25" />

<line x1="0" y1="0" x2="500" y2="50" stroke="black" />

<text x="250" y="25">Easy-peasy</text>

<path d="M150 0 L75 200 L225 200 Z">
                    

SVG (super) basics

  • Must have a base svg element
  • Element declaration order determines depth

<svg width="800" height="600">
    <circle cx="50" y="50" r="20" />
    <circle cx="100" y="250" r="20" />
</svg>
                    

Enough already!

How do we build stuff?!

Basic bar chart


var dataset = [ 5, 10, 15, 20, 25 ];

d3.select( "body" )
  .selectAll( "div" )
  .data( dataset )
  .enter()
    .append( "div" )
    .attr( "class", "bar" )
    .style( "height", function( d ) {
        var barHeight = d * 5;
        return barHeight + "px";
    } );
                        

div.bar {
    display: inline-block;
    width: 20px;
    height: 75px;  /* Gets overriden by D3-assigned height */
    margin-right: 2px;
    background-color: teal;
}
                        

Ta-da

Credit: Scott Murray

Here's the thing...


<body></body>
                    
But what about this code?

var dataset = [ 5, 10, 15, 20, 25 ];

d3.select( "body" )
  .selectAll( "div" )
  .data( dataset )
                    

Keep this in mind

Instead of telling D3 how to do something, tell D3 what you want.

Selections

  • Selections are a core concept of D3
  • Array of elements pulled from the current document
  • Uses CSS3 to select elements
  • data() joins a selection to data
    • Everything beyond data() in a chain is executed for each data point
  • Read the wiki

Selections API

  • .attr(name[, value])
  • .style(name[, value[, priority]])
  • .property(name[, value])
  • .html([value])
  • .text([value])

Selections API

  • .attr(name, {constant})

d3.select( "body" )
  .selectAll( "div" )
  .attr( "class", "bar" )
                    
  • .attr(name, {function})

d3.select( "body" )
  .selectAll( "div" )
  .attr( "class", function( d, i ) {
    // d is datum being rendered
    // i is datum's index in dataset
    // return value based on logic
  } )
                    

Selections API

  • .style(name, {constant})

d3.select( "body" )
  .selectAll( "div" )
  .style( "background-color", "teal" )
                    
  • .style(name, {function})

d3.select( "body" )
  .selectAll( "div" )
  .style( "background-color", function( d, i ) {
    // d is datum being rendered
    // i is datum's index in dataset
    // return value based on logic
  } )
                    

Selections API

  • .attr(name)

var className = d3.select( "body" )
  .selectAll( "div" )
  .attr( "class" )
                    
  • .style(name)

var bgColor = d3.select( "body" )
  .selectAll( "div" )
  .style( "background-color" )
                    

enter() and exit()

  • Subselections representing elements to be added or removed
  • enter() creates new, data-bound DOM elements and is generally followed by append()
  • exit() is generally followed (eventually) by remove()

Scales

  • “Scales are functions that map from an input domain to an output range.” -Mike Bostock
  • Normalize values
  • Make charts infinitely more flexible

Scatterplot without scales


var dataset = [ 5, 10, 15, 20, 25 ];
var w = 200;
var h = 200;

var svg = d3.select( "body" )
        .append( "svg" )
        .attr( "width", w ).attr( "height", h )
        .style( "background-color", "#DDDDDD" );

svg.selectAll( "circle" )
        .data( dataset )
        .enter()
        .append( "circle" )
        .attr( "opacity", .3 )
        .attr( "cx", function( d ) {
            return d;
        } )
        .attr( "cy", function( d ) {
            return d;
        } )
        .attr( "r", 12 );
                    

Scatterplot without scales

Scatterplot with X scale


// instead of simply returning the data value
.attr( "cx", function( d ) {
    return d;
} )

// create a scale and use it to transform the value
var xScale = d3.scale.linear()
        .domain( [d3.min( dataset ), d3.max( dataset )] )
        .range( [0, w] );

.attr( "cx", function( d ) {
    return xScale( d );
} )
                    

Scatterplot with X scale

Scatterplot with X and Y scale


var xScale = d3.scale.linear()
        .domain( [d3.min( dataset ), d3.max( dataset )] )
        .range( [0, w] );

var yScale = d3.scale.linear()
        .domain( [d3.min( dataset ), d3.max( dataset )] )
        .range( [0, h] );

        .attr( "cx", function( d ) {
            return xScale( d );
        } )
        .attr( "cy", function( d ) {
            return yScale( d );
        } )
                    

Scatterplot with X and Y scale

Flexible you say?

Changing this

var w = 600;
var h = 300;
                    
Gets you this

Prevent run off with padding


var padding = 30;

var xScale = d3.scale.linear()
        .domain( [d3.min( dataset ), d3.max( dataset )] )
        .range( [padding, w - padding] );

var yScale = d3.scale.linear()
        .domain( [d3.min( dataset ), d3.max( dataset )] )
        .range( [padding, h - padding] );
                    

Scumbag SVG orients top-left

Wouldn't you expect this data

var dataset = [ 5, 10, 15, 20, 25 ];
                    
To produce something like this?

Scales to the rescue

Reversing the range does the trick

    .range( [padding, h - padding] );
                    
becomes

    .range( [h - padding, padding] );
                    

Scales normalize...


var dataset = [ 5, 10, 15, 20, 25, 100 ];
                    

...and interpolate


var colorScale = d3.scale.linear()
        .domain( [ 5, 25 ] )
        .range( ["orange", "purple"] );

.attr( "fill", function( d ) {
    return colorScale( d );
} )
                    

Crazy powerful


var colorScale = d3.scale.linear()
        .domain( [ 5, 15, 25 ] )
        .range( ["red", "purple", "green"] );

.attr( "fill", function( d ) {
    return colorScale( d );
} )
                    

OK, let's move on!

Axes


var xScale = d3.scale.linear()
        .domain( [0, 50] )
        .range( [padding, w - padding] );

var xAxis = d3.svg.axis()
    .scale( xScale ).orient( "bottom" ).ticks( 5 );

svg.append( "g" )
    .attr( "class", "axis" )
    .attr( "transform", "translate(0, " + (h - padding) + " )" )
    .call( xAxis );
                

Transitions


    .enter()
    .append( "circle" )
    .attr( "cx", w )
    .attr( "cy", h )
    .attr( "r", 0 )
        .transition()
            .delay( 250 )
            .duration( 500 )
            .attr( "cx", function( d ) {
                return xScale( d );
            } )
            .attr( "cy", function( d ) {
                return yScale( d );
            } )
            .attr( "r", 12 );
                    
Open Demo

Transitions

Values swapped for functions... again

    .enter()
    .append( "circle" )
    .attr( "cx", w )
    .attr( "cy", h )
    .attr( "r", 0 )
        .transition()
            .delay( function( d, i ) {
               return i * 100;
            } )
            .duration( 500 )
            .attr( "cx", function( d ) {
                return xScale( d );
            } )
            .attr( "cy", function( d ) {
                return yScale( d );
            } )
            .attr( "r", 12 );
                    
Open Demo          Open Gratuitous Demo

Further Reading

Questions?

Thank You!