Reusable
Visualizations

Miles McCrocklin

@milr0c

d3.js

data driven documents

Maps Data to DOM

Reusable Visualizations API

Backend MV*

Frontend MV* (Brief)

Resources

data = randomizeData(20, Math.random()*100000);
margin = {top: 0, bottom: 20, left: 0, right: 0},
    width = 400,
    height = 400,
    duration = 500,
    formatNumber = d3.format(',d'),
    brush = d3.svg.brush();
margin.left = formatNumber(d3.max(data, function(d) { return d.y; })).length * 14;
w = width - margin.left - margin.right,
    h = height - margin.top - margin.bottom;
x = d3.scale.ordinal()
            .rangeRoundBands([0, w], .1),
    y = d3.scale.linear()
            .range([h, 0]);
y.domain([0, d3.max(data, function(d) { return d.y; })]);
x.domain(data.map(function(d) { return d.x; }));
xAxis = d3.svg.axis()
              .scale(x)
              .orient('bottom'),
    yAxis = d3.svg.axis()
              .scale(y)
              .orient('left');
svg = d3.select('#chart').selectAll('svg').data([data]),
    svgEnter = svg.enter().append('svg')
                            .append('g')
                              .attr('width', w)
                              .attr('height', h)
                              .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
                              .classed('chart', true),
    chart = d3.select('.chart');
svgEnter.append('g')
          .classed('x axis', true)
          .attr('transform', 'translate(' + 0 + ',' + h + ')');
svgEnter.append('g')
          .classed('y axis', true)
svgEnter.append('g').classed('barGroup', true);
chart.selectAll('.brush').remove();
chart.selectAll('.selected').classed('selected', false);
chart.append('g')
          .classed('brush', true)
          .call(brush)
        .selectAll('rect')
          .attr('height', h);
bars = chart.select('.barGroup').selectAll('.bar').data(data);
bars.enter()
      .append('rect')
        .classed('bar', true)
        .attr('x', w) // start here for object constancy
        .attr('width', x.rangeBand())
        .attr('y', function(d, i) { return y(d.y); })
        .attr('height', function(d, i) { return h - y(d.y); });
bars.attr('width', x.rangeBand())
    .attr('x', function(d, i) { return x(d.x); })
    .attr('y', function(d, i) { return y(d.y); })
    .attr('height', function(d, i) { return h - y(d.y); });
bars.exit().style('opacity', 0).remove();
chart.select('.x.axis').call(xAxis);
chart.select('.y.axis').call(yAxis);
var bar = charts.bar()
                  .width(600)
                  .height(600);
var data = [{x: 0, y: 30}, 
            {x: 1, y: 100}, 
            {x: 2, y: 60}, 
            {x:3, y:2}];

d3.select('#DIV-ID')
    .datum(data)
    .call(bar);
        
      

Reusable
Visualizations

INTERACT WITH THE VISUAL
charts.bar = function() {
  function render(selection) {
    // render chart
  }
  // accessors
  return render;
};
var bar = charts.bar();
var data = [{x: 0, y: 30}, {x: 1, y: 100}, 
            {x: 2, y: 60}, {x:3, y:2}];

d3.select('#DIV-ID')
    .call(bar);
        
      

Accessors

var bar = charts.bar()
                  .width(200);
var data = [{x: 0, y: 30}, {x: 1, y: 100}, 
            {x: 2, y: 60}, {x:3, y:2}];

d3.select('#DIV-ID')
    .datum(data)
    .call(bar);
        
      

Closure

closes over the free variables (variables which are not local variables)

charts.bar = function() {
  var width = 800;

  function render(selection) {
    // render chart
  }
  // accessors
   
  return render;
};
      
charts.bar = function() {
  var width = 800;

  function render(selection) {
    // render chart
  }
  // accessors
   render.width = function(val) {
     if(!arguments.length) return width;
     width = val;
     return render;
   };
  return render;
};
      
// basic data
var margin = {top: 20, bottom: 20, left: 0, right: 0},
    width = 400,
    height = 400,
    // accessors
    xValue = function(d) { return d.x; },
    yValue = function(d) { return d.y; },
    // chart underpinnings
    brush = d3.svg.brush(),
    xAxis = d3.svg.axis().orient('bottom'),
    yAxis = d3.svg.axis().orient('left'),
    x = d3.scale.ordinal(),
    y = d3.scale.linear(),
    // chart enhancements
    duration = 500,
    formatNumber = d3.format(',d');

Render Chart

var bar = charts.bar();
var data = [{x: 0, y: 30}, {x: 1, y: 100}, 
            {x: 2, y: 60}, {x:3, y:2}];

d3.select('#DIV-ID')
    .datum(data)
    .call(bar);
        
      
charts.bar = function() {
  var width = 800;
  function render(selection) {
    // render chart
  }
  // accessors
  render.width = function(val) {
    if(!arguments.length) return width;
    width = val;
    return render;
  };
  return render;
};
      
var bar = charts.bar()
                  .xValue(function(d) { return d.id; })
                  .yValue(function(d) { return d.value; });
var data = [{id: 0, value: 30}, {id: 1, value: 100}, 
            {id: 2, value: 60}, {id:3, value:2}];

d3.select('#DIV-ID')
    .datum(data)
    .call(bar);
        
      
function render(selection) {
  // selection has __data__ = data, no join.
  selection.each(function(data) {
    
    // if needed, convert to standard representation of data
    data = data.map(function(d, i) {
      return [xValue.call(data, d, i), 
              yValue.call(data, d, i)];
    });

    // create skeleton chart
    // visualize
  });
}
function render(selection) {
  // selection has __data__ = data, no join.
  selection.each(function(data) {
    // if needed, convert to standard representation of data
    data = data.map(function(d, i) {
      return [xValue.call(data, d, i), 
              yValue.call(data, d, i)];
    });
    // create skeleton chart
    var svg = d3.select(this)
                    .selectAll('svg')
                    .data([data]);
    var gEnter = svg.enter().append("svg").append("g");
    // visualize
  });
}
// visualize
// reset closure'd values
var w = width - margin.left - margin.right;
x.domain(data.map(function(d) { return d.x; }))
  .rangeRoundBands([0, w], .1);
      
// visualize
// render the bars
var bars = chart.select('.barGroup')
                  .selectAll('.bar')
                    .data(data);
bars.enter().append('rect') // new data
              .classed('bar', true);
bars.style('opacity', 1) // new data and updating current data
    .attr('width', x.rangeBand())
    .attr('x', function(d, i) { return x(d.x); })
    .attr('y', function(d, i) { return y(d.y); })
    .attr('height', function(d, i) { return h - y(d.y); });
bars.exit().style('opacity', 0).remove(); // removed data
charts.bar = function() {
  function render(selection) {
    // render chart
  } 
  // accessors
  
  d3.rebind(render, brush, 'on');
  
  return render;
};
      
var bar = charts.bar()
                  .on('brushstart', sum)
                  .on('brushend', sum)
                  .on('brush', sum);
var data = [{x: 0, y: 30}, {x: 1, y: 100}, 
            {x: 2, y: 60}, {x:3, y:2}];

d3.select('#DIV-ID')
    .datum(data)
    .call(bar);

function sum() {
  var extent = d3.event.target.extent();
  var x = bar.x();
  // sum the elements in extent
}
        
      
INTERACT WITH THE VISUAL

Backend MV*

BACKEND
CONTROLLER
MODEL
VIEW
BROWSER
FRONTEND
SHIFT + CLICK to step through interactions

d3.json

var data;
d3.json('/controller/call/get/model', function(json) {
  data = json;
  main();
});

function main() {
  var bar = charts.bar();
  d3.select('#chart')
      .datum(data)
      .call(bar);
}
INTERACT WITH THE VISUAL

Frontend MV*

Keep data visualization code separate from your MVWhatever code

D3 is intentionally a low-level system. During the early design of D3, we even referred to it as a "visualization kernel" rather than a "toolkit" or "framework".

Ember.js is the backbone of our new analytics page, while D3.js – to continue the tortured analogy – is the muscle powering our visualizations.

Angular.js

main.directive('chartBar', function() {
  var bar = charts.bar();    
  return {
    restrict: 'E',
    replace: true,
    template: '<div class="chart"></div>',
    scope: {
      data: '=',
    },
    link: function($scope, $element, $attr) {
      $scope.$watch('data', function(newVal, oldVal) {
        d3.select($element[0]).datum(newVal).call(bar);
      });
    }
  }
});
<chart-bar height={{height}} width={{width}} data={{data}}>
</chart-bar>
        
      

Backbone.js

var BarChart = Backbone.View.extend({
  el: "#chart",
  initialize: function() {
    _.bindAll(this, "render");
    this.collection.bind("change add remove", this.render);
    var bar = this.bar = charts.bar();
  },
  render: function() {
    var data = this.collection.models;
    d3.select(this.$el.selector)
        .datum(data)
        .call(this.bar);
  },
});
        
      

Ember.js

App.barView = Ember.View.extend({
  update: function() {
    var elementId = this.get('elementId');
    var data = this.get('content');
    var bar = this.get('bar') || charts.bar();
    d3.select('#'+elementId)
          .datum(data)
          .call(bar);
    this.set('bar', bar);
  }.observes([email protected]'),
  didInsertElement: function() {
    this.update();
  }
});

Resources

vega

Visualization Grammar

NVD3

Reusable Charting

dc.js

Dimensional Charting*

*crossfilter.js

Thank You!