Skip to content

Instantly share code, notes, and snippets.

@marcdhansen
Last active March 6, 2017 19:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcdhansen/1ace92ea6344aa05bbac to your computer and use it in GitHub Desktop.
Save marcdhansen/1ace92ea6344aa05bbac to your computer and use it in GitHub Desktop.
Dancing Histograms

Click on bar chart or legend to change which group moves to the y=0 baseline.

D3 version of Alan Dix's dancing histograms at http://www.meandeviation.com/dancing-histograms/

The problem

A stacked histogram allows three judgements: (i) the trends on the total height of the columns, (ii) the proportion of each category within each column and (iii) the trends in the lowest category. The trends, or even inter-column comparisons for any other category is very difficult as the blocks are at different heights.

The interactive stacked histogram solves this problem by allowing different trends to be analysed using the same dynamic graph. It is an example of a general princple of adding interactivity to existing paper visualisations.

For more information on this and related topics see Alan Dix's visualisation pages and general research topics.

Also see the paper describing this work: A. Dix and G. Ellis (1998). Starting Simple - adding value to static visualisation through simple interaction. http://www.comp.lancs.ac.uk/computing/users/dixa/papers/simple98/

var chart = function module(){
// d3 version of Alan Dix's original dancing histograms java applet at
// http://www.meandeviation.com/dancing-histograms/
var transitionTime = 750;
//Width and height
var outerWidth = 960;
var outerHeight = 500;
var margin = {top: 50, right: 40, bottom: 20, left: 50};
var padding = {top: 60, right: 60, bottom: 60, left: 60};
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
var w = innerWidth - padding.left - padding.right;
var h = innerHeight - padding.top - padding.bottom;
var legendHeight = 20;
var legendWidth = 175;
var legendTopOffset = 30;
var polyOpacity = 1;
var legendOpacity = 0;
//outline around groups and legend box -- set to 1 to match original
var groupStrokeOpacity = 0;
// hard-coded colors to try to match original
var colors = function(i){
return ["limegreen", "yellow", "orange", "crimson"][i];
};
var dataByGroup = null;
var nest = d3.nest()
.key(function(d) {
return d.group; });
//Set up scales
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, w], 0.05);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yScale = d3.scale.linear()
.range([h/2,0]);///
var yAxisScale = d3.scale.linear()
.range([h,0]);
var yAxis = d3.svg.axis().scale(yAxisScale).orient("left");
var heightScale = d3.scale.linear()
.range([0,h/2]);///
var svg = d3.select("body")
.append("svg");
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var groups = null;
var numGroups = null;
var rects = null;
var legends = null;
var line = null;
var offset = 0; // determines which group is at baseline
// create a partially initialized array of offsets for each bar stack
var offsets = [0];
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", 1)
.style("opacity", 1e-6);
function mouseover() {
div.transition()
.duration(500)
.style("opacity", 1);
}
function mousemove() {
div.transition()
.duration(500)
.style("opacity", 1);
var datum =d3.select(this).datum();
div
.html(datum.year + "<br/>" + datum.fruit + "<br/>" + datum.sales )
.style("left", (d3.event.pageX - 2) + "px")
.style("top", (d3.event.pageY - 64) + "px");
}
function mouseout() {
disappearTooltip();
}
function disappearTooltip(){
div.transition()
.duration(500)
.style("opacity", 1e-6)
}
d3.csv("fruits.csv", function(error, data) {
data.forEach(function(d) {
d.group = d.fruit;
d.x = d.year;
d.y = +d.sales;
});
dataByGroup = nest.entries(data);
numGroups = dataByGroup.length;
var stackz = d3.layout.stack();
/////////////////////////////////////////////////////////////////////
var stack = d3.layout.stack()
.values(function(d) { return d.values; })
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.out(function(d, y0) { d.y0 = y0; });
stack(dataByGroup);
xScale
.domain(dataByGroup[0].values.map(function(d) { return d.x; }));
var yMax = d3.max(dataByGroup, function(d) {
return d3.max(d.values, function(d) {
return d.y0 + d.y;
});
});
yScale
.domain([0,yMax]);
yAxisScale
.domain([-yMax,yMax]);
heightScale
.domain([0,
d3.max(dataByGroup, function(d) {
return d3.max(d.values, function(d) {
return d.y0 + d.y;
});
})
]);
// Add a group for each row of data
groups = g.selectAll(".groups")
.data(dataByGroup)
.enter()
.append("g")
.attr("class", function(d,i){ return i; })
.style("fill", function(d, i) {
return colors(i);
})
.style("stroke", "black")
.style("stroke-opacity", groupStrokeOpacity)
.style("opacity", function(d,i){
return polyOpacity;
});
groups
.on("click", function(d,i){
var thisclass = +d3.select(this).attr("class");
// ignore clicks on groups already at baseline
if(offset !== thisclass){
offset = thisclass;
my.updateChart(false, offset);
disappearTooltip();
}
});
g
.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + 4 + ", 0)")
.call(yAxis);
g
.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0, " + h/2 + ")")///
.call(xAxis);
// Add a rect for each data value
rects = groups.selectAll("rect")
.data(function(d,i) {
var offsetsArray = d.values.map(function(d){
return d.y0;});
offsets[i] = offsetsArray.map(function(num){
return heightScale(num);});
return d.values; })
.enter()
.append("rect")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
legends = groups
.append("g");
// draw a big transparent background rectangle for a bigger cursor target
legends
.append("rect")
.attr("x",0)
.attr("y",-10)
.attr("height", legendHeight)
.attr("width", 150)
.attr("fill-opacity", legendOpacity)
.style("stroke-opacity", legendOpacity);
legends
.append("rect")
.attr("x",15)
.attr("y",-10)
.attr("height", 20)
.attr("width", 20)
.attr("fill", function(d,i){return colors(i);})
.attr("fill-opacity", polyOpacity);
legends.append("g:text")
.attr("x", 40)
.attr("dy", ".31em")
.attr("fill", "black")
.text(function(d,i,j){
return d.key;});
// baseline:
line = g.append("line")
.style("stroke","black")
.style("stroke-width",2);
my.updateChart(true, 0); // don't draw the chart until all the data has been processed
});
function my(){}
var lineOffset = 4;
my.updateChart = function(init, inputOffset){
if(init){
transitionMS = 0;
} else {
transitionMS = transitionTime;
}
offset = inputOffset;
var legendXOffset = w;
svg
.attr("width", outerWidth)
.attr("height", outerHeight);
line
.attr("x1", lineOffset)
.attr("y1", h/2)
.attr("x2", w - lineOffset)
.attr("y2", h/2);
legends
.attr("transform", function (d, i) {
return "translate(" + legendXOffset + "," +
((numGroups - i) * legendTopOffset) + ")";
});
rects
.transition().duration(transitionMS)
.attr("x", function(d, i) {
return xScale(d.x);
})
.attr("y", function(d,i,j) {
return yScale(d.y0 + d.y) + offsets[offset][i];
})
.attr("height", function(d) {
return heightScale(d.y0 + d.y) - heightScale(d.y0);
})
.attr("width", function(d,i){return xScale.rangeBand();});
// move the x axis labels along with the bars
svg
.select(".xAxis")
.selectAll("text")
.transition().duration(transitionMS)
.attr("transform", "translate(0," + d3.max(offsets[offset]) + ")");
// chart title
svg.append("text")
.attr("x", (innerWidth / 2))
.attr("y", 0 + (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "20px")
.text("Fruit Sales 1992-1997");
}
return my;
}
fruit sales year
apples 12 1992
apples 15 1993
apples 17 1994
apples 19 1995
apples 17 1996
apples 15 1997
bananas 21 1992
bananas 20 1993
bananas 21 1994
bananas 22 1995
bananas 23 1996
bananas 24 1997
clementines 26 1992
clementines 25 1993
clementines 24 1994
clementines 20 1995
clementines 17 1996
clementines 20 1997
dates 6 1992
dates 7 1993
dates 7 1994
dates 3 1995
dates 7 1996
dates 6 1997
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reproduction of Alan Dix's Dancing Histograms</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
text {
font: 20px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.xAxis path, .xAxis line {
fill: none;
stroke: none;
shape-rendering: crispEdges;
}
div.tooltip {
position: absolute;
text-align: left;
width: 90px;
height: 56px;
padding: 4px;
font-size: 16px;
color: #fff;
background: #000;
border-radius: 8px;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript" src="dancingHistograms.js"></script>
<script type="text/javascript">
var myChart = chart();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment