|
<!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> |