Interactive Small Multiples
Jim Vallandingham
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
Origins
Tufte?
Interaction
Linked
Updateable
Sortable
Highlightable
Scrubbable
Brushable
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});
// ...
});
});
Thanks