Skip to content

Commit

Permalink
Add d3.treemapResquarify.
Browse files Browse the repository at this point in the history
Fixes #45.
  • Loading branch information
mbostock committed Jun 2, 2016
1 parent 7940c29 commit de68cf5
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 61 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,14 @@ Implements the [squarified treemap](https://www.win.tue.nl/~vanwijk/stm.pdf) alg
Specifies the desired aspect ratio of the generated rectangles. The specified *ratio* is merely a hint; the rectangles are not guaranteed to have the specified aspect ratio. If not specified, the aspect ratio defaults to the golden ratio, φ = (1 + sqrt(5)) / 2, per [Kong *et al.*](http://vis.stanford.edu/papers/perception-treemaps)
<a name="treemapResquarify" href="#treemapResquarify">#</a> d3.<b>treemapResquarify</b>(<i>node</i>, <i>x0</i>, <i>y0</i>, <i>x1</i>, <i>y1</i>)
Like [d3.treemapSquarify](#treemapSquarify), except preserves the topology of the previous layout computed by d3.treemapResquarify, if any, and if that layout was generated using the same [target aspect ratio](#resquarify_ratio).
<a name="resquarify_ratio" href="#resquarify_ratio">#</a> <i>resquarify</i>.<b>ratio</b>(<i>ratio</i>)
Specifies the desired aspect ratio of the generated rectangles. The specified *ratio* is merely a hint; the rectangles are not guaranteed to have the specified aspect ratio. If not specified, the aspect ratio defaults to the golden ratio, φ = (1 + sqrt(5)) / 2, per [Kong *et al.*](http://vis.stanford.edu/papers/perception-treemaps)
### Partition
[<img alt="Partition" src="https://raw.githubusercontent.com/d3/d3-hierarchy/master/img/partition.png">](http://bl.ocks.org/mbostock/2e73ec84221cb9773f4c)
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export {default as treemapDice} from "./src/treemap/dice";
export {default as treemapSlice} from "./src/treemap/slice";
export {default as treemapSliceDice} from "./src/treemap/sliceDice";
export {default as treemapSquarify} from "./src/treemap/squarify";
export {default as treemapResquarify} from "./src/treemap/resquarify";
36 changes: 36 additions & 0 deletions src/treemap/resquarify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import treemapDice from "./dice";
import treemapSlice from "./slice";
import {phi, squarifyRatio} from "./squarify";

export default (function custom(ratio) {

function resquarify(parent, x0, y0, x1, y1) {
if ((rows = parent._squarify) && (rows.ratio === ratio)) {
var rows,
row,
nodes,
i,
j = -1,
n,
m = rows.length,
value = parent.value;

while (++j < m) {
row = rows[j], nodes = row.children;
for (i = row.value = 0, n = nodes.length; i < n; ++i) row.value += nodes[i].value;
if (row.dice) treemapDice(row, x0, y0, x1, y0 += (y1 - y0) * row.value / value);
else treemapSlice(row, x0, y0, x0 += (x1 - x0) * row.value / value, y1);
value -= row.value;
}
} else {
parent._squarify = rows = squarifyRatio(ratio, parent, x0, y0, x1, y1);
rows.ratio = ratio;
}
}

resquarify.ratio = function(x) {
return custom((x = +x) > 1 ? x : 1);
};

return resquarify;
})(phi);
109 changes: 48 additions & 61 deletions src/treemap/squarify.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,63 @@
import treemapDice from "./dice";
import treemapSlice from "./slice";

export default (function custom(ratio) {
export var phi = (1 + Math.sqrt(5)) / 2;

function squarify(parent, x0, y0, x1, y1) {
if (parent._squarify) return resquarify(parent, x0, y0, x1, y1);
export function squarifyRatio(ratio, parent, x0, y0, x1, y1) {
var rows = [],
nodes = parent.children,
row,
nodeValue,
i0 = 0,
i1,
n = nodes.length,
dx, dy,
value = parent.value,
sumValue,
minValue,
maxValue,
newRatio,
minRatio,
alpha,
beta;

while (i0 < n) {
dx = x1 - x0, dy = y1 - y0;
minValue = maxValue = sumValue = nodes[i0].value;
alpha = Math.max(dy / dx, dx / dy) / (value * ratio);
beta = sumValue * sumValue * alpha;
minRatio = Math.max(maxValue / beta, beta / minValue);

// Keep adding nodes while the aspect ratio maintains or improves.
for (i1 = i0 + 1; i1 < n; ++i1) {
sumValue += nodeValue = nodes[i1].value;
if (nodeValue < minValue) minValue = nodeValue;
if (nodeValue > maxValue) maxValue = nodeValue;
beta = sumValue * sumValue * alpha;
newRatio = Math.max(maxValue / beta, beta / minValue);
if (newRatio > minRatio) { sumValue -= nodeValue; break; }
minRatio = newRatio;
}

var squarified = parent._squarify = [],
nodes = parent.children,
row,
nodeValue,
i0 = 0,
i1,
n = nodes.length,
dx, dy,
value = parent.value,
sumValue,
minValue,
maxValue,
newRatio,
minRatio,
alpha,
beta;
// Position and record the row orientation.
rows.push(row = {value: sumValue, dice: dx < dy, children: nodes.slice(i0, i1)});
if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += dy * sumValue / value : y1);
else treemapSlice(row, x0, y0, value ? x0 += dx * sumValue / value : x1, y1);
value -= sumValue, i0 = i1;
}

while (i0 < n) {
dx = x1 - x0, dy = y1 - y0;
minValue = maxValue = sumValue = nodes[i0].value;
alpha = Math.max(dy / dx, dx / dy) / (value * ratio);
beta = sumValue * sumValue * alpha;
minRatio = Math.max(maxValue / beta, beta / minValue);
return rows;
}

// Keep adding nodes while the aspect ratio maintains or improves.
for (i1 = i0 + 1; i1 < n; ++i1) {
sumValue += nodeValue = nodes[i1].value;
if (nodeValue < minValue) minValue = nodeValue;
if (nodeValue > maxValue) maxValue = nodeValue;
beta = sumValue * sumValue * alpha;
newRatio = Math.max(maxValue / beta, beta / minValue);
if (newRatio > minRatio) { sumValue -= nodeValue; break; }
minRatio = newRatio;
}
export default (function custom(ratio) {

// Position and record the row orientation.
squarified.push(row = {value: sumValue, dice: dx < dy, children: nodes.slice(i0, i1)});
if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += dy * sumValue / value : y1);
else treemapSlice(row, x0, y0, value ? x0 += dx * sumValue / value : x1, y1);
value -= sumValue, i0 = i1;
}
function squarify(parent, x0, y0, x1, y1) {
squarifyRatio(ratio, parent, x0, y0, x1, y1);
}

squarify.ratio = function(x) {
return custom((x = +x) > 1 ? x : 1);
};

return squarify;
})((1 + Math.sqrt(5)) / 2, false);

function resquarify(parent, x0, y0, x1, y1) {
var squarified = parent._squarify,
row,
nodes,
i,
j = -1,
n,
m = squarified.length,
value = parent.value;

while (++j < m) {
row = squarified[j], nodes = row.children;
for (i = row.value = 0, n = nodes.length; i < n; ++i) row.value += nodes[i].value;
if (row.dice) treemapDice(row, x0, y0, x1, y0 += (y1 - y0) * row.value / value);
else treemapSlice(row, x0, y0, x0 += (x1 - x0) * row.value / value, y1);
value -= row.value;
}
}
})(phi);
76 changes: 76 additions & 0 deletions test/treemap/resquarify-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
var tape = require("tape"),
d3_hierarchy = require("../../"),
round = require("./round");

tape("treemapResquarify(parent, x0, y0, x1, y1) produces a stable update", function(test) {
var tile = d3_hierarchy.treemapResquarify,
root = {value: 20, children: [{value: 10}, {value: 10}]};
tile(root, 0, 0, 20, 10);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 10, x1: 20, y0: 0, y1: 10}
]);
tile(root, 0, 0, 10, 20);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 5, y0: 0, y1: 20},
{x0: 5, x1: 10, y0: 0, y1: 20}
]);
test.end();
});

tape("treemapResquarify.ratio(ratio) observes the specified ratio", function(test) {
var tile = d3_hierarchy.treemapResquarify.ratio(1),
root = {
value: 24,
children: [
{value: 6},
{value: 6},
{value: 4},
{value: 3},
{value: 2},
{value: 2},
{value: 1}
]
};
tile(root, 0, 0, 6, 4);
test.deepEqual(root.children.map(round), [
{x0: 0.00, x1: 3.00, y0: 0.00, y1: 2.00},
{x0: 0.00, x1: 3.00, y0: 2.00, y1: 4.00},
{x0: 3.00, x1: 4.71, y0: 0.00, y1: 2.33},
{x0: 4.71, x1: 6.00, y0: 0.00, y1: 2.33},
{x0: 3.00, x1: 4.20, y0: 2.33, y1: 4.00},
{x0: 4.20, x1: 5.40, y0: 2.33, y1: 4.00},
{x0: 5.40, x1: 6.00, y0: 2.33, y1: 4.00}
]);
test.end();
});

tape("treemapResquarify.ratio(ratio) is stable if the ratio is unchanged", function(test) {
var root = {value: 20, children: [{value: 10}, {value: 10}]};
d3_hierarchy.treemapResquarify(root, 0, 0, 20, 10);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 10, x1: 20, y0: 0, y1: 10}
]);
d3_hierarchy.treemapResquarify.ratio((1 + Math.sqrt(5)) / 2)(root, 0, 0, 10, 20);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 5, y0: 0, y1: 20},
{x0: 5, x1: 10, y0: 0, y1: 20}
]);
test.end();
});

tape("treemapResquarify.ratio(ratio) is unstable if the ratio is changed", function(test) {
var root = {value: 20, children: [{value: 10}, {value: 10}]};
d3_hierarchy.treemapResquarify(root, 0, 0, 20, 10);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 10, x1: 20, y0: 0, y1: 10}
]);
d3_hierarchy.treemapResquarify.ratio(1)(root, 0, 0, 10, 20);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 0, x1: 10, y0: 10, y1: 20}
]);
test.end();
});
16 changes: 16 additions & 0 deletions test/treemap/squarify-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ tape("treemapSquarify(parent, x0, y0, x1, y1) generates a squarified layout", fu
test.end();
});

tape("treemapSquarify(parent, x0, y0, x1, y1) does not produce a stable update", function(test) {
var tile = d3_hierarchy.treemapSquarify,
root = {value: 20, children: [{value: 10}, {value: 10}]};
tile(root, 0, 0, 20, 10);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 10, x1: 20, y0: 0, y1: 10}
]);
tile(root, 0, 0, 10, 20);
test.deepEqual(root.children.map(round), [
{x0: 0, x1: 10, y0: 0, y1: 10},
{x0: 0, x1: 10, y0: 10, y1: 20}
]);
test.end();
});

tape("treemapSquarify.ratio(ratio) observes the specified ratio", function(test) {
var tile = d3_hierarchy.treemapSquarify.ratio(1),
root = {
Expand Down

0 comments on commit de68cf5

Please sign in to comment.