This bar chart visualizes hierarchical data using D3. Each blue bar represents a folder, whose length encodes the total size of all files in that folder (and all subfolders). Clicking on a bar dives into that folder, while clicking on the background bubbles back up to the parent folder. The effect is similar to a zoomable partition layout, though in a more conventional display.
-
-
Save svenhakvoort/91fa3f6538b78278d08c824c3456d747 to your computer and use it in GitHub Desktop.
Hierarchical Bar Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
text { | |
font: 10px sans-serif; | |
} | |
rect.background { | |
fill: white; | |
} | |
.axis { | |
shape-rendering: crispEdges; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
} | |
#container { | |
overflow-y: scroll; | |
height: 500px; | |
} | |
</style> | |
<body> | |
<div id="container"></div> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script> | |
var firstData; | |
var secondData; | |
var firstCol = "#56C0E3"; | |
var secondCol = "#F02E51"; | |
var minHeight = 150; | |
var barSpaceHeight = 1.5; | |
$.getJSON( | |
"https://codepen.io/svenhakvoort/pen/eMjORb.js", | |
function(taxData) { | |
firstData = taxData; | |
}); | |
$.getJSON( | |
"https://codepen.io/svenhakvoort/pen/vRjpGo.js", | |
function(taxData) { | |
secondData = taxData; | |
combineData(); | |
}); | |
function combineData() { | |
$.each(firstData["children"], function(index, node) {addColor(node, firstCol)}); | |
$.each(secondData["children"], function(index, node) {addColor(node, secondCol)}); | |
firstData["children"] = firstData["children"].concat(secondData["children"]); | |
sortRecursive(firstData); | |
plotResults(firstData); | |
} | |
function sortNodes(a, b) { | |
if (a["name"] == b["name"]) { | |
return (a["color"] < b["color"] ) ? 1 :(a["color"] > b["color"] ) ? -1 : 0; | |
} else { | |
return (a["name"] < b["name"] ) ? -1 :(a["name"] > b["name"] ) ? 1 : (a["name"] >= b["name"] ) ? 0 : NaN; | |
} | |
} | |
function sortRecursive(node) { | |
if (node["children"].length > 0) { | |
node["children"].sort(sortNodes); | |
$.each(node["children"], function(index, child) {sortRecursive(child); }); | |
} | |
} | |
function addColor(node, color, remove=0) { | |
node["color"] = color | |
if (remove != 0) { | |
node["name"] = ""; | |
} | |
if (node["children"].length > 0) { | |
$.each(node["children"], function(index, node) { | |
addColor(node, color); | |
}); | |
} | |
} | |
function getParent(node) { | |
if (node.parent) { | |
node = getParent(node) | |
} | |
return node; | |
} | |
function combineChilds(node1, node2) { | |
var children1 = node1.children.sort(sortNodes); | |
var children2 = node2.children.sort(sortNodes); | |
for (var i = 0; i < children1.length-1; i++) { | |
if (children1[i].name == children1[i+1].name && typeof children1[i].children !== 'undefined' && typeof children2[i].children !== 'undefined') { | |
var allChildren = children1[i].children.concat(children1[i+1].children) | |
children1[i].children = allChildren; | |
children2[i].children = allChildren; | |
children1[i+1].children = allChildren; | |
children2[i+1].children = allChildren; | |
combineChilds(children1[i], children1[i+1]); | |
} | |
} | |
} | |
function combineTwoGroups(node) { | |
var children = node.children; | |
for (var i = 0; i < children.length-1; i++) { | |
if (children[i].name == children[i+1].name) { | |
var allChildren = children[i].children.concat(children[i+1].children) | |
children[i].children = allChildren; | |
children[i+1].children = allChildren; | |
combineChilds(children[i], children[i+1]); | |
} | |
} | |
} | |
function plotResults(taxData) { | |
var margin = {top: 30, right: 120, bottom: 0, left: 120}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var x = d3.scale.linear() | |
.range([0, width]); | |
var barHeight = 20; | |
var color = d3.scale.ordinal() | |
.range(["steelblue", "#ccc"]); | |
var duration = 750, | |
delay = 25; | |
var partition = d3.layout.partition() | |
.value(function(d) { return d.size; }).sort(null); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("top"); | |
var svg = d3.select("#container").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
//The data for our line | |
var lineData = [ { "x": 50, "y": 0}, { "x": 60, "y": 10}, | |
{ "x": 20, "y": 50}, { "x": 60, "y": 90}, | |
{ "x": 50, "y": 100}, { "x": 0, "y": 50}, {"x": 50, "y": 0}]; | |
//This is the accessor function we talked about above | |
var lineFunction = d3.svg.line() | |
.x(function(d) { return d.x; }) | |
.y(function(d) { return d.y; }) | |
.interpolate("linear"); | |
var group = svg.append("g").attr("transform", "translate(" + 750 + "," + 0 + ")"); | |
//The line SVG Path we draw | |
group.append("rect").attr("class", "background") | |
.attr("width", 100) | |
.attr("height", 100) | |
.on("click", up).style("cursor", "pointer"); | |
var lineGraph = group.append("path") | |
.attr("d", lineFunction(lineData)) | |
.attr("stroke", "black") | |
.attr("stroke-width", 1) | |
.attr("fill", "none"); | |
svg.append("g") | |
.attr("class", "x axis"); | |
svg.append("g") | |
.attr("class", "y axis") | |
.append("line") | |
.attr("y1", "100%"); | |
partition.nodes(taxData); | |
x.domain([0, taxData.value]).nice(); | |
combineTwoGroups(taxData); | |
down(taxData, 0); | |
function down(d, i) { | |
if (!d.children || this.__transition__) return; | |
var end = duration + d.children.length * delay; | |
// Mark any currently-displayed bars as exiting. | |
var exit = svg.selectAll(".enter") | |
.attr("class", "exit"); | |
// Entering nodes immediately obscure the clicked-on bar, so hide it. | |
exit.selectAll("rect").filter(function(p) { return p === d; }) | |
.style("fill-opacity", 1e-6); | |
var newHeight = barHeight*1.2*d.children.length + margin.top + margin.bottom | |
d3.select("#container").select("svg").attr("height", (newHeight < minHeight) ? minHeight : newHeight); | |
// Enter the new bars for the clicked-on data. | |
// Per above, entering bars are immediately visible. | |
var enter = bar(d) | |
.attr("transform", stack(i)) | |
.style("opacity", 1); | |
// Have the text fade-in, even though the bars are visible. | |
// Color the bars as parents; they will fade to children if appropriate. | |
enter.select("text").style("fill-opacity", 1e-6); | |
enter.select("rect").style("fill", function(d) { | |
if (d.color) { return d.color; | |
} else {return color(!!d.children); }}); | |
// Update the x-scale domain. | |
x.domain([0, d3.max(d.children, function(d) { return d.value; })]).nice(); | |
// Update the x-axis. | |
svg.selectAll(".x.axis").transition() | |
.duration(duration) | |
.call(xAxis); | |
// Transition entering bars to their new position. | |
var enterTransition = enter.transition() | |
.duration(duration) | |
.delay(function(d, i) { return i * delay; }) | |
.attr("transform", function(d, i) { return "translate(0," + (barHeight * i * barSpaceHeight - ((d.color == firstCol) ? 10 : 0)) + ")"; }); | |
// Transition entering text. | |
enterTransition.select("text") | |
.style("fill-opacity", 1); | |
// Transition entering rects to the new x-scale. | |
enterTransition.select("rect") | |
.attr("width", function(d) {return x(d.value); }) | |
.style("fill", function(d) { | |
if (d.color) { return d.color; | |
} else {return color(!!d.children); }}); | |
// Transition exiting bars to fade out. | |
var exitTransition = exit.transition() | |
.duration(duration) | |
.style("opacity", 1e-6) | |
.remove(); | |
// Transition exiting bars to the new x-scale. | |
exitTransition.selectAll("rect") | |
.attr("width", function(d) { return x(d.value); }); | |
// Rebind the current node to the background. | |
svg.select(".background") | |
.datum(d) | |
.transition() | |
.duration(end); | |
d.index = i; | |
} | |
function up(d) { | |
var duration = 500; | |
if (!d.parent || this.__transition__) return; | |
var end = 500; | |
var newHeight = barHeight*1.2*d.parent.children.length + margin.top + margin.bottom | |
d3.select("#container").select("svg").attr("height", (newHeight < minHeight) ? minHeight : newHeight); | |
// Mark any currently-displayed bars as exiting. | |
var exit = svg.selectAll(".enter") | |
.attr("class", "exit"); | |
// Enter the new bars for the clicked-on data's parent. | |
var enter = bar(d.parent) | |
.attr("transform", function(d, i) { return "translate(0," + (barHeight * i * barSpaceHeight - ((d.color == firstCol) ? 10 : 0)) + ")"; }) | |
.style("opacity", 1e-6); | |
// Color the bars as appropriate. | |
// Exiting nodes will obscure the parent bar, so hide it. | |
enter.select("rect") | |
.style("fill", function(d) { | |
if (d.color) { return d.color; | |
} else {return color(!!d.children); }}) | |
.filter(function(p) { return p === d; }) | |
.style("fill-opacity", 1e-6); | |
// Update the x-scale domain. | |
x.domain([0, d3.max(d.parent.children, function(d) { return d.value; })]).nice(); | |
// Update the x-axis. | |
svg.selectAll(".x.axis").transition() | |
.duration(duration) | |
.call(xAxis); | |
// Transition entering bars to fade in over the full duration. | |
var enterTransition = enter.transition() | |
.duration(end) | |
.style("opacity", 1); | |
// Transition entering rects to the new x-scale. | |
// When the entering parent rect is done, make it visible! | |
enterTransition.select("rect") | |
.attr("width", function(d) { return x(d.value); }) | |
.each("end", function(p) { if (p === d) d3.select(this).style("fill-opacity", null); }); | |
// Transition exiting bars to the parent's position. | |
var exitTransition = exit.selectAll("g").transition() | |
.duration(duration) | |
.delay(function(d, i) { return i * delay; }) | |
.attr("transform", stack(d.index)); | |
// Transition exiting text to fade out. | |
exitTransition.select("text") | |
.style("fill-opacity", 1e-6); | |
// Transition exiting rects to the new scale and fade to parent color. | |
exitTransition.select("rect") | |
.attr("width", function(d) { return x(d.value); }) | |
.style("fill", function(d) { | |
if (d.color) { return d.color; | |
} else {return color(!!d.children); }}); | |
// Remove exiting nodes when the last child has finished transitioning. | |
exit.transition() | |
.duration(duration) | |
.remove(); | |
// Rebind the current parent to the background. | |
svg.select(".background") | |
.datum(d.parent) | |
.transition() | |
.duration(end); | |
} | |
// Creates a set of bars for the given data node, at the specified index. | |
function bar(d) { | |
var bar = svg.insert("g", ".y.axis") | |
.attr("class", "enter") | |
.attr("transform", "translate(0,5)") | |
.selectAll("g") | |
.data(d.children) | |
.enter().append("g") | |
.style("cursor", function(d) { return !d.children ? null : "pointer"; }) | |
.on("click", down); | |
bar.append("text") | |
.attr("x", -6) | |
.attr("y", barHeight / 2) | |
.attr("dy", ".35em") | |
.style("text-anchor", "end") | |
.text(function(d) { return d.name; }); | |
bar.append("rect") | |
.attr("width", function(d) { return x(d.value); }) | |
.attr("height", barHeight); | |
return bar; | |
} | |
// A stateful closure for stacking bars horizontally. | |
function stack(i) { | |
var x0 = 0; | |
return function(d) { | |
var tx = "translate(" + x0 + "," + barHeight * i * 1.2 + ")"; | |
x0 += x(d.value); | |
return tx; | |
}; | |
} | |
}; | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="VcsDirectoryMappings"> | |
<mapping directory="" vcs="Git" /> | |
</component> | |
</project> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment