UNPKG

7.94 kBJavaScriptView Raw
1// Vendored from https://github.com/rmmh/webtreemap/blob/9fa0c066a10ea4402d960b0c6c1a432846ac7fc4/webtreemap.js
2
3// Copyright 2013 Google Inc. All Rights Reserved.
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Size of border around nodes.
18// We could support arbitrary borders using getComputedStyle(), but I am
19// skeptical the extra complexity (and performance hit) is worth it.
20
21;(function() {
22var kBorderWidth = 1;
23
24// Padding around contents.
25// TODO: do this with a nested div to allow it to be CSS-styleable.
26var kPadding = 4;
27
28// x/y ratio to aim for -- wider rectangles are better for text display
29var kAspectRatio = 1.2;
30
31var focused = null;
32
33function focus(tree) {
34 focused = tree;
35
36 // Hide all visible siblings of all our ancestors by lowering them.
37 var level = 0;
38 var root = tree;
39 while (root.parent) {
40 root = root.parent;
41 level += 1;
42 for (var i = 0, sibling; sibling = root.children[i]; ++i) {
43 if (sibling.dom)
44 sibling.dom.style.zIndex = 0;
45 }
46 }
47 var width = root.dom.offsetWidth;
48 var height = root.dom.offsetHeight;
49 // Unhide (raise) and maximize us and our ancestors.
50 for (var t = tree; t.parent; t = t.parent) {
51 // Shift off by border so we don't get nested borders.
52 // TODO: actually make nested borders work (need to adjust width/height).
53 position(t.dom, -kBorderWidth, -kBorderWidth, width, height);
54 t.dom.style.zIndex = 1;
55 }
56 // And layout into the topmost box.
57 layout(tree, level, width, height);
58}
59
60function makeDom(tree, level) {
61 var dom = document.createElement('div');
62 dom.style.zIndex = 1;
63 dom.className = 'webtreemap-node webtreemap-level' + Math.min(level, 4);
64 if (tree.data['$symbol']) {
65 dom.className += (' webtreemap-symbol-' +
66 tree.data['$symbol'].replace(' ', '_'));
67 }
68 if (tree.data['$dominant_symbol']) {
69 dom.className += (' webtreemap-symbol-' +
70 tree.data['$dominant_symbol'].replace(' ', '_'));
71 dom.className += (' webtreemap-aggregate');
72 }
73
74 dom.onmousedown = function(e) {
75 if (e.button == 0) {
76 if (focused && tree == focused && focused.parent) {
77 focus(focused.parent);
78 } else {
79 focus(tree);
80 }
81 }
82 e.stopPropagation();
83 return true;
84 };
85
86 var caption = document.createElement('div');
87 caption.className = 'webtreemap-caption';
88 caption.innerHTML = tree.name;
89 dom.appendChild(caption);
90 dom.title = tree.name;
91
92 tree.dom = dom;
93 return dom;
94}
95
96function position(dom, x, y, width, height) {
97 // CSS width/height does not include border.
98 width -= kBorderWidth*2;
99 height -= kBorderWidth*2;
100
101 dom.style.left = x + 'px';
102 dom.style.top = y + 'px';
103 dom.style.width = Math.max(width, 0) + 'px';
104 dom.style.height = Math.max(height, 0) + 'px';
105}
106
107// Given a list of rectangles |nodes|, the 1-d space available
108// |space|, and a starting rectangle index |start|, compute an span of
109// rectangles that optimizes a pleasant aspect ratio.
110//
111// Returns [end, sum], where end is one past the last rectangle and sum is the
112// 2-d sum of the rectangles' areas.
113function selectSpan(nodes, space, start) {
114 // Add rectangle one by one, stopping when aspect ratios begin to go
115 // bad. Result is [start,end) covering the best run for this span.
116 // http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.36.6685
117 var node = nodes[start];
118 var rmin = node.data['$area']; // Smallest seen child so far.
119 var rmax = rmin; // Largest child.
120 var rsum = 0; // Sum of children in this span.
121 var last_score = 0; // Best score yet found.
122 for (var end = start; node = nodes[end]; ++end) {
123 var size = node.data['$area'];
124 if (size < rmin)
125 rmin = size;
126 if (size > rmax)
127 rmax = size;
128 rsum += size;
129
130 // This formula is from the paper, but you can easily prove to
131 // yourself it's taking the larger of the x/y aspect ratio or the
132 // y/x aspect ratio. The additional magic fudge constant of kAspectRatio
133 // lets us prefer wider rectangles to taller ones.
134 var score = Math.max(space*space*rmax / (rsum*rsum),
135 kAspectRatio*rsum*rsum / (space*space*rmin));
136 if (last_score && score > last_score) {
137 rsum -= size; // Undo size addition from just above.
138 break;
139 }
140 last_score = score;
141 }
142 return [end, rsum];
143}
144
145function layout(tree, level, width, height) {
146 if (!('children' in tree))
147 return;
148
149 var total = tree.data['$area'];
150
151 // XXX why do I need an extra -1/-2 here for width/height to look right?
152 var x1 = 0, y1 = 0, x2 = width - 1, y2 = height - 2;
153 x1 += kPadding; y1 += kPadding;
154 x2 -= kPadding; y2 -= kPadding;
155 y1 += 14; // XXX get first child height for caption spacing
156
157 var pixels_to_units = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
158
159 // The algorithm does best at laying out items from largest to smallest.
160 // Sort them to ensure this.
161 if (!tree.children.sorted) {
162 tree.children.sort(function (a, b) {
163 return b.data['$area'] - a.data['$area'];
164 });
165 tree.children.sorted = true;
166 }
167
168 for (var start = 0, child; child = tree.children[start]; ++start) {
169 if (x2 - x1 < 60 || y2 - y1 < 40) {
170 if (child.dom) {
171 child.dom.style.zIndex = 0;
172 position(child.dom, -2, -2, 0, 0);
173 }
174 continue;
175 }
176
177 // Dynamically decide whether to split in x or y based on aspect ratio.
178 var ysplit = ((y2 - y1) / (x2 - x1)) > kAspectRatio;
179
180 var space; // Space available along layout axis.
181 if (ysplit)
182 space = (y2 - y1) * pixels_to_units;
183 else
184 space = (x2 - x1) * pixels_to_units;
185
186 var span = selectSpan(tree.children, space, start);
187 var end = span[0], rsum = span[1];
188
189 // Now that we've selected a span, lay out rectangles [start,end) in our
190 // available space.
191 var x = x1, y = y1;
192 for (var i = start; i < end; ++i) {
193 child = tree.children[i];
194 if (!child.dom) {
195 child.parent = tree;
196 child.dom = makeDom(child, level + 1);
197 tree.dom.appendChild(child.dom);
198 } else {
199 child.dom.style.zIndex = 1;
200 }
201 var size = child.data['$area'];
202 var frac = size / rsum;
203 if (ysplit) {
204 width = rsum / space;
205 height = size / width;
206 } else {
207 height = rsum / space;
208 width = size / height;
209 }
210 width /= pixels_to_units;
211 height /= pixels_to_units;
212 width = Math.round(width);
213 height = Math.round(height);
214 position(child.dom, x, y, width, height);
215 if ('children' in child) {
216 layout(child, level + 1, width, height);
217 }
218 if (ysplit)
219 y += height;
220 else
221 x += width;
222 }
223
224 // Shrink our available space based on the amount we used.
225 if (ysplit)
226 x1 += Math.round((rsum / space) / pixels_to_units);
227 else
228 y1 += Math.round((rsum / space) / pixels_to_units);
229
230 // end points one past where we ended, which is where we want to
231 // begin the next iteration, but subtract one to balance the ++ in
232 // the loop.
233 start = end - 1;
234 }
235}
236
237function appendTreemap(dom, data) {
238 var style = getComputedStyle(dom, null);
239 var width = parseInt(style.width);
240 var height = parseInt(style.height);
241 if (!data.dom)
242 makeDom(data, 0);
243 dom.appendChild(data.dom);
244 position(data.dom, 0, 0, width, height);
245 layout(data, 0, width, height);
246}
247
248window.appendTreemap = appendTreemap;
249})(window);