Home | History | Annotate | Download | only in static-dashboards
      1 /*
      2  * Copyright (C) 2011 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 // Size of border around nodes.
     32 // We could support arbitrary borders using getComputedStyle(), but I am
     33 // skeptical the extra complexity (and performance hit) is worth it.
     34 var kBorderWidth = 1;
     35 
     36 // Padding around contents.
     37 // TODO: do this with a nested div to allow it to be CSS-styleable.
     38 var kPadding = 4;
     39 
     40 var focused = null;
     41 
     42 // Callback for embedding page to update after a focus.
     43 function handleFocus(tree) {}
     44 
     45 function focus(tree) {
     46   focused = tree;
     47 
     48   // Hide all visible siblings of all our ancestors by lowering them.
     49   var level = 0;
     50   var root = tree;
     51   while (root.parent) {
     52     root = root.parent;
     53     level += 1;
     54     for (var i = 0, sibling; sibling = root.children[i]; ++i) {
     55       if (sibling.dom)
     56         sibling.dom.style.zIndex = 0;
     57     }
     58   }
     59   var width = root.dom.offsetWidth;
     60   var height = root.dom.offsetHeight;
     61   // Unhide (raise) and maximize us and our ancestors.
     62   for (var t = tree; t.parent; t = t.parent) {
     63     // Shift off by border so we don't get nested borders.
     64     // TODO: actually make nested borders work (need to adjust width/height).
     65     position(t.dom, -kBorderWidth, -kBorderWidth, width, height);
     66     t.dom.style.zIndex = 1;
     67   }
     68   // And layout into the topmost box.
     69   layout(tree, level, width, height);
     70   handleFocus(tree);
     71 }
     72 
     73 function makeDom(tree, level) {
     74   var dom = document.createElement('div');
     75   dom.style.zIndex = 1;
     76   dom.className = 'webtreemap-node webtreemap-level' + Math.min(level, 4);
     77 
     78   dom.onmousedown = function(e) {
     79     if (e.button == 0) {
     80       if (focused && tree == focused && focused.parent) {
     81         focus(focused.parent);
     82       } else {
     83         focus(tree);
     84       }
     85     }
     86     e.stopPropagation();
     87     return true;
     88   };
     89 
     90   var caption = document.createElement('div');
     91   caption.className = 'webtreemap-caption';
     92   caption.innerHTML = tree.name;
     93   dom.appendChild(caption);
     94 
     95   tree.dom = dom;
     96   return dom;
     97 }
     98 
     99 function position(dom, x, y, width, height) {
    100   // CSS width/height does not include border.
    101   width -= kBorderWidth*2;
    102   height -= kBorderWidth*2;
    103 
    104   dom.style.left   = x + 'px';
    105   dom.style.top    = y + 'px';
    106   dom.style.width  = Math.max(width, 0) + 'px';
    107   dom.style.height = Math.max(height, 0) + 'px';
    108 }
    109 
    110 // Given a list of rectangles |nodes|, the 1-d space available
    111 // |space|, and a starting rectangle index |start|, compute an span of
    112 // rectangles that optimizes a pleasant aspect ratio.
    113 //
    114 // Returns [end, sum], where end is one past the last rectangle and sum is the
    115 // 2-d sum of the rectangles' areas.
    116 function selectSpan(nodes, space, start) {
    117   // Add rectangle one by one, stopping when aspect ratios begin to go
    118   // bad.  Result is [start,end) covering the best run for this span.
    119   // http://scholar.google.com/scholar?cluster=5972512107845615474
    120   var node = nodes[start];
    121   var rmin = node.data['$area'];  // Smallest seen child so far.
    122   var rmax = rmin;                // Largest child.
    123   var rsum = 0;                   // Sum of children in this span.
    124   var last_score = 0;             // Best score yet found.
    125   for (var end = start; node = nodes[end]; ++end) {
    126     var size = node.data['$area'];
    127     if (size < rmin)
    128       rmin = size;
    129     if (size > rmax)
    130       rmax = size;
    131     rsum += size;
    132 
    133     // This formula is from the paper, but you can easily prove to
    134     // yourself it's taking the larger of the x/y aspect ratio or the
    135     // y/x aspect ratio.  The additional magic fudge constant of 5
    136     // makes us prefer wider rectangles to taller ones.
    137     var score = Math.max(5*space*space*rmax / (rsum*rsum),
    138                          1*rsum*rsum / (space*space*rmin));
    139     if (last_score && score > last_score) {
    140       rsum -= size;  // Undo size addition from just above.
    141       break;
    142     }
    143     last_score = score;
    144   }
    145   return [end, rsum];
    146 }
    147 
    148 function layout(tree, level, width, height) {
    149   if (!('children' in tree))
    150     return;
    151 
    152   var total = tree.data['$area'];
    153 
    154   // XXX why do I need an extra -1/-2 here for width/height to look right?
    155   var x1 = 0, y1 = 0, x2 = width - 1, y2 = height - 2;
    156   x1 += kPadding; y1 += kPadding;
    157   x2 -= kPadding; y2 -= kPadding;
    158   y1 += 14;  // XXX get first child height for caption spacing
    159 
    160   var pixels_to_units = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
    161 
    162   for (var start = 0, child; child = tree.children[start]; ++start) {
    163     if (x2 - x1 < 60 || y2 - y1 < 40) {
    164       if (child.dom) {
    165         child.dom.style.zIndex = 0;
    166         position(child.dom, -2, -2, 0, 0);
    167       }
    168       continue;
    169     }
    170 
    171     // In theory we can dynamically decide whether to split in x or y based
    172     // on aspect ratio.  In practice, changing split direction with this
    173     // layout doesn't look very good.
    174     //   var ysplit = (y2 - y1) > (x2 - x1);
    175     var ysplit = true;
    176 
    177     var space;  // Space available along layout axis.
    178     if (ysplit)
    179       space = (y2 - y1) * pixels_to_units;
    180     else
    181       space = (x2 - x1) * pixels_to_units;
    182 
    183     var span = selectSpan(tree.children, space, start);
    184     var end = span[0], rsum = span[1];
    185 
    186     // Now that we've selected a span, lay out rectangles [start,end) in our
    187     // available space.
    188     var x = x1, y = y1;
    189     for (var i = start; i < end; ++i) {
    190       child = tree.children[i];
    191       if (!child.dom) {
    192         child.parent = tree;
    193         child.dom = makeDom(child, level + 1);
    194         tree.dom.appendChild(child.dom);
    195       } else {
    196         child.dom.style.zIndex = 1;
    197       }
    198       var size = child.data['$area'];
    199       var frac = size / rsum;
    200       if (ysplit) {
    201         width = rsum / space;
    202         height = size / width;
    203       } else {
    204         height = rsum / space;
    205         width = size / height;
    206       }
    207       width /= pixels_to_units;
    208       height /= pixels_to_units;
    209       width = Math.round(width);
    210       height = Math.round(height);
    211       position(child.dom, x, y, width, height);
    212       if ('children' in child) {
    213         layout(child, level + 1, width, height);
    214       }
    215       if (ysplit)
    216         y += height;
    217       else
    218         x += width;
    219     }
    220 
    221     // Shrink our available space based on the amount we used.
    222     if (ysplit)
    223       x1 += Math.round((rsum / space) / pixels_to_units);
    224     else
    225       y1 += Math.round((rsum / space) / pixels_to_units);
    226 
    227     // end points one past where we ended, which is where we want to
    228     // begin the next iteration, but subtract one to balance the ++ in
    229     // the loop.
    230     start = end - 1;
    231   }
    232 }
    233 
    234 function appendTreemap(dom, data) {
    235   var style = getComputedStyle(dom, null);
    236   var width = parseInt(style.width);
    237   var height = parseInt(style.height);
    238   if (!data.dom)
    239     makeDom(data, 0);
    240   dom.appendChild(data.dom);
    241   position(data.dom, 0, 0, width, height);
    242   layout(data, 0, width, height);
    243 }