Sunburst as in Mike Bostock's block
The select menu should control the zoom state of the graph.
Sunburst as in Mike Bostock's block
The select menu should control the zoom state of the graph.
/** | |
* 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> |