Skip to content

Instantly share code, notes, and snippets.

@svenhakvoort
Forked from mbostock/.block
Created April 4, 2018 18:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save svenhakvoort/91fa3f6538b78278d08c824c3456d747 to your computer and use it in GitHub Desktop.
Save svenhakvoort/91fa3f6538b78278d08c824c3456d747 to your computer and use it in GitHub Desktop.
Hierarchical Bar Chart
license: gpl-3.0

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.

<!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>
<?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