Skip to content

Instantly share code, notes, and snippets.

@carlvlewis
Last active July 19, 2017 16:56
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 carlvlewis/6bba1fcb51ec3fdccb1da0e0a92832f1 to your computer and use it in GitHub Desktop.
Save carlvlewis/6bba1fcb51ec3fdccb1da0e0a92832f1 to your computer and use it in GitHub Desktop.
Responsive d3v4 treemap with hierarchical zoom
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="author" content="@carlvlewis">
<!--This is so bootstrap knows how to handle viewports hassle-free-->
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>City of Savannah Expenses, 2017</title>
<link href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/150248/squaire.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://openbudgetsac.org/css/style.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.5/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/150248/squaire.js"></script>
<style>
html{
overflow-x:hidden;
}
#chart {
width: 100%;
margin-top:-80px;
height: 460px;
position: relative;
pointer-events: all;
font-family: helvetica,arial, sans-serif;
}
#legend {
width: 100%;
height: 20px;
margin: 25px auto;
position: relative;
margin-left: 20%;
float: center;
text-align: center;
}
h1,h2,h3,h4,h5, p, em {
font-family: helvetica, arial, sans-serif;
}
text {
pointer-events: all;
}
.alignleft {
float: left;
}
.alignright {
float: right;
}
.grandparent text {
font-weight: bold;
font-size: 22px;
}
rect {
stroke: #fff;
stroke-width: 1.5px;
}
rect.parent,
.grandparent rect {
stroke-width: 1.5px;
}
.grandparent rect {
fill: #bbb;
text-align: center;
}
.grandparent:hover rect {
fill: #f0f0f0;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.child {
opacity: 1;
}
.children rect.parent {
opacity: 1;
}
.children:hover rect.child {
opacity: 1;
stroke-width: 1.5px;
}
.children:hover rect.parent {
opacity: .5;
}
.legend rect {
stroke-width: 0px;
}
.legend text {
text-anchor: middle;
font-size: 11px;
fill: black;
}
.textdiv { /* text in the boxes */
font-size: 13px;
padding: 5px;
}
div.tooltip {
position: absolute;
text-align: center;
width: 30%;
height: 285px;
line-height: 18px;
padding: 2px;
font-size: 14px;
font-weight: normal;
background: rgba(0, 0, 0, 0.8);
border: 1px;
border-radius: 8px;
color: rgba(255, 255, 255, .95);
pointer-events: none;
}
h5.center {
text-align: center;
}
</style></head>
<body>
<script>
function ZoomableTreeMap() {
var dollarformat = d3.format("$0,000")
var pctformat = d3.format("00.0%")
var margin = {top: 40, right: 0, bottom: 0, left: 0},
width = 960,
height = 460 - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
formatLegend = d3.format(",%"),
colorDomain = [-5000, -500, -20, -5, 0, 5, 20, 500, 5000],
colorRange = ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac", "#053061"],
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var color = d3.scale.linear()
.domain(colorDomain)
.range(colorRange);
function colorIncrements(d){
return (30)/12*d + -15;
}
var treemap = d3.layout.treemap()
.value(function(d) {return d.amount2017})
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { console.log(a.amount2017); return a.amount2017 - b.amount2017; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
/////////////
/// LEGEND //
/////////////
var legend = d3.select("#legend").append("svg")
.attr("width", '100%')
.attr("height", 40)
.attr('class', 'legend')
.attr("x", 12)
.selectAll("g")
.data([0,2,4,6,8,10,12])
.enter()
.append('g');
legend.append("rect")
.attr("x", function(d){return margin.left + d * 40})
.attr("y", 0)
.attr("fill", function(d) {return color(colorIncrements(d))})
.attr('width', '40px')
.attr('height', '8px')
legend.append("text")
.text(function(d){return formatLegend(colorIncrements(d)/100)})
.attr('y', 30)
.attr('x', function(d){return margin.left + d * 40 + 20});
/// END LEGEND ///
var svg = d3.select("#chart").append("svg")
.attr("width", '100%')
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var aspect = width / height,
chart = d3.select("svg");
d3.select("html")
.on("resize", function() {
var targetWidth = chart.node().getBoundingClientRect().width;
chart.attr("width", targetWidth);
chart.attr("height", targetWidth / aspect);
});
var div = d3.select("#chart").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", "100%")
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 10 - margin.top)
.attr("dy", ".75em");
//////////////////
/// Second chart
//////////////////
var lgsize=100
var smsize=50
d3.json("https://gist.githubusercontent.com/carlvlewis/d0b2cbb6b214c7ca658a89d3c2fd6bfa/raw/e290e0947c3998b19a4a9a0facb9547962c1b5e6/sav_budget_expenses_json", function(root) {
initialize(root);
accumulate(root);
accum2(root);
layout(root);
display(root);
function initialize(root) {
root.x = root.y = 0;
root.dx = document.getElementById("chart").getBoundingClientRect().width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
console.log(d.amount2017);
return (d._children = d.children)
? d.amount2017 = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.amount2017;
}
function accum2(d) {
console.log(d.amount2016);
return (d._children = d.children)
? d.amount2016= d.children.reduce(function(p, v) { return p + accum2(v); }, 0)
: d.amount2016
}
function layout(d) {
if (d._children) {
treemap.nodes({_children: d._children});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
// filter
//var data=d._children.filter(function(d) { return d.amount2017 >= 20000000; });
//added for small box
grandparent
.datum(d.parent)
.on("click", transition_out)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) { return d._children; })
.classed("children", true)
.on("click", transition_in);
//
g.on("mouseover", function(d) {
div.transition().duration(50)
.style("opacity", 1)
.style("height", (d.desc!=null ? (Math.ceil(d.desc.length/75)*19+107) + "px" : "107px"))
div.html("<br/>"+d.name + "<br/>" + "2017 : "+ dollarformat(d.amount2017) + " <br/> 2016 : " +dollarformat(d.amount2016) +"<br/> Percentage change: "+ (((d.amount2017-d.amount2016)/d.amount2016)*100).toFixed(2).toString()+"% <br/>"+(d.desc!=null ? d.desc : ""))
.style("left", Math.max(Math.min((d3.event.pageX - 350),430),20) + "px")
.style("top", Math.min((d3.event.pageY-70),200) + "px");
})
.on("mouseout", function() {
div.transition().duration(50)
.style("opacity", 0);
})
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) { return formatNumber(d.amount2017); });
g.append("foreignObject")
.call(rect)
.attr("class","foreignobj")
.append("xhtml:div")
.attr("dy", ".75em")
.html(function(d) { if(parseInt( x(d.x + d.dx) - x(d.x) ) >25 ) return d.name + ": " + dollarformat(d.amount2017); else return " "; })
.attr("class","textdiv"); //textdiv class allows us to style the text easily with CSS
function transition_in(d) {
if (transitioning || !d) return;
transitioning = true;
arrow=grandparent.append("text") // append arrow
.attr('font-family', 'FontAwesome')
.attr("class", "svg-icon")
.attr("x", 915)
.attr("y", 25 - margin.top)
.text("\uF106")
.style("opacity", 0);
arrow.transition().duration(500) // fade in arrow
.style("opacity", 1)
g.selectAll(".child")
.data(function(d) { return d.parent._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll(".textdiv").style("color", "rgba(0, 0, 0, 0)"); /* added */
// Transition to the new view.
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
t1.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".textdiv").style("display", "block"); /* added */
g2.transition().delay(650).duration(100).selectAll(".textdiv").style("color", "rgba(0, 0, 0, 1)"); /* added */
t1.selectAll(".foreignobj").call(foreign); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
function transition_out(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll(".textdiv").style("color", "rgba(0, 0, 0, 0)"); /* added */
// Transition to the new view.
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
t1.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".textdiv").style("display", "block"); /* added */
g2.transition().delay(650).duration(100).selectAll(".textdiv").style("color", "rgba(0, 0, 0, 1)"); /* added */
t1.selectAll(".foreignobj").call(foreign); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); })
.attr("fill", function(d){return isNaN((parseFloat(d.amount2017)/parseFloat(d.amount2016)-1)*100) ? color(0) : color((parseFloat(d.amount2017)/parseFloat(d.amount2016)-1)*100);});
}
function foreign(foreign){ /* added */
foreign.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); });
}
function name(d) {
return d.parent
? d.name + ": "+ dollarformat(d.amount2017)
: d.name + ": "+ dollarformat(d.amount2017);
}
});
}
ZoomableTreeMap();
$(window).resize(function () {
var div = $("#chart").html('');
var div = $("#legend").html('');
ZoomableTreeMap();
});
</script>
<div id="chart"></div>
<h5 class="center">% Change from Previous Year</div>
<div id="legend"></div></body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment