forked from qneutron's block: Zoomable Treemap with Heatmap Coloring
forked from anonymous's block: Zoomable Treemap with Heatmap Coloring
xxxxxxxxxx
<meta charset="utf-8">
<body>
<style>
#chart {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 9px;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect:hover {
fill-opacity: 0.4;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.hasChildren rect.parent,
.grandparent rect {
cursor: pointer;
}
.hasChildren rect.parent {
fill: none;
/*stroke: grey;
stroke-width: 1px;
stroke-opacity: 0.6;*/
}
.hasChildren:hover rect.child {
fill: #bbb;
}
#tooltip {
position: absolute;
min-width: 140px;
height: auto;
padding: 10px;
background-color: lightblue;
opacity: .9;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
pointer-events: none;
}
#tooltip.hidden {
display: none;
}
#tooltip p {
margin: 0;
font-family: sans-serif;
font-size: 12px;
line-height: 14px;
}
</style>
<p id="chart">
<script src="//d3js.org/d3.v4.js" charset="utf-8"></script></script>
<script>
/*
level_names example: ['level_0_title', 'level_1_title', 'level_2_title', ...]
measure_1 example: 'budget_usd'. Will be used for sizing the rects in the tree.
measure_2 example: 'actual_usd'. Can be used to compute a actual/budget ratio.
*/
function buildHierarchy(flat_data, level_names, measure_1, measure_2=null, measure_3=null, percent=false, numerator=null, denom=null, color_measure=null, tooltip_measures=null) {
var nest = d3.nest().rollup(function(leaves) {
var sums = {};
sums[measure_1] = d3.sum(leaves, l => l[measure_1]) ;
if (measure_2) {
sums[measure_2] = d3.sum(leaves, l => l[measure_2]);
}
if (measure_3) {
sums[measure_3] = d3.sum(leaves, l => l[measure_3]);
}
return sums;
});
// If first level is not a unique grandparent, create one!
var first_level = d3.map(flat_data, d => d[level_names[0]]).keys();
if (first_level.length > 1) {
nest = nest.key(function(d) {return 'All';});
}
// Add all level columns into nesting structure.
level_names.forEach(function(level) {
nest = nest.key(function(d) {return d[level]});
});
var data = nest.entries(flat_data)[0];
// Start the d3.hierarchy magic.
var root = d3.hierarchy(data, (d) => d.values)
// Computes a 'value' property for all nodes.
.sum((d) => d['value'] ? d['value'][measure_1] : 0)
.sort(function(a, b) { return b.value - a.value; });
if (measure_2) {
// Compute the second measure for all nodes, starting with leaves.
root.leaves().forEach(function(leaf) {
leaf[measure_2] = leaf.data.value[measure_2];
// Accumulate this second_measure up the tree.
leaf.ancestors().forEach(function(node, idx){
if (idx == 0) {return;} // excluding itself
node[measure_2] = leaf[measure_2] + (node[measure_2] || 0);
});
});
}
if (measure_3) {
// Compute the second measure for all nodes, starting with leaves.
root.leaves().forEach(function(leaf) {
leaf[measure_3] = leaf.data.value[measure_3];
// Accumulate this second_measure up the tree.
leaf.ancestors().forEach(function(node, idx){
if (idx == 0) {return;} // excluding itself
node[measure_3] = leaf[measure_3] + (node[measure_3] || 0);
});
});
}
// Prune the tree - remove repeated leaf nodes
prune(root);
// console.log(root);
function prune(node) {
if (!node.children) {
return;
}
if (node.children.length > 1) {
node.children.forEach(n => prune(n));
} else {
delete node.children;
}
}
// Compute the aggregate measure for all nodes, starting with leaves.
root.each(function(node) {
node[measure_1] = node.value;
if (percent) {
// Repeat the size_measure so that it is available for computation.
node['percent'] = (node[numerator] / node[denom]) || 0;
node.percent = Math.min(1, Math.max(0, node.percent));
}
if (color_measure) {
node['color_metric'] = node[color_measure];
}
});
return root;
}
function drawChart(hierData, color_measure=null, color_range=null, tooltip_measures=null) {
// Clear any existing svg elements
d3.selectAll('svg').remove();
var margin = {top: 32, right: 0, bottom: 0, left: 0},
width = window.innerWidth,
height = window.innerHeight - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
padding = 2,
transitioning;
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([0, height]);
// A function that computes x0, x1, y0, y1 for nodes in a given hierarchy.
const treemap = d3.treemap().size([width, height])
.paddingOuter(padding);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.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 grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("x", padding * 2)
.attr("y", -margin.top)
.attr("width", width - padding * 4)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
var color;
var color_accessor;
if (color_measure != null) {
color = d3.scaleLinear()
.range(color_range || ['#ffb300', '#ADD8E6', '#03A9F4'])
.domain(colorDomain());
color_accessor = function(d) { return color(d.color_metric); };
} else {
color = d3.scaleOrdinal().range(color_range || d3.schemeCategory20c);
color_accessor = function(d) { return color(d.data.key); };
}
treemap(hierData);
display(hierData);
function colorDomain() {
var root = hierData;
color_values = [];
if (!root.children) {
color_values = [root.color_metric];
return;
}
root.children.forEach(child => {
if (child.children && child.children.length) {
child.children.forEach(grandchild => {
color_values.push(grandchild.color_metric);
});
} else {
color_values.push(child.color_metric);
}
});
return [d3.min(color_values), d3.mean(color_values), d3.max(color_values)];
}
function display(d) {
var grandparent_color_metric = d.color_metric;
grandparent
// Attach zoom function -- works after 1st zoom, when node has 'parent'
.datum(d.parent)
.on("click", zoom)
.select("text")
// Name for grandparent is the hierarchical path
.text(name(d) + ": " + formatNumber(d.value));
grandparent.select('rect')
.style("fill", function(d) { return color(grandparent_color_metric); });
// g1 = main graphical element to hold the tree, inserted after the top g.
// g1 is "the grandparent", even though the top g holds the text about it.
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
// Within g1, create g's for the "parents" (children of grandparent)
var g = g1.selectAll("g")
.data(d.children)
.enter().append("g");
// Attach zoom function to to parents who have children.
g.filter(function(d) { return d.children; })
.classed("hasChildren", true)
.on("click", zoom);
// Within each parent g, add g's to hold rect + text for each child.
var children = g.selectAll(".child")
.data(function(d) { return d.children || [d]; })
.enter().append("g");
children.append("rect")
.attr("class", "child")
.call(rect)
.style("fill", color_accessor)
.on("mousemove", d => mousemove(d))
.on("mouseout", mouseout);
// .append("title")
// .text(function(d) {return d.data.key + " (" + formatNumber(d.value) + ")";});
children.append("text")
.attr("class", "ctext")
.text(function(d) {return d.data.key;})
.call(childrenText);
// Even though all children are rects, parents are also presented as rects.
g.append("rect")
.attr("class", "parent")
.call(rect);
var t = g.append("text")
.attr("class", "ptext")
.attr("dy", ".75em")
t.append("tspan")
.text(function(d) { return d.data.key; });
t.append("tspan")
.attr("dy", "1.0em")
.text(function(d) { return formatNumber(d.value); });
t.call(parentText);
// Draw/move child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) {return a.depth - b.depth;});
// FIXME: Move to private function
function zoom(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d);
// At this point in time, g1 with its rects are still in place.
// The parent that has just been promoted to grandparent now has 2 levels
// of rects visualized, but still positioned exactly where it was.
var t1 = g1.transition().duration(750);
var t2 = g2.transition().duration(750);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// // Draw/move child nodes on top of parent nodes.
// svg.selectAll(".depth").sort(function(a, b) {return a.depth - b.depth;});
// Update the domain only after entering new elements (within display()).
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
// This moves all rects into the right position & width/height.
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition texts to the new view.
t1.selectAll(".ptext").call(parentText).style("fill-opacity", 0);
t1.selectAll(".ctext").call(childrenText).style("fill-opacity", 0);
t2.selectAll(".ptext").call(parentText).style("fill-opacity", 1);
t2.selectAll(".ctext").call(childrenText).style("fill-opacity", 1);
// Remove the old node when the transition is finished.
t1.remove().on("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function parentText(text) {
text.selectAll("tspan")
.attr("x", function(d) { return x(d.x0) + 6; });
text.attr("x", function(d) { return x(d.x0) + 6; })
.attr("y", function(d) { return y(d.y0) + 6; })
.style("opacity", function(d) { return this.getComputedTextLength() < x(d.x1) - x(d.x0) - 6 ? 1 : 0; });
}
function childrenText(text) {
text.attr("x", function(d) { return x(d.x1) - this.getComputedTextLength() - 6; })
.attr("y", function(d) { return y(d.y1) - 6; })
.style("opacity", function(d) { return this.getComputedTextLength() < x(d.x1) - x(d.x0) - 6 ? 1 : 0; });
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x0); })
.attr("y", function(d) { return y(d.y0); })
.attr("width", function(d) { return x(d.x1) - x(d.x0); })
.attr("height", function(d) { return y(d.y1) - y(d.y0); });
}
function name(d) {
return d.parent
? name(d.parent) + " / " + d.data.key
: d.data.key;
}
function mousemove(d) {
var xPosition = d3.event.pageX + 5;
var yPosition = d3.event.pageY + 5;
d3.select("#tooltip")
.style("background-color", color(d.color_metric))
.style("left", xPosition + "px")
.style("top", yPosition + "px");
d3.select("#tooltip #heading")
.text(d.data.key);
tooltip_measures.forEach(m => {
d3.select("#tooltip #" + m.key + "_title")
.text(m.title);
d3.select("#tooltip #" + m.key)
.text(m.format ? d3.format(m.format)(d[m.measure]) : d[m.measure]);
});
d3.select("#tooltip").classed("hidden", false);
};
function mouseout() {
d3.select("#tooltip").classed("hidden", true);
};
}
d3.csv('treemap_region_city_population.csv', function(error, data) {
// console.log(data);
// Data source: https://datahub.io/dataset/global-city-population-estimates
// Region lookup source:
// https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes/blob/master/all/all.csv
var hier = [
'Region',
'SubRegion',
'CountryOrArea',
'UrbanAgglomeration'
];
// optional, but must be [min, med, max];
color_range = ['#03A9F4', '#ADD8E6', '#ffb300'];
var tooltip_measures = [
{
"key": "tooltip_measure_1",
"title": '2015 Population:',
"measure": '2015',
"format": ',.0f',
},
{
"key": "tooltip_measure_2",
"title": '2025 Population:',
"measure": '2025',
"format": ',.0f',
},
{
"key": "tooltip_measure_3",
"title": 'Growth rate:',
"measure": 'percent',
"format": ',.0%',
}
];
var hierarchical_data =
buildHierarchy(
data,
hier,
'2015',
'2025',
'2015_2025_delta',
true, // Tell it to compute a percentage
'2015_2025_delta', // numerator
'2015', // denominator
'percent', // color_measure
tooltip_measures
);
drawChart(
hierarchical_data,
'percent',
color_range,
tooltip_measures
);
});
</script>
<div id="tooltip" class="hidden">
<p><strong id="heading"></strong></p>
<p><span id="tooltip_measure_1_title"></span> <span id="tooltip_measure_1"></span></p>
<p><span id="tooltip_measure_2_title"></span> <span id="tooltip_measure_2"></span></p>
<p><span id="tooltip_measure_3_title"></span> <span id="tooltip_measure_3"></span></p>
</div>
</body>
https://d3js.org/d3.v4.js