[76] | 1 | // Squarified Treemaps by Mark Bruls, Kees Huizing, and Jarke J. van Wijk |
---|
| 2 | // Modified to support a target aspect ratio by Jeff Heer |
---|
| 3 | d3.layout.treemap = function() { |
---|
| 4 | var hierarchy = d3.layout.hierarchy(), |
---|
| 5 | round = Math.round, |
---|
| 6 | size = [1, 1], // width, height |
---|
| 7 | padding = null, |
---|
| 8 | pad = d3_layout_treemapPadNull, |
---|
| 9 | sticky = false, |
---|
| 10 | stickies, |
---|
| 11 | ratio = 0.5 * (1 + Math.sqrt(5)); // golden ratio |
---|
| 12 | |
---|
| 13 | // Compute the area for each child based on value & scale. |
---|
| 14 | function scale(children, k) { |
---|
| 15 | var i = -1, |
---|
| 16 | n = children.length, |
---|
| 17 | child, |
---|
| 18 | area; |
---|
| 19 | while (++i < n) { |
---|
| 20 | area = (child = children[i]).value * (k < 0 ? 0 : k); |
---|
| 21 | child.area = isNaN(area) || area <= 0 ? 0 : area; |
---|
| 22 | } |
---|
| 23 | } |
---|
| 24 | |
---|
| 25 | // Recursively arranges the specified node's children into squarified rows. |
---|
| 26 | function squarify(node) { |
---|
| 27 | if (!node.children) return; |
---|
| 28 | var rect = pad(node), |
---|
| 29 | row = [], |
---|
| 30 | children = node.children.slice(), // copy-on-write |
---|
| 31 | child, |
---|
| 32 | best = Infinity, // the best row score so far |
---|
| 33 | score, // the current row score |
---|
| 34 | u = Math.min(rect.dx, rect.dy), // initial orientation |
---|
| 35 | n; |
---|
| 36 | scale(children, rect.dx * rect.dy / node.value); |
---|
| 37 | row.area = 0; |
---|
| 38 | while ((n = children.length) > 0) { |
---|
| 39 | row.push(child = children[n - 1]); |
---|
| 40 | row.area += child.area; |
---|
| 41 | if ((score = worst(row, u)) <= best) { // continue with this orientation |
---|
| 42 | children.pop(); |
---|
| 43 | best = score; |
---|
| 44 | } else { // abort, and try a different orientation |
---|
| 45 | row.area -= row.pop().area; |
---|
| 46 | position(row, u, rect, false); |
---|
| 47 | u = Math.min(rect.dx, rect.dy); |
---|
| 48 | row.length = row.area = 0; |
---|
| 49 | best = Infinity; |
---|
| 50 | } |
---|
| 51 | } |
---|
| 52 | if (row.length) { |
---|
| 53 | position(row, u, rect, true); |
---|
| 54 | row.length = row.area = 0; |
---|
| 55 | } |
---|
| 56 | node.children.forEach(squarify); |
---|
| 57 | } |
---|
| 58 | |
---|
| 59 | // Recursively resizes the specified node's children into existing rows. |
---|
| 60 | // Preserves the existing layout! |
---|
| 61 | function stickify(node) { |
---|
| 62 | if (!node.children) return; |
---|
| 63 | var rect = pad(node), |
---|
| 64 | children = node.children.slice(), // copy-on-write |
---|
| 65 | child, |
---|
| 66 | row = []; |
---|
| 67 | scale(children, rect.dx * rect.dy / node.value); |
---|
| 68 | row.area = 0; |
---|
| 69 | while (child = children.pop()) { |
---|
| 70 | row.push(child); |
---|
| 71 | row.area += child.area; |
---|
| 72 | if (child.z != null) { |
---|
| 73 | position(row, child.z ? rect.dx : rect.dy, rect, !children.length); |
---|
| 74 | row.length = row.area = 0; |
---|
| 75 | } |
---|
| 76 | } |
---|
| 77 | node.children.forEach(stickify); |
---|
| 78 | } |
---|
| 79 | |
---|
| 80 | // Computes the score for the specified row, as the worst aspect ratio. |
---|
| 81 | function worst(row, u) { |
---|
| 82 | var s = row.area, |
---|
| 83 | r, |
---|
| 84 | rmax = 0, |
---|
| 85 | rmin = Infinity, |
---|
| 86 | i = -1, |
---|
| 87 | n = row.length; |
---|
| 88 | while (++i < n) { |
---|
| 89 | if (!(r = row[i].area)) continue; |
---|
| 90 | if (r < rmin) rmin = r; |
---|
| 91 | if (r > rmax) rmax = r; |
---|
| 92 | } |
---|
| 93 | s *= s; |
---|
| 94 | u *= u; |
---|
| 95 | return s |
---|
| 96 | ? Math.max((u * rmax * ratio) / s, s / (u * rmin * ratio)) |
---|
| 97 | : Infinity; |
---|
| 98 | } |
---|
| 99 | |
---|
| 100 | // Positions the specified row of nodes. Modifies `rect`. |
---|
| 101 | function position(row, u, rect, flush) { |
---|
| 102 | var i = -1, |
---|
| 103 | n = row.length, |
---|
| 104 | x = rect.x, |
---|
| 105 | y = rect.y, |
---|
| 106 | v = u ? round(row.area / u) : 0, |
---|
| 107 | o; |
---|
| 108 | if (u == rect.dx) { // horizontal subdivision |
---|
| 109 | if (flush || v > rect.dy) v = rect.dy; // over+underflow |
---|
| 110 | while (++i < n) { |
---|
| 111 | o = row[i]; |
---|
| 112 | o.x = x; |
---|
| 113 | o.y = y; |
---|
| 114 | o.dy = v; |
---|
| 115 | x += o.dx = v ? round(o.area / v) : 0; |
---|
| 116 | } |
---|
| 117 | o.z = true; |
---|
| 118 | o.dx += rect.x + rect.dx - x; // rounding error |
---|
| 119 | rect.y += v; |
---|
| 120 | rect.dy -= v; |
---|
| 121 | } else { // vertical subdivision |
---|
| 122 | if (flush || v > rect.dx) v = rect.dx; // over+underflow |
---|
| 123 | while (++i < n) { |
---|
| 124 | o = row[i]; |
---|
| 125 | o.x = x; |
---|
| 126 | o.y = y; |
---|
| 127 | o.dx = v; |
---|
| 128 | y += o.dy = v ? round(o.area / v) : 0; |
---|
| 129 | } |
---|
| 130 | o.z = false; |
---|
| 131 | o.dy += rect.y + rect.dy - y; // rounding error |
---|
| 132 | rect.x += v; |
---|
| 133 | rect.dx -= v; |
---|
| 134 | } |
---|
| 135 | } |
---|
| 136 | |
---|
| 137 | function treemap(d) { |
---|
| 138 | var nodes = stickies || hierarchy(d), |
---|
| 139 | root = nodes[0]; |
---|
| 140 | root.x = 0; |
---|
| 141 | root.y = 0; |
---|
| 142 | root.dx = size[0]; |
---|
| 143 | root.dy = size[1]; |
---|
| 144 | if (stickies) hierarchy.revalue(root); |
---|
| 145 | scale([root], root.dx * root.dy / root.value); |
---|
| 146 | (stickies ? stickify : squarify)(root); |
---|
| 147 | if (sticky) stickies = nodes; |
---|
| 148 | return nodes; |
---|
| 149 | } |
---|
| 150 | |
---|
| 151 | treemap.size = function(x) { |
---|
| 152 | if (!arguments.length) return size; |
---|
| 153 | size = x; |
---|
| 154 | return treemap; |
---|
| 155 | }; |
---|
| 156 | |
---|
| 157 | treemap.padding = function(x) { |
---|
| 158 | if (!arguments.length) return padding; |
---|
| 159 | |
---|
| 160 | function padFunction(node) { |
---|
| 161 | var p = x.call(treemap, node, node.depth); |
---|
| 162 | return p == null |
---|
| 163 | ? d3_layout_treemapPadNull(node) |
---|
| 164 | : d3_layout_treemapPad(node, typeof p === "number" ? [p, p, p, p] : p); |
---|
| 165 | } |
---|
| 166 | |
---|
| 167 | function padConstant(node) { |
---|
| 168 | return d3_layout_treemapPad(node, x); |
---|
| 169 | } |
---|
| 170 | |
---|
| 171 | var type; |
---|
| 172 | pad = (padding = x) == null ? d3_layout_treemapPadNull |
---|
| 173 | : (type = typeof x) === "function" ? padFunction |
---|
| 174 | : type === "number" ? (x = [x, x, x, x], padConstant) |
---|
| 175 | : padConstant; |
---|
| 176 | return treemap; |
---|
| 177 | }; |
---|
| 178 | |
---|
| 179 | treemap.round = function(x) { |
---|
| 180 | if (!arguments.length) return round != Number; |
---|
| 181 | round = x ? Math.round : Number; |
---|
| 182 | return treemap; |
---|
| 183 | }; |
---|
| 184 | |
---|
| 185 | treemap.sticky = function(x) { |
---|
| 186 | if (!arguments.length) return sticky; |
---|
| 187 | sticky = x; |
---|
| 188 | stickies = null; |
---|
| 189 | return treemap; |
---|
| 190 | }; |
---|
| 191 | |
---|
| 192 | treemap.ratio = function(x) { |
---|
| 193 | if (!arguments.length) return ratio; |
---|
| 194 | ratio = x; |
---|
| 195 | return treemap; |
---|
| 196 | }; |
---|
| 197 | |
---|
| 198 | return d3_layout_hierarchyRebind(treemap, hierarchy); |
---|
| 199 | }; |
---|
| 200 | |
---|
| 201 | function d3_layout_treemapPadNull(node) { |
---|
| 202 | return {x: node.x, y: node.y, dx: node.dx, dy: node.dy}; |
---|
| 203 | } |
---|
| 204 | |
---|
| 205 | function d3_layout_treemapPad(node, padding) { |
---|
| 206 | var x = node.x + padding[3], |
---|
| 207 | y = node.y + padding[0], |
---|
| 208 | dx = node.dx - padding[1] - padding[3], |
---|
| 209 | dy = node.dy - padding[0] - padding[2]; |
---|
| 210 | if (dx < 0) { x += dx / 2; dx = 0; } |
---|
| 211 | if (dy < 0) { y += dy / 2; dy = 0; } |
---|
| 212 | return {x: x, y: y, dx: dx, dy: dy}; |
---|
| 213 | } |
---|