Skip to content

Instantly share code, notes, and snippets.

@rpgove
Forked from mbostock/.block
Last active December 27, 2019 00:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rpgove/3e22f658c933f42fbf264e7e8ea11080 to your computer and use it in GitHub Desktop.
Save rpgove/3e22f658c933f42fbf264e7e8ea11080 to your computer and use it in GitHub Desktop.
Spiral Treemap
border: no
height: 600
license: gpl-3.0

A treemap recursively subdivides area into rectangles; the area of any node in the tree corresponds to its value. This example uses color to encode the order of several perfect squares (the children of each perfect square are the nonzero digits of that perfect square). Treemap design invented by Ben Shneiderman. Spiral treemap algorithm by Tu and Shen.

The spiral treemap layout has several benefits: It preserves the order of the nodes in the tree, and it maintains a good average aspect ratio (close to the golden ratio in this example).

<!DOCTYPE html>
<style>
form {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
svg {
font: 10px sans-serif;
float: left;
}
rect {
stroke: #000;
stroke-width: 1px;
}
</style>
<svg width="600" height="600"></svg>
<form>
<label><input type="radio" name="mode" value="sortAscending" checked> Ascending</label>
<label><input type="radio" name="mode" value="sortDescending"> Descending</label>
</form>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var minVal = 10;
var maxVal = 40;
var color = d3.scaleSequential()
.domain([0, maxVal - minVal])
.interpolator(d3.interpolateRdPu);
var treemap = d3.treemap()
.size([width, height])
.round(true)
.paddingOuter(1)
.tile(treemapSpiral);
var data = generateData();
var root = d3.hierarchy(data)
.eachBefore(function(d) {
d.data.id = (d.parent ? d.parent.data.id + "." : "") + d.data.name + '-' + d.data.index;
})
.sum(function (d) { return d.size; })
.sort(sortAscending);
treemap(root);
var cell = svg.selectAll("g")
.data(root.leaves())
.enter().append("g")
.attr("transform", function(d) { return "translate(" + d.x0 + "," + d.y0 + ")"; });
cell.append("rect")
.attr("id", function(d) { return d.data.id; })
.attr("width", function(d) { return d.x1 - d.x0; })
.attr("height", function(d) { return d.y1 - d.y0; })
.attr("stroke", function(d) { return d3.color(color(d.parent.data.index || d.data.index)).darker(1); })
.attr("fill", function(d) { return color(d.parent.data.index || d.data.index); });
cell.append("clipPath")
.attr("id", function(d) { return "clip-" + d.data.id; })
.append("use")
.attr("xlink:href", function(d) { return "#" + d.data.id; });
cell.append("text")
.attr("clip-path", function(d) { return "url(#clip-" + d.data.id + ")"; })
.selectAll("tspan")
.data(function(d) { return d.data.name.split('.'); })
.enter().append("tspan")
.attr("x", 4)
.attr("y", function(d, i) { return 13 + i * 10; })
.text(function(d) { return d; });
cell.append("title")
.text(function(d) { return d.parent.data.size + ' -> ' + d.value; });
d3.selectAll("input")
.data([sortAscending, sortDescending], function(d) { return d ? d.name : this.value; })
.on("change", changed);
function changed(sort) {
treemap(root.sort(sort));
cell.transition()
.duration(750)
.attr("transform", function(d) { return "translate(" + d.x0 + "," + d.y0 + ")"; })
.select("rect")
.attr("width", function(d) { return d.x1 - d.x0; })
.attr("height", function(d) { return d.y1 - d.y0; });
}
function sortDescending (a, b) { return b.height - a.height || b.value - a.value; }
function sortAscending (a, b) { return a.height - b.height || a.value - b.value; }
function generateData() {
var firstLevel = d3.range(minVal, maxVal).map(function (d, i) {
return {name: Number(d * d).toString(), size: d * d, index: i};
});
var result = {
name: 'squared',
index: 0,
children: firstLevel
};
result.children.forEach(function (d) {
d.children = Number(d.size).toString().split('').filter(function (s) { return +s; }).map(function (s, i) {
return {name: d.name + '.' + s, size: +s, index: i};
});
});
return result;
}
function treemapSpiral (parent, x0, y0, x1, y1) {
var EAST = 0;
var SOUTH = 1;
var WEST = 2;
var NORTH = 3;
var direction = EAST;
var nodes = parent.children;
var node;
var n = nodes.length;
var i = -1;
var newX0 = x0;
var newX1 = x1;
var newY0 = y0;
var newY1 = y1;
var availWidth = x1 - x0;
var availHeight = y1 - y0;
var avgAspectRatio = 0;
var nodeAspectRatio = 0;
var segment = [];
var segmentSum = 0;
var nodesSum = 0;
for (i = n; i--;) nodesSum += nodes[i].value;
i = -1;
while (++i < n) {
node = nodes[i];
segment.push(node);
segmentSum += node.value;
if (direction === EAST) {
// Update positions for each node.
segment.forEach(function (d, i, arr) {
d.x0 = i ? arr[i-1].x1 : newX0;
d.x1 = d.x0 + (d.value / segmentSum) * availWidth;
d.y0 = newY0;
d.y1 = newY0 + (segmentSum / nodesSum) * availHeight;
});
} else if (direction === SOUTH) {
segment.forEach(function (d, i, arr) {
d.x0 = newX1 - (segmentSum / nodesSum) * availWidth;
d.x1 = newX1;
d.y0 = i ? arr[i-1].y1 : newY0;
d.y1 = d.y0 + (d.value / segmentSum) * availHeight;
});
} else if (direction === WEST) {
segment.forEach(function (d, i, arr) {
d.x1 = i ? arr[i-1].x0 : newX1;
d.x0 = d.x1 - (d.value / segmentSum) * availWidth;
d.y0 = newY1 - (segmentSum / nodesSum) * availHeight;
d.y1 = newY1;
});
} else if (direction === NORTH) {
segment.forEach(function (d, i, arr) {
d.x1 = newX0 + (segmentSum / nodesSum) * availWidth;
d.x0 = newX0;
d.y1 = i ? arr[i-1].y0 : newY1;
d.y0 = d.y1 - (d.value / segmentSum) * availHeight;
});
}
// Compute new aspect ratio.
nodeAspectRatio = direction & 1 ? (node.y1 - node.y0) / (node.x1 - node.x0) : (node.x1 - node.x0) / (node.y1 - node.y0);
avgAspectRatio = d3.sum(segment, function (d) {
return direction & 1 ? (d.y1 - d.y0) / (d.x1 - d.x0) : (d.x1 - d.x0) / (d.y1 - d.y0);
});
// If avg aspect ratio is small, update boundaries and start a new segment.
if (avgAspectRatio / segment.length < 1.618033988749895) {
if (direction === EAST) {
newY0 = node.y1;
availHeight = newY1 - newY0;
} else if (direction === SOUTH) {
newX1 = node.x0;
availWidth = newX1 - newX0;
} else if (direction === WEST) {
newY1 = node.y0;
availHeight = newY1 - newY0;
} else if (direction === NORTH) {
newX0 = node.x1;
availWidth = newX1 - newX0;
}
nodesSum -= segmentSum;
segment.length = 0;
segmentSum = 0;
avgAspectRatio = 0;
direction = (direction + 1) % 4;
}
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment