this iteration converts the code to ES2015 in something like the airbnb style
still d3 version 3
for linting and lebab convenience, the javascript is abstracted out into a new file, drawTreemap.js
would really like to see block converted to d3 version 4
read the Hierarchies section of CHANGES.md, but haven’t quite figured out exactly how to use the v4 treemap layout with the custom layout functions in this block 😅🤔
do tweet @micahstubbs if you figure it out 😀
a fork of Zoomable Treemap Template from ganeshv
Treemaps for visualizing hierarchical data. Click to zoom to the next level. Click on the top orange band to zoom out. Based on Mike Bostock’s Zoomable Treemaps
This template follows pigshell‘s convention for “gist templates”:
{ opts: {...}, data: [...] }
{ height: <number> }
to enable the framing context to adjust the height of the iframe.The following options are supported:
{
title: "", // Title
rootname: "TOP", // Name of top-level entity in case data is an array
format: ",d", // Format as per d3.format (https://github.com/mbostock/d3/wiki/Formatting)
field: "data", // Object field to treat as data [default: data]
width: 960, // Width of SVG
height: 500, // Height of SVG
margin: { top: 48, right: 0, bottom: 0, left: 0 } // Margin as per D3 convention
}
data
is a tree-like object, or an array of tree-like objects. Each non-leaf
node of the tree must contain a “key” property and an array of “values”.
Leaf nodes must contain a “key” and a “value” property.
[
{
"key": "Asia",
"values": [
{
"key": "India",
"value": 1236670000
},
{
"key": "China",
"value": 1361170000
},
...
},
{
"key": "Africa",
"values": [
{
"key": "Nigeria",
"value": 173615000
},
{
"key": "Egypt",
"value": 83661000
},
...
},
]
Sample data is world population from countries.git.
Data may be easily converted to tree form using d3.nest(). See the first example below.
Examples (to be run in pigshell):
load http://d3js.org/d3.v3.min.js
cat /usr/share/misc/countries.json | to text | jf 'JSON.parse(x)'| rename -f "name,population" -t "key,value" | jf -g 'd3.nest().key(function(d) {return d.region;}).key(function(d) { return d.subregion; }).entries(x)' | iframe -o title="World Population",rootname="World" -g 'http://bl.ocks.org/ganeshv/raw/6a8e9ada3ab7f2d88022/#wait'
<!DOCTYPE html>
<!--
Generic treemap, based on //bost.ocks.org/mike/treemap/
-->
<html>
<head>
<meta charset='utf-8'>
<title>Zoomable treemap template</title>
<style>
#chart {
background: #fff;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.title {
font-weight: bold;
font-size: 24px;
text-align: center;
margin-top: 6px;
margin-bottom: 6px;
position: relative;
width: 960px;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
rect.parent {
pointer-events: none;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
</style>
</head>
<body>
<div id='chart'></div>
<script src='https://d3js.org/d3.v3.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.0/babel.min.js'></script>
<script src='drawTreemap.js' lang='babel' type='text/babel'></script>
</body>
</html>
window.addEventListener('message', (e) => {
const opts = e.data.opts;
const data = e.data.data;
return main(opts, data);
});
const defaults = {
margin: { top: 24, right: 0, bottom: 0, left: 0 },
rootname: 'TOP',
format: ',d',
title: '',
width: 960,
height: 500,
};
function main(o, data) {
let root;
const opts = deepExtend({}, defaults, o);
const formatNumber = d3.format(opts.format);
const rname = opts.rootname;
const margin = opts.margin;
const theight = 36 + 16;
// set size of chart div
d3.select('#chart')
.attr('width', opts.width)
.attr('height', opts.height);
const width = opts.width - margin.left - margin.right;
const height = opts.height - margin.top - margin.bottom - theight;
let transitioning;
//
// setup scales
//
const color = d3.scale.category20c();
const x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
const y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
const treemap = d3.layout.treemap()
.children((d, depth) => {
if (depth) {
return null;
}
return d._children;
})
.sort((a, b) => a.value - b.value)
.ratio(height / (width * 0.5 * (1 + Math.sqrt(5))))
.round(false);
//
// setup the page
//
// add svg to the page
const 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');
// add grandparent svg group
const grandparent = svg.append('g')
.attr('class', 'grandparent');
grandparent.append('rect')
.attr('y', -margin.top)
.attr('width', width)
.attr('height', margin.top);
grandparent.append('text')
.attr('x', 6)
.attr('y', 6 - margin.top)
.attr('dy', '.75em');
// if the options specify a title
if (opts.title) {
// add the title to the page before the chart div
const parentNode = document.querySelector('div#chart');
const newChild = document.createElement('div');
newChild.innerHTML = `<p class='title'>${opts.title}</p>`;
const refChild = parentNode.firstElementChild;
parentNode.insertBefore(newChild, refChild);
}
//
// construct the tree data
//
// build the root node from the data
// support both
// an array of objects
// and
// a nested json object
if (data instanceof Array) {
root = { key: rname, values: data };
} else {
root = data;
}
// call functions
// to add properties to
// the root object
initialize(root);
accumulate(root);
layout(root);
console.log('root', root);
display(root);
if (window.parent !== window) {
const myheight = document.documentElement.scrollHeight || document.body.scrollHeight;
window.parent.postMessage({ height: myheight }, '*');
}
// add position values to the root node
function initialize(root) {
root.x = root.y = 0;
root.dx = 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) {
d._children = d.values;
if (d._children) {
d.value = d.values.reduce((p, v) => p + accumulate(v), 0);
return d.value;
}
return d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({ _children: d._children });
d._children.forEach((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) {
grandparent
.datum(d.parent)
.on('click', transition)
.select('text')
.text(name(d));
const g1 = svg.insert('g', '.grandparent')
.datum(d)
.attr('class', 'depth');
const g = g1.selectAll('g')
.data(d._children)
.enter().append('g');
g.filter(d => d._children)
.classed('children', true)
.on('click', transition);
const children = g.selectAll('.child')
.data(d => d._children || [d])
.enter().append('g');
children.append('rect')
.attr('class', 'child')
.call(rect)
.append('title')
.text(d => `${d.key} (${formatNumber(d.value)})`);
children.append('text')
.attr('class', 'ctext')
.text(d => d.key)
.call(text2);
g.append('rect')
.attr('class', 'parent')
.call(rect);
const t = g.append('text')
.attr('class', 'ptext')
.attr('dy', '.75em');
t.append('tspan')
.text(d => d.key);
t.append('tspan')
.attr('dy', '1.0em')
.text(d => formatNumber(d.value));
t.call(text);
g.selectAll('rect')
.style('fill', d => color(d.key));
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
const g2 = display(d);
const t1 = g1.transition().duration(750);
const 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((a, b) => a.depth - b.depth);
// Fade-in entering text.
g2.selectAll('text').style('fill-opacity', 0);
// Transition to the new view.
t1.selectAll('.ptext').call(text).style('fill-opacity', 0);
t1.selectAll('.ctext').call(text2).style('fill-opacity', 0);
t2.selectAll('.ptext').call(text).style('fill-opacity', 1);
t2.selectAll('.ctext').call(text2).style('fill-opacity', 1);
t1.selectAll('rect').call(rect);
t2.selectAll('rect').call(rect);
// Remove the old node when the transition is finished.
t1.remove().each('end', () => {
svg.style('shape-rendering', 'crispEdges');
transitioning = false;
});
}
return g;
}
function text(text) {
text.selectAll('tspan')
.attr('x', d => x(d.x) + 6);
text.attr('x', d => x(d.x) + 6)
.attr('y', d => y(d.y) + 6)
.style('opacity', function (d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
}
function text2(text) {
text.attr('x', function (d) { return x(d.x + d.dx) - this.getComputedTextLength() - 6; })
.attr('y', d => y(d.y + d.dy) - 6)
.style('opacity', function (d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
}
function rect(rect) {
rect.attr('x', d => x(d.x))
.attr('y', d => y(d.y))
.attr('width', d => x(d.x + d.dx) - x(d.x))
.attr('height', d => y(d.y + d.dy) - y(d.y));
}
function name(d) {
if (d.parent) {
return `${name(d.parent)} / ${d.key} (${formatNumber(d.value)})`;
}
return `${d.key} (${formatNumber(d.value)})`;
}
// a replacement for the jQuery `$.extend(true, {}, objA, objB);`
// http://youmightnotneedjquery.com/ with search term `extend`
function deepExtend(out) {
out = out || {};
for (var i = 1; i < arguments.length; i++) {
var obj = arguments[i];
if (!obj)
continue;
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object')
out[key] = deepExtend(out[key], obj[key]);
else
out[key] = obj[key];
}
}
}
return out;
};
}
if (window.location.hash === '') {
d3.json('countries.json', (err, res) => {
if (!err) {
console.log(res);
const data = d3.nest().key(d => d.region).key(d => d.subregion).entries(res);
main({ title: 'World Population' }, { key: 'World', values: data });
}
});
}