1 // Copyright 2013 Google Inc. All Rights Reserved.
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 // Size of border around nodes.
16 // We could support arbitrary borders using getComputedStyle(), but I am
17 // skeptical the extra complexity (and performance hit) is worth it.
20 // Padding around contents.
21 // TODO: do this with a nested div to allow it to be CSS-styleable.
26 function focus(tree
) {
29 // Hide all visible siblings of all our ancestors by lowering them.
35 for (var i
= 0, sibling
; sibling
= root
.children
[i
]; ++i
) {
37 sibling
.dom
.style
.zIndex
= 0;
40 var width
= root
.dom
.offsetWidth
;
41 var height
= root
.dom
.offsetHeight
;
42 // Unhide (raise) and maximize us and our ancestors.
43 for (var t
= tree
; t
.parent
; t
= t
.parent
) {
44 // Shift off by border so we don't get nested borders.
45 // TODO: actually make nested borders work (need to adjust width/height).
46 position(t
.dom
, -kBorderWidth
, -kBorderWidth
, width
, height
);
47 t
.dom
.style
.zIndex
= 1;
49 // And layout into the topmost box.
50 layout(tree
, level
, width
, height
);
53 function makeDom(tree
, level
) {
54 var dom
= document
.createElement('div');
56 dom
.className
= 'webtreemap-node webtreemap-level' + Math
.min(level
, 4);
57 if (tree
.data
['$symbol']) {
58 dom
.className
+= (' webtreemap-symbol-' +
59 tree
.data
['$symbol'].replace(' ', '_'));
61 if (tree
.data
['$dominant_symbol']) {
62 dom
.className
+= (' webtreemap-symbol-' +
63 tree
.data
['$dominant_symbol'].replace(' ', '_'));
64 dom
.className
+= (' webtreemap-aggregate');
67 dom
.onmousedown = function(e
) {
69 if (focused
&& tree
== focused
&& focused
.parent
) {
70 focus(focused
.parent
);
79 var caption
= document
.createElement('div');
80 caption
.className
= 'webtreemap-caption';
81 caption
.innerHTML
= tree
.name
;
82 dom
.appendChild(caption
);
88 function position(dom
, x
, y
, width
, height
) {
89 // CSS width/height does not include border.
90 width
-= kBorderWidth
*2;
91 height
-= kBorderWidth
*2;
93 dom
.style
.left
= x
+ 'px';
94 dom
.style
.top
= y
+ 'px';
95 dom
.style
.width
= Math
.max(width
, 0) + 'px';
96 dom
.style
.height
= Math
.max(height
, 0) + 'px';
99 // Given a list of rectangles |nodes|, the 1-d space available
100 // |space|, and a starting rectangle index |start|, compute an span of
101 // rectangles that optimizes a pleasant aspect ratio.
103 // Returns [end, sum], where end is one past the last rectangle and sum is the
104 // 2-d sum of the rectangles' areas.
105 function selectSpan(nodes
, space
, start
) {
106 // Add rectangle one by one, stopping when aspect ratios begin to go
107 // bad. Result is [start,end) covering the best run for this span.
108 // http://scholar.google.com/scholar?cluster=5972512107845615474
109 var node
= nodes
[start
];
110 var rmin
= node
.data
['$area']; // Smallest seen child so far.
111 var rmax
= rmin
; // Largest child.
112 var rsum
= 0; // Sum of children in this span.
113 var last_score
= 0; // Best score yet found.
114 for (var end
= start
; node
= nodes
[end
]; ++end
) {
115 var size
= node
.data
['$area'];
122 // This formula is from the paper, but you can easily prove to
123 // yourself it's taking the larger of the x/y aspect ratio or the
124 // y/x aspect ratio. The additional magic fudge constant of 5
125 // makes us prefer wider rectangles to taller ones.
126 var score
= Math
.max(5*space
*space
*rmax
/ (rsum
*rsum
),
127 1*rsum
*rsum
/ (space
*space
*rmin
));
128 if (last_score
&& score
> last_score
) {
129 rsum
-= size
; // Undo size addition from just above.
137 function layout(tree
, level
, width
, height
) {
138 if (!('children' in tree
))
141 var total
= tree
.data
['$area'];
143 // XXX why do I need an extra -1/-2 here for width/height to look right?
144 var x1
= 0, y1
= 0, x2
= width
- 1, y2
= height
- 2;
145 x1
+= kPadding
; y1
+= kPadding
;
146 x2
-= kPadding
; y2
-= kPadding
;
147 y1
+= 14; // XXX get first child height for caption spacing
149 var pixels_to_units
= Math
.sqrt(total
/ ((x2
- x1
) * (y2
- y1
)));
151 for (var start
= 0, child
; child
= tree
.children
[start
]; ++start
) {
152 if (x2
- x1
< 60 || y2
- y1
< 40) {
154 child
.dom
.style
.zIndex
= 0;
155 position(child
.dom
, -2, -2, 0, 0);
160 // In theory we can dynamically decide whether to split in x or y based
161 // on aspect ratio. In practice, changing split direction with this
162 // layout doesn't look very good.
163 // var ysplit = (y2 - y1) > (x2 - x1);
166 var space
; // Space available along layout axis.
168 space
= (y2
- y1
) * pixels_to_units
;
170 space
= (x2
- x1
) * pixels_to_units
;
172 var span
= selectSpan(tree
.children
, space
, start
);
173 var end
= span
[0], rsum
= span
[1];
175 // Now that we've selected a span, lay out rectangles [start,end) in our
178 for (var i
= start
; i
< end
; ++i
) {
179 child
= tree
.children
[i
];
182 child
.dom
= makeDom(child
, level
+ 1);
183 tree
.dom
.appendChild(child
.dom
);
185 child
.dom
.style
.zIndex
= 1;
187 var size
= child
.data
['$area'];
188 var frac
= size
/ rsum
;
190 width
= rsum
/ space
;
191 height
= size
/ width
;
193 height
= rsum
/ space
;
194 width
= size
/ height
;
196 width
/= pixels_to_units
;
197 height
/= pixels_to_units
;
198 width
= Math
.round(width
);
199 height
= Math
.round(height
);
200 position(child
.dom
, x
, y
, width
, height
);
201 if ('children' in child
) {
202 layout(child
, level
+ 1, width
, height
);
210 // Shrink our available space based on the amount we used.
212 x1
+= Math
.round((rsum
/ space
) / pixels_to_units
);
214 y1
+= Math
.round((rsum
/ space
) / pixels_to_units
);
216 // end points one past where we ended, which is where we want to
217 // begin the next iteration, but subtract one to balance the ++ in
223 function appendTreemap(dom
, data
) {
224 var style
= getComputedStyle(dom
, null);
225 var width
= parseInt(style
.width
);
226 var height
= parseInt(style
.height
);
229 dom
.appendChild(data
.dom
);
230 position(data
.dom
, 0, 0, width
, height
);
231 layout(data
, 0, width
, height
);