Skip to content

Instantly share code, notes, and snippets.

@eric
Last active December 18, 2015 17:28
Show Gist options
  • Save eric/5818316 to your computer and use it in GitHub Desktop.
Save eric/5818316 to your computer and use it in GitHub Desktop.
// Originally from:
// * http://blog.stephenboak.com/2011/08/07/easy-as-a-pie.html
// * http://jsfiddle.net/stephenboak/hYuPb/
//
// Improved by Eric Lindvall to be more d3-ish
function donutChart() {
var width = 450;
var height = 300;
var radius = 100;
var innerRadius = 45;
var textOffset = 14;
var tweenDuration = 250;
var totalTitleText = "TOTAL";
var totalLabelText = "";
var waitingLabelText = "Waiting...";
var totalFormat = d3.format(",.1s");
var label = function(d) { return d.label; };
var value = function(d) { return d.value; };
//D3 helper function to create colors from an ordinal scale
var color = d3.scale.category20();
function chart(selection) {
//D3 helper function to draw arcs, populates parameter "d" in path object
var arc = d3.svg.arc()
.startAngle(function(d){ return d.startAngle; })
.endAngle(function(d){ return d.endAngle; })
.innerRadius(innerRadius)
.outerRadius(radius);
//D3 helper function to populate pie slice parameters from array data
var donut = d3.layout.pie().value(value);
selection.each(function(data) {
///////////////////////////////////////////////////////////
// CREATE VIS & GROUPS ////////////////////////////////////
///////////////////////////////////////////////////////////
var vis = d3.select(this).selectAll("svg").data([[]]);
vis.attr("width", width)
.attr("height", height);
var gEnter = vis.enter()
.append("svg");
//GROUP FOR ARCS/PATHS
gEnter.append("svg:g")
.attr("class", "arc");
//GROUP FOR LABELS
gEnter.append("svg:g")
.attr("class", "label_group");
//GROUP FOR CENTER TEXT
gEnter.append("svg:g")
.attr("class", "center_group");
//GROUP FOR ARCS/PATHS
var arc_group = vis.select(".arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
//GROUP FOR LABELS
var label_group = vis.select(".label_group")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
//GROUP FOR CENTER TEXT
var center_group = vis.select(".center_group")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
//PLACEHOLDER GRAY CIRCLE
var paths = arc_group.append("svg:circle")
.attr("fill", "#EFEFEF")
.attr("r", radius);
///////////////////////////////////////////////////////////
// CENTER TEXT ////////////////////////////////////////////
///////////////////////////////////////////////////////////
//WHITE CIRCLE BEHIND LABELS
var whiteCircle = center_group.append("svg:circle")
.attr("fill", "white")
.attr("r", innerRadius);
// "TOTAL" LABEL
var totalLabel = center_group.append("svg:text")
.attr("class", "label")
.attr("dy", -15)
.attr("text-anchor", "middle") // text-align: right
.text(totalTitleText);
//TOTAL TRAFFIC VALUE
var totalValue = center_group.append("svg:text")
.attr("class", "total")
.attr("dy", 7)
.attr("text-anchor", "middle") // text-align: right
.text(waitingLabelText);
//UNITS LABEL
var totalUnits = center_group.append("svg:text")
.attr("class", "units")
.attr("dy", 21)
.attr("text-anchor", "middle") // text-align: right
.text(totalLabelText);
if (data.length > 0) {
var totalValueNumber = d3.sum(data, value);
var oldData = arc_group.selectAll("path").data();
filteredData = donut(data);
// Ensure that the original data is added back to the filtered data
filteredData.forEach(function(elem) {
elem.label = label(elem.data);
});
var transformFunction = function(d) {
return "translate(" + Math.cos(((d.startAngle+d.endAngle - Math.PI)/2)) * (radius+textOffset) + "," +
Math.sin((d.startAngle+d.endAngle - Math.PI)/2) * (radius+textOffset) + ")";
};
var textAnchor = function(d) {
var averageDegrees = (d.startAngle + d.endAngle) / 2 * 180 / Math.PI;
if (averageDegrees > 359) {
return "middle";
} else if ((d.startAngle+d.endAngle)/2 < Math.PI) {
return "beginning";
} else {
return "end";
}
}
// REMOVE PLACEHOLDER CIRCLE
arc_group.selectAll("circle").remove();
totalValue.text(totalFormat(totalValueNumber));
//DRAW ARC PATHS
var paths = arc_group.selectAll("path")
.data(filteredData);
paths.enter()
.append("svg:path")
paths
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(tweenDuration)
.attrTween("d", pieTween(oldData));
paths.exit()
.transition()
.duration(tweenDuration)
.attrTween("d", removePieTween)
.remove();
//DRAW TICK MARK LINES FOR LABELS
var lines = label_group.selectAll("line")
.data(filteredData);
lines.enter()
.append("svg:line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", -radius - 3)
.attr("y2", -radius - 8)
.attr("stroke", "gray")
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines
.transition()
.duration(tweenDuration)
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines.exit()
.remove();
//DRAW LABELS WITH PERCENTAGE VALUES
var valueLabels = label_group.selectAll("text.value").data(filteredData)
valueLabels.enter()
.append("svg:text")
.attr("class", "value")
valueLabels
.attr("dy", function(d){
if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
return 5;
} else {
return -7;
}
})
.attr("transform", transformFunction)
.attr("text-anchor", textAnchor)
.text(function(d) {
var percentage = d.value / totalValueNumber * 100;
if (percentage < 0.1) {
return "< 0.1%";
} else {
return percentage.toFixed(1) + "%";
}
})
.transition()
.duration(tweenDuration)
.attrTween("transform", textTween(oldData));
valueLabels.exit()
.remove();
//DRAW LABELS WITH ENTITY NAMES
var nameLabels = label_group.selectAll("text.units").data(filteredData);
nameLabels.enter()
.append("svg:text")
.attr("class", "units")
nameLabels
.attr("dy", function(d){
if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
return 17;
} else {
return 5;
}
})
.attr("text-anchor", textAnchor)
.attr("transform", transformFunction)
.text(function(d) { return d.label; })
.transition()
.duration(tweenDuration)
.attrTween("transform", textTween(oldData));
nameLabels.exit()
.remove();
}
})
///////////////////////////////////////////////////////////
// FUNCTIONS //////////////////////////////////////////////
///////////////////////////////////////////////////////////
// Interpolate the arcs in data space.
function pieTween(oldPieData) {
return function(d, i) {
var s0;
var e0;
if(oldPieData[i]){
s0 = oldPieData[i].startAngle;
e0 = oldPieData[i].endAngle;
} else if (!(oldPieData[i]) && oldPieData[i-1]) {
s0 = oldPieData[i-1].endAngle;
e0 = oldPieData[i-1].endAngle;
} else if(!(oldPieData[i-1]) && oldPieData.length > 0){
s0 = oldPieData[oldPieData.length-1].endAngle;
e0 = oldPieData[oldPieData.length-1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});
return function(t) {
var b = i(t);
return arc(b);
};
}
}
function removePieTween(d, i) {
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});
return function(t) {
var b = i(t);
return arc(b);
};
}
function textTween(oldPieData) {
return function(d, i) {
var a;
if(oldPieData[i]){
a = (oldPieData[i].startAngle + oldPieData[i].endAngle - Math.PI)/2;
} else if (!(oldPieData[i]) && oldPieData[i-1]) {
a = (oldPieData[i-1].startAngle + oldPieData[i-1].endAngle - Math.PI)/2;
} else if(!(oldPieData[i-1]) && oldPieData.length > 0) {
a = (oldPieData[oldPieData.length-1].startAngle + oldPieData[oldPieData.length-1].endAngle - Math.PI)/2;
} else {
a = 0;
}
var b = (d.startAngle + d.endAngle - Math.PI)/2;
var fn = d3.interpolateNumber(a, b);
return function(t) {
var val = fn(t);
return "translate(" + Math.cos(val) * (radius+textOffset) + "," + Math.sin(val) * (radius+textOffset) + ")";
};
}
}
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.radius = function(_) {
if (!arguments.length) return radius;
radius = _;
return chart;
};
chart.innerRadius = function(_) {
if (!arguments.length) return innerRadius;
innerRadius = _;
return chart;
};
chart.textOffset = function(_) {
if (!arguments.length) return textOffset;
textOffset = _;
return chart;
};
chart.tweenDuration = function(_) {
if (!arguments.length) return tweenDuration;
tweenDuration = _;
return chart;
};
chart.label = function(_) {
if (!arguments.length) return label;
label = _;
return chart;
};
chart.name = function(_) {
if (!arguments.length) return name;
name = _;
return chart;
};
chart.value = function(_) {
if (!arguments.length) return value;
value = _;
return chart;
};
chart.totalTitle = function(_) {
if (!arguments.length) return totalTitleText;
totalTitleText = _;
return chart;
}
chart.totalLabel = function(_) {
if (!arguments.length) return totalLabelText;
totalLabelText = _;
return chart;
};
chart.totalFormat = function(_) {
if (!arguments.length) return totalFormat;
totalFormat = _;
return chart;
};
chart.waitingLabel = function(_) {
if (!arguments.length) return waitingLabelText;
waitingLabelText = _;
return chart;
};
return chart;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment