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