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).
xxxxxxxxxx
<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>
https://d3js.org/d3.v5.min.js