Interactive Small Multiples

Jim Vallandingham

Wee Little Things

Lena Groeger

Basics

Small multiples use the same basic graphic or chart to display difference slices of a data set

A chorus of little stories to help tell a bigger one.

Uses

stephanie evergreen - declutter dataviz

stephanie evergreen - declutter dataviz

latimes.com

latimes.com

Origins

Tufte?

Interaction

manifest destiny

Linked

Updateable

githut.info

cancer racial gap

Sortable

kepler tally

ambulances

Highlightable

middle classes

Scrubbable

tax burden

tax burden

unplanned pregnancies

faces of fracking

Brushable

brushable scatterplot

Implementation

MetaFilter

Data


"year"  "category"  "n"
"2004"  "clothing, beauty, & fashion"   141
"2004"  "computers & internet"  2489
"2004"  "education" 151
"2005"  "clothing, beauty, & fashion"   203
"2005"  "computers & internet"  2200
"2005"  "education" 201
"2005"  "food & drink"  324
"2005"  "grab bag"  248
"2005"  "health & fitness"  590
    

"year"  "category"  "n"
"2004"  "clothing, beauty, & fashion"   141
"2004"  "computers & internet"  2489
"2004"  "education" 151
"2005"  "clothing, beauty, & fashion"   203
"2005"  "computers & internet"  2200
"2005"  "education" 201
"2005"  "food & drink"  324
"2005"  "grab bag"  248
"2005"  "health & fitness"  590
    
[
  {"key":"clothing, beauty, & fashion",
   "values":
     [{"year":"2004", "n":141, "date":"2004-01-01"},
      {"year":"2005", "n":203, "date":"2005-01-01"},...]
  },
  {"key":"computers & internet",
   "values":
     [{"year":"2004", "n":2489, "date":"2004-01-01"},
      {"year":"2005", "n":2200, "date":"2005-01-01"},...]
  },
  ...
]
data = d3.nest()
  .key(function(d) { return d.category;})
  .sortValues(function(a, b) {
    return d3.ascending(a.date, b.date);
  })
  .entries(rawData);
    
[
  {"key":"clothing, beauty, & fashion",
   "values":
     [{"year":"2004", "n":141, "date":"2004-01-01"},
      {"year":"2005", "n":203, "date":"2005-01-01"},...]
  },
  {"key":"computers & internet",
   "values":
     [{"year":"2004", "n":2489, "date":"2004-01-01"},
      {"year":"2005", "n":2200, "date":"2005-01-01"},...]
  },
  ...
]
    
var div = d3.select("#vis")
  .selectAll(".chart")
  .data(data);
  
div.enter()
  .append("div")
  .attr("class", "chart")
  .append("svg").append("g");
    
var div = d3.select("#vis")
  .selectAll(".chart")
  .data(data);
  
div.enter()
  .append("div")
  .attr("class", "chart")
  .append("svg").append("g");
    
var svg = div.select("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom);

var g = svg
  .select("g")
  .attr("transform", "translate(" + margin.left + 
        "," + margin.top + ")");
    
g.append("rect")
  .attr("class", "background")
  .style("pointer-events", "all")
  .attr("width", width + margin.right)
  .attr("height", height);
.chart {
  float: left;
  padding-right: 5px;
  padding-bottom: 5px;
  padding-top: 0;
  padding-left: 0;
}

.background {
  fill: black;
}
    
var xScale = d3.time.scale().range([0, width]);
var yScale = d3.scale.linear().range([height, 0]);
    
var line = d3.svg.line()
  .x(function(d) { return xScale(d.date); })
  .y(function(d) { return yScale(d.n); });

var area = d3.svg.area()
  .x(function(d) { return xScale(d.date); })
  .y0(height).y1(function(d) { return yScale(d.n); });

    
lines = g.append("g");

lines.append("path")
  .attr("class", "area")
  .attr("d", function(c) {
    return area(c.values);
  });

lines.append("path")
  .attr("class", "line")
  .attr("d", function(c) {
    return line(c.values);
  });
 .area {
  fill: #cec6b9;
}

.line {
  fill: none;
  stroke: #74736c;
  stroke-width: 1.4px;
}
    
    

Interactive

Scrubbable

g.append("rect")
  .attr("class", "background")
  .style("pointer-events", "all")
  .attr("width", width + margin.right)
  .attr("height", height)
  .on("mouseover", mouseover)
  .on("mousemove", mousemove)
  .on("mouseout", mouseout);
    
// hide for now
circle = lines.append("circle")
  .attr("r", 2.2)
  .attr("opacity", 0)
    
var mouseover = function() {
  circle.attr("opacity", 1.0);

  return mousemove.call(this);
};
    
var mouseout = function() {
  circle.attr("opacity", 0);
  
  return true;
};
    
var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var date = xScale.invert(xpos);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = findIndex(c.values, date);
      return yScale(c.values[index].n);
  });
  //...
}
    
var format = d3.time.format("%Y");
    
var xScale = d3.time.scale()
  .domain([format.parse("2013"), format.parse("2014")]);
  .range([0, width])
    
var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var date = xScale.invert(xpos);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = findIndex(c.values, date);
      return yScale(c.values[index].n);
  });
  //...
}
var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var date = xScale.invert(xpos);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = findIndex(c.values, date);
      return yScale(c.values[index].n);
  });
  //...
}
// find location in 'array' with
// date nearest to 'date'
var findIndex = function(array, date) {
  var found = false;
  var index = 1;
  while (!found && index < array.length ) {
    if(array[index].date > date) {
      found = true;
    } else {
      index++;
    }
  }

  return index - 1;
};
    
var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var date = xScale.invert(xpos);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = findIndex(c.values, date);
      return yScale(c.values[index].n);
  });
  //...
}
//create bisector
// (binary search)
var bisect = d3.bisector(function(d) { return d.date; })
  .left;
    
//with bisector
var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var date = xScale.invert(xpos);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = bisect(c.values, date, 0, c.values.length - 1);
      return yScale(c.values[index].n);
  });
  //...
}
    
[
  {"key":"clothing, beauty, & fashion",
   "values":
     [{"year":"2004", "n":141, "date":"2004-01-01"},
      {"year":"2005", "n":203, "date":"2005-01-01"},...]
  },
  {"key":"computers & internet",
   "values":
     [{"year":"2004", "n":2489, "date":"2004-01-01"},
      {"year":"2005", "n":2200, "date":"2005-01-01"},...]
  },
  ...
]
    
var format = d3.time.format("%Y");

var mousemove = function() {
  // 'this' is current DOM element
  var xpos = d3.mouse(this)[0];
  var year = xScale.invert(xpos).getFullYear();
  var date = format.parse('' + year);
  var index = 0;

  circle
    .attr("cx", xScale(date))
    .attr("cy", function(c) {
      index = bisect(c.values, date, 0, c.values.length - 1);
      return yScale(c.values[index].n);
  });
  //...
}

Sortable

var setupIsoytpe = function() {
  $("#vis").isotope({
    itemSelector: '.chart',
    layoutMode: 'fitRows',
    getSortData: {
    }
  });
};
var setupIsoytpe = function() {
  $("#vis").isotope({
    itemSelector: '.chart',
    layoutMode: 'fitRows',
    getSortData: {
      count: function(e) {
        var d = d3.select(e).datum();
        var sum = d3.sum(d.values, function(d) {
          return d.n;
        });
        return sum * -1;
      }    
    }
  });
};
var setupIsoytpe = function() {
  $("#vis").isotope({
    itemSelector: '.chart',
    layoutMode: 'fitRows',
    getSortData: {
      count: function(e) {
        var d = d3.select(e).datum();
        var sum = d3.sum(d.values, function(d) {
          return d.n;
        });
        return sum * -1;
      },
      name: function(e) {
        var d = d3.select(e).datum();
        return d.key;
      }
    }
  });
};
$("#vis").isotope({sortBy: 'count'});
    
d3.select("#button-wrap")
  .selectAll("div")
  .on("click", function() {
    var id = d3.select(this).attr("id");

    $("#vis").isotope({sortBy: id});
    // ...
  });
});
    

github.com/vlandham/small_mults

vallandingham.me/small_mults/

Thanks