Skip to content

Instantly share code, notes, and snippets.

@tomalrussell
Last active September 29, 2015 17:36
Show Gist options
  • Save tomalrussell/2c8c5e24bf614f80e3bc to your computer and use it in GitHub Desktop.
Save tomalrussell/2c8c5e24bf614f80e3bc to your computer and use it in GitHub Desktop.
Sunburst with controls
/**
* Sunburst Chart
* based on the Bilevel Partition by Mike Bostock http://bl.ocks.org/mbostock/5944371
* and the original Sunburst design by John Stasko http://www.cc.gatech.edu/gvu/ii/sunburst/
*/
function sunburstChart(filterCallback){
var margin = {top: 200, right: 400, bottom: 200, left: 400},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 50;
var hue = d3.scale.category10();
var luminance = d3.scale.sqrt()
.domain([0, 20])
.clamp(true)
.range([100, 50]);
var percentify = d3.format(".2%");
var partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, radius]);
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1); });
var outerArc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return radius / 3 * (d.depth + 1); })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1); });
function chart(selection) {
var chart_this = this;
selection.each(function(data) {
var svg = d3.select(this).selectAll("svg").data([data]);
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("g").classed("labels", true);
gEnter.append("path").attr("d","M-5 0 L5 8 L5 -8Z");
svg.attr("width", margin.left + margin.right)
.attr("height", margin.top + margin.bottom);
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.size; })
.nodes(data)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition
.children(function(d, depth) { return depth < 2 ? d._children : null; })
.value(function(d) { return d.sum; });
var labelsWrap = svg.select(".labels");
var labels = labelsWrap.selectAll(".label")
.data( partition.nodes(data).slice(1).filter(function(item){return item.depth === 1 && item.sum > 0;}), function(d) { return d.key; } );
labels.enter().append("g")
.style("opacity", 0)
.classed("label", true)
.each(appendLabel);
labels.each(updateLabel);
labels.each(function(d) { this._current = arc.centroid(updateArc(d)); });
labels.exit().transition()
.style("opacity", 0)
.remove();
labels.transition()
.style("opacity", 1);
gEnter.append("circle")
.attr("r", radius / 3)
.style({fill: "#fff"})
.on("click", zoomOut);
var center = g.select("circle")
.datum(data);
var segments = g.selectAll(".segment")
.data(partition.nodes(data).slice(1), function(d) { return key(d); });
var segmentsEnter = segments.enter().append("path")
.classed("segment", true)
.attr("d", arc)
.style("fill", function(d) { return d.fill; });
segmentsEnter.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn);
segments.exit().remove();
d3.transition().duration(750).each(function() {
segments.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
segments
.classed("fade-out", function(d) { return d.fadeOut; });
});
function focus(p, el){
filterCallback(p);
if (!p.children){
segments.classed("fade-out", true);
d3.select(el).classed("fade-out", false);
}
}
function unfocus(p, el){
center.classed("fade-out", false);
filterCallback(p.parent);
segments.classed("fade-out", false);
}
function zoomIn(p) {
focus(p, this);
center.classed("fade-out", true);
if (p.depth > 1) p = p.parent;
if (!p.children){
return;
}
zoom(p, p);
}
function zoomOut(p) {
unfocus(p, this);
if(typeof(p) === 'undefined' || !p.parent){
return;
}
zoom(p.parent, p);
}
// Zoom to the specified new root.
var zoom = chart_this.zoom = function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
if( p.key > d.key ){
return {depth: d.depth - 1, x: 0, dx: 0};
} else if ( p.key < d.key ){
return {depth: d.depth - 1, x: 2 * Math.PI, dx: 0};
} else {
return {depth: 0, x: 0, dx: 2 * Math.PI};
}
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if ( root === p ){
enterArc = outsideArc;
exitArc = insideArc;
outsideAngle.range([p.x, p.x + p.dx]);
} else {
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
enterArc = insideArc;
exitArc = outsideArc;
outsideAngle.range([p.x, p.x + p.dx]);
}
segments = g.selectAll(".segment").data(partition.nodes(root).slice(1), function(d) { return d.key; });
labels = labelsWrap.selectAll(".label").data( partition.nodes(root).slice(1).filter(function(item){return item.depth === 1;}), function(d) { return d.key; } );
d3.transition().duration(750).each(function() {
labels.exit().transition()
.style("opacity", 0)
.remove();
labels.enter().append("g")
.style("opacity", 0)
.classed("label", true)
.each(appendLabel);
labels.transition().delay(500)
.style("opacity",1);
segments.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
segments.enter().append("path")
.classed("segment", true)
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.on("click", zoomIn)
.each(function(d) { this._current = enterArc(d); });
segments.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
};
});
}
function appendLabel(d,i){
var centroid = outerArc.centroid(d);
var label = labelPolyline(centroid);
var el = d3.select(this);
appendLabelGuts(d,i,centroid,label,el);
}
function appendLabelGuts (d,i,centroid,label,el) {
el.append("polyline")
.attr("points", label.points)
.style({stroke: "#222", "stroke-width":"0.5px", fill: "none"});
el.append("svg:text")
.classed("text1",true)
.attr("x", label.end[0])
.attr("y", label.end[1])
.attr("dy", "0.35em")
.attr("dx", (centroid[0]>0)? "0.25em": "-0.25em")
.attr("text-anchor", (centroid[0]>0)? "start": "end")
.text(function(d){
return d.name.split("<br>")[0];
});
if(d.name.contains("<br>") ){
el.append("svg:text")
.classed("text2",true)
.attr("x", label.end[0])
.attr("y", label.end[1])
.attr("dy", "1.35em")
.attr("dx", (centroid[0]>0)? "0.25em": "-0.25em")
.attr("text-anchor", (centroid[0]>0)? "start": "end")
.text(function(d){
return d.name.split("<br>")[1];
});
}
}
function labelPolyline(centroid){
var inflection = [0,0];
var end = [0,0];
var xDist = radius+20;
if(centroid[0] > 0){
// right of center
inflection[0] = xDist;
end[0] = inflection[0] + 10;
} else {
// left of center
inflection[0] = -xDist;
end[0] = inflection[0] - 10;
}
if(centroid[0] < 0 && centroid[1] > 0 || centroid[0] > 0 && centroid[1] < 0){
inflection[1] = centroid[1] + (inflection[0]-centroid[0])*Math.tan((-1/6)*Math.PI);
} else {
inflection[1] = centroid[1] + (inflection[0]-centroid[0])*Math.tan((1/6)*Math.PI);
}
end[1] = inflection[1];
return {
points: centroid[0] + "," + centroid[1] + " " + inflection[0] + "," + inflection[1] + " " + end[0] + "," + end[1],
end: end
};
}
function updateLabel(d,i){
var centroid = outerArc.centroid(d);
var label = labelPolyline(centroid);
var el = d3.select(this);
updateLabelGuts(d,i,centroid,label,el);
}
function updateLabelGuts (d,i,centroid,label,el) {
el.select("polyline")
.attr("points", label.points)
.style({stroke: "#222", "stroke-width":"0.5px", fill: "none"});
el.select(".text1").attr("x", label.end[0])
.attr("y", label.end[1])
.attr("dy", "0.35em")
.attr("dx", (centroid[0]>0)? "0.25em": "-0.25em")
.attr("text-anchor", (centroid[0]>0)? "start": "end");
el.select(".text2").attr("x", label.end[0])
.attr("y", label.end[1])
.attr("dy", "1.35em")
.attr("dx", (centroid[0]>0)? "0.25em": "-0.25em")
.attr("text-anchor", (centroid[0]>0)? "start": "end");
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function key(d) {
// var k = [], p = d;
// while (p.depth){
// k.push(p.name);
// p = p.parent;
// }
// return k.reverse().join(".");
// reyinig different key function ( might break insideArc )
return d.name.split(":")[0];
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.name));
var i;
if(d.depth <= 1){
if (typeof(d.children) !== "undefined"){
i = d.children.length + 1;
} else {
i = 1;
}
} else {
i = d.parent.children.indexOf(d) + 1;
}
c.l = luminance(i);
return c;
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
function formatName(name, size){
if(arguments.length == 2){
if(size < 1){
size = percentify(size);
}
return name + " ("+size+")";
} else {
return name;
}
}
/**
* Export 'public' functions or variables
*/
chart.hue = function(value) {
if (!arguments.length) return hue;
hue = value;
return chart;
};
chart.fill = function(value) {
if (!arguments.length) return fill;
fill = value;
return chart;
};
chart.margin = function(value) {
if (!arguments.length) return margin;
margin = value;
return chart;
};
chart.radius = function(value) {
if (!arguments.length) return radius;
radius = value;
return chart;
};
return chart;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sunburst with controls</title>
<style>
.fade-out{
opacity:0.3
}
circle,
path{
cursor:pointer;
-webkit-transition:0.3s opacity;
transition:0.3s opacity
}
</style>
</head>
<body>
<select name="areas" id="areas">
<option value="root">all areas</option>
<option value="A">A</option>
<optgroup>
<option value="A1">A1</option>
<option value="A2">A2</option>
<option value="A3">A3</option>
</optgroup>
<option value="B">B</option>
<optgroup>
<option value="B1">B1</option>
<option value="B2">B2</option>
<option value="B3">B3</option>
</optgroup>
<option value="C">C</option>
<optgroup>
<option value="C1">C1</option>
<option value="C2">C2</option>
<option value="C3">C3</option>
</optgroup>
<option value="D">D</option>
<optgroup>
<option value="D1">D1</option>
<option value="D2">D2</option>
<option value="D3">D3</option>
</optgroup>
</select>
<figure class="chart-wrap">
<figcaption>Areas A-D</figcaption>
</figure>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="./d3.sunburst.js"></script>
<script>
var areas = {
name: "root",
children: [
{
name: "A",
size: 20,
children: [
{
name: "A1",
size: 5,
},
{
name: "A2",
size: 14,
},
{
name: "A3",
size: 1,
},
]
},
{
name: "B",
size: 50,
children: [
{
name: "B1",
size: 14,
},
{
name: "B2",
size: 6,
},
{
name: "B3",
size: 40,
},
]
},
{
name: "C",
size: 10,
children: [
{
name: "C1",
size: 4,
},
{
name: "C2",
size: 4,
},
{
name: "C3",
size: 2,
},
]
},
{
name: "D",
size: 20,
children: [
{
name: "D1",
size: 9,
},
{
name: "D2",
size: 4,
},
{
name: "D3",
size: 7,
},
]
}
]
};
var chart = sunburstChart(function(d){
if(!d) return;
d3.select("#areas").property("value", d.name);
console.log(d);
});
function renderChart(areas){
d3.select(".chart-wrap").datum(areas).call(chart);
}
d3.select("#areas").on("change",function(){
var area_selected = d3.select(this).property("value");
var areas_to_render = areas;
for (var i = areas.children.length - 1; i >= 0; i--) {
var child = areas.children[i];
child.fadeOut = false;
for (var j = child.children.length - 1; j >= 0; j--) {
child.children[j].fadeOut = false;
}
}
if(areas.name !== area_selected){
for (var k = areas.children.length - 1; k >= 0; k--) {
var child = areas.children[k];
child.fadeOut = false;
for (var l = child.children.length - 1; l >= 0; l--) {
child.children[l].fadeOut = false;
}
if(area_selected === child.name){
areas_to_render = child;
} else {
for (var m = child.children.length - 1; m >= 0; m--) {
var grandchild = child.children[m];
if(area_selected === grandchild.name){
for (var n = child.children.length - 1; n >= 0; n--) {
child.children[n].fadeOut = true;
}
grandchild.fadeOut = false;
areas_to_render = child;
}
}
}
}
}
renderChart(areas_to_render);
});
renderChart(areas);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment