Skip to content
This repository has been archived by the owner on Apr 30, 2023. It is now read-only.

Commit

Permalink
Treemap improvements.
Browse files Browse the repository at this point in the history
Treemaps now support two modes: "squarify" for squarified treemaps (as before),
and "slice-and-dice" for the simpler algorithm that alternates between
horizontal and vertical cuts at different hierarchy levels. In addition,
treemaps now support three orders: "ascending", "descending", and null (for
unsorted). The default is "ascending" as before.

This commit also fixes an occasional bug with rounding.
  • Loading branch information
Mike Bostock committed Mar 17, 2010
1 parent 4851475 commit ab01d67
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 79 deletions.
3 changes: 3 additions & 0 deletions TODO
Expand Up @@ -21,6 +21,9 @@ layout/Tree.js
layout/{Cluster,Partition}.js
- support customized inner/outer radius

mark/Anchor.js
- support anchors across panels: mirrored descent (fallback to last?)

mark/Mark.js
- antialias doesn't always work as expected; requires pixel rounding?

Expand Down
186 changes: 116 additions & 70 deletions src/layout/Treemap.js
Expand Up @@ -48,7 +48,14 @@ pv.Layout.Treemap.prototype = pv.extend(pv.Layout.Hierarchy)
.property("left", Number)
.property("right", Number)
.property("top", Number)
.property("bottom", Number);
.property("bottom", Number)
.property("mode", String)
.property("order", String);

pv.Layout.Treemap.prototype.defaults = new pv.Layout.Treemap()
.extend(pv.Layout.Hierarchy.prototype.defaults)
.mode("squarify") // squarify, slice-and-dice
.order("ascending"); // ascending, descending, null == unsorted

pv.Layout.Treemap.prototype.$size = Number;

Expand All @@ -66,8 +73,34 @@ pv.Layout.Treemap.prototype.init = function() {
right = that.right(),
top = that.top(),
bottom = that.bottom(),
size = function(n) { return n.size; },
round = that.round() ? Math.round : Number;

/** @private */
function slice(row, sum, horizontal, x, y, w, h) {
for (var i = 0, d = 0; i < row.length; i++) {
var n = row[i];
if (horizontal) {
n.x = x + d;
n.y = y;
d += n.dx = round(w * n.size / sum);
n.dy = h;
} else {
n.x = x;
n.y = y + d;
n.dx = w;
d += n.dy = round(h * n.size / sum);
}
}
if (n) { // correct on-axis rounding error
if (horizontal) {
n.dx += w - d;
} else {
n.dy += h - d;
}
}
}

/** @private */
function ratio(row, l) {
var rmax = -Infinity, rmin = Infinity, s = 0;
Expand All @@ -83,81 +116,83 @@ pv.Layout.Treemap.prototype.init = function() {
}

/** @private */
function squarify(n) {
var row = [],
mink = Infinity,
x = n.x + left,
y = n.y + top,
w = n.dx - left - right,
h = n.dy - top - bottom,
l = Math.min(w, h),
k = w * h / n.size;

/* Scale the sizes to fill the current subregion. */
n.visitBefore(function(n) { n.size *= k; });

/** @private Position the specified nodes along one dimension. */
function position(row) {
var s = pv.sum(row, function(n) { return n.size; }),
hh = (l == 0) ? 0 : round(s / l);

for (var i = 0, d = 0; i < row.length; i++) {
var n = row[i], nw = round(n.size / hh);
if (w == l) {
n.x = x + d;
n.y = y;
n.dx = nw;
n.dy = hh;
var modes = {
"slice-and-dice": function(n, i) {
slice(n.childNodes,
pv.sum(n.childNodes, size),
i & 1,
n.x + left,
n.y + top,
n.dx - left - right,
n.dy - top - bottom);
},

"squarify": function(n) {
var row = [],
mink = Infinity,
x = n.x + left,
y = n.y + top,
w = n.dx - left - right,
h = n.dy - top - bottom,
l = Math.min(w, h),
k = w * h / n.size;

/* Abort if the size is nonpositive. */
if (n.size <= 0) {
n.dx = 0;
n.dy = 0;
return;
}

/* Scale the sizes to fill the current subregion. */
n.visitBefore(function(n) { n.size *= k; });

/** @private Position the specified nodes along one dimension. */
function position(row) {
var horizontal = w == l,
sum = pv.sum(row, size),
r = l ? round(sum / l) : 0;
slice(row, sum, horizontal, x, y, horizontal ? w : r, horizontal ? r : h);
if (horizontal) {
y += r;
h -= r;
} else {
n.x = x;
n.y = y + d;
n.dx = hh;
n.dy = nw;
x += r;
w -= r;
}
d += nw;
l = Math.min(w, h);
return horizontal;
}

if (w == l) {
if (n) n.dx += w - d; // correct rounding error
y += hh;
h -= hh;
} else {
if (n) n.dy += h - d; // correct rounding error
x += hh;
w -= hh;
}
l = Math.min(w, h);
}
var children = n.childNodes.slice(); // copy
while (children.length) {
var child = children[children.length - 1];
if (!child.size) {
children.pop();
continue;
}
row.push(child);

var children = n.childNodes.slice(); // copy
while (children.length) {
var child = children[children.length - 1];
if (!child.size) {
children.pop();
continue;
var k = ratio(row, l);
if (k <= mink) {
children.pop();
mink = k;
} else {
row.pop();
position(row);
row.length = 0;
mink = Infinity;
}
}
row.push(child);

var k = ratio(row, l);
if (k <= mink) {
children.pop();
mink = k;
} else {
row.pop();
position(row);
row.length = 0;
mink = Infinity;
/* correct off-axis rounding error */
if (position(row)) for (var i = 0; i < row.length; i++) {
row[i].dy += h;
} else for (var i = 0; i < row.length; i++) {
row[i].dx += w;
}
}
position(row);

/* correct rounding error */
if (w == l) for (var i = 0; i < row.length; i++) {
row[i].dx += w;
} else for (var i = 0; i < row.length; i++) {
row[i].dy += h;
}
}
};

/* Recursively compute the node depth and size. */
stack.unshift(null);
Expand All @@ -169,11 +204,22 @@ pv.Layout.Treemap.prototype.init = function() {
});
stack.shift();

/* Sort by ascending size, then recursively compute the layout. */
root.sort(function(a, b) { return a.size - b.size; });
/* Sort. */
switch (that.order()) {
case "ascending": {
root.sort(function(a, b) { return a.size - b.size; });
break;
}
case "descending": {
root.sort(function(a, b) { return b.size - a.size; });
break;
}
}

/* Recursively compute the layout. */
root.x = 0;
root.y = 0;
root.dx = that.parent.width();
root.dy = that.parent.height();
root.visitBefore(squarify);
root.visitBefore(modes[that.mode()]);
};
22 changes: 16 additions & 6 deletions tests/layout/treemap-round.html
Expand Up @@ -3,6 +3,13 @@
<title>Flare Treemap</title>
<script type="text/javascript" src="../../protovis-d3.2.js"></script>
<script type="text/javascript" src="../flare.js"></script>
<style type="text/css">

body {
margin: 0;
}

</style>
</head>
<body>
<script type="text/javascript+protovis">
Expand All @@ -12,23 +19,26 @@
}

var vis = new pv.Panel()
.width(860)
.height(668)
.width(function() window.innerWidth - 1)
.height(function() window.innerHeight - 1)
.margin(.5);

vis.add(pv.Layout.Treemap)
.data(pv.dom(flare).nodes())
.margin(6)
.round(true)
.add(pv.Panel)
.event("mouseover", function(d) (active(d, true), this))
.event("mouseout", function(d) (active(d, false), this))
.add(pv.Bar)
.fillStyle(function(d) d.active ? "lightcoral" : "#ccc")
.strokeStyle("#fff")
.lineWidth(1);
.lineWidth(1)
.antialias(false)
.event("mouseover", function(d) (active(d, true), this))
.event("mouseout", function(d) (active(d, false), this));

vis.render();

window.onresize = function() vis.render();

</script>
</body>
</html>
40 changes: 40 additions & 0 deletions tests/layout/treemap-slice.html
@@ -0,0 +1,40 @@
<html>
<head>
<title>Flare Treemap</title>
<script type="text/javascript" src="../../protovis-d3.2.js"></script>
<script type="text/javascript" src="../flare.js"></script>
<style type="text/css">

body {
margin: 0;
}

</style>
</head>
<body>
<script type="text/javascript+protovis">

var vis = new pv.Panel()
.width(function() window.innerWidth - 1)
.height(function() window.innerHeight - 1)
.margin(.5);

vis.add(pv.Layout.Treemap)
.data(pv.dom(flare).root("flare").nodes())
.mode("slice-and-dice")
.order("descending")
.add(pv.Panel)
.overflow("hidden")
.fillStyle("#aec7e8")
.visible(function(d) !d.firstChild)
.anchor("center").add(pv.Label)
.textAngle(function(d) d.dx > d.dy ? 0 : -Math.PI / 2)
.text(function(d) d.nodeName);

vis.render();

window.onresize = function() vis.render();

</script>
</body>
</html>
24 changes: 21 additions & 3 deletions tests/layout/treemap.html
Expand Up @@ -3,20 +3,38 @@
<title>Flare Treemap</title>
<script type="text/javascript" src="../../protovis-d3.2.js"></script>
<script type="text/javascript" src="../flare.js"></script>
<style type="text/css">

body {
margin: 0;
}

</style>
</head>
<body>
<script type="text/javascript+protovis">

var vis = new pv.Panel()
.width(860)
.height(668);
.width(function() window.innerWidth - 1)
.height(function() window.innerHeight - 1)
.margin(.5);

vis.add(pv.Layout.Treemap)
.data(pv.dom(flare).root("flare").nodes())
.add(pv.Bar);
.round(true)
.add(pv.Panel)
.visible(function(d) !d.firstChild)
.overflow("hidden")
.fillStyle(pv.Colors.category19().by(function(d) d.parentNode.nodeName))
.lineWidth(1)
.anchor("center").add(pv.Label)
.textAngle(function(d) d.dx > d.dy ? 0 : -Math.PI / 2)
.text(function(d) d.nodeName);

vis.render();

window.onresize = function() vis.render();

</script>
</body>
</html>

0 comments on commit ab01d67

Please sign in to comment.