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