1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO:
6// 1. Visibility functions: base on boxPadding.t, not 15
7// 2. Track a maxDisplayDepth that is user-settable:
8//    maxDepth == currentRoot.depth + maxDisplayDepth
9function D3SymbolTreeMap(mapWidth, mapHeight, levelsToShow) {
10  this._mapContainer = undefined;
11  this._mapWidth = mapWidth;
12  this._mapHeight = mapHeight;
13  this.boxPadding = {'l': 5, 'r': 5, 't': 20, 'b': 5};
14  this.infobox = undefined;
15  this._maskContainer = undefined;
16  this._highlightContainer = undefined;
17  // Transition in this order:
18  // 1. Exiting items go away.
19  // 2. Updated items move.
20  // 3. New items enter.
21  this._exitDuration=500;
22  this._updateDuration=500;
23  this._enterDuration=500;
24  this._firstTransition=true;
25  this._layout = undefined;
26  this._currentRoot = undefined;
27  this._currentNodes = undefined;
28  this._treeData = undefined;
29  this._maxLevelsToShow = levelsToShow;
30  this._currentMaxDepth = this._maxLevelsToShow;
31}
32
33/**
34 * Make a number pretty, with comma separators.
35 */
36D3SymbolTreeMap._pretty = function(num) {
37  var asString = String(num);
38  var result = '';
39  var counter = 0;
40  for (var x = asString.length - 1; x >= 0; x--) {
41    counter++;
42    if (counter === 4) {
43      result = ',' + result;
44      counter = 1;
45    }
46    result = asString.charAt(x) + result;
47  }
48  return result;
49}
50
51/**
52 * Express a number in terms of KiB, MiB, GiB, etc.
53 * Note that these are powers of 2, not of 10.
54 */
55D3SymbolTreeMap._byteify = function(num) {
56  var suffix;
57  if (num >= 1024) {
58    if (num >= 1024 * 1024 * 1024) {
59      suffix = 'GiB';
60      num = num / (1024 * 1024 * 1024);
61    } else if (num >= 1024 * 1024) {
62      suffix = 'MiB';
63      num = num / (1024 * 1024);
64    } else if (num >= 1024) {
65      suffix = 'KiB'
66      num = num / 1024;
67    }
68    return num.toFixed(2) + ' ' + suffix;
69  }
70  return num + ' B';
71}
72
73D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS = {
74  // Definitions concisely derived from the nm 'man' page
75  'A': 'Global absolute (A)',
76  'B': 'Global uninitialized data (B)',
77  'b': 'Local uninitialized data (b)',
78  'C': 'Global uninitialized common (C)',
79  'D': 'Global initialized data (D)',
80  'd': 'Local initialized data (d)',
81  'G': 'Global small initialized data (G)',
82  'g': 'Local small initialized data (g)',
83  'i': 'Indirect function (i)',
84  'N': 'Debugging (N)',
85  'p': 'Stack unwind (p)',
86  'R': 'Global read-only data (R)',
87  'r': 'Local read-only data (r)',
88  'S': 'Global small uninitialized data (S)',
89  's': 'Local small uninitialized data (s)',
90  'T': 'Global code (T)',
91  't': 'Local code (t)',
92  'U': 'Undefined (U)',
93  'u': 'Unique (u)',
94  'V': 'Global weak object (V)',
95  'v': 'Local weak object (v)',
96  'W': 'Global weak symbol (W)',
97  'w': 'Local weak symbol (w)',
98  '@': 'Vtable entry (@)', // non-standard, hack.
99  '-': 'STABS debugging (-)',
100  '?': 'Unrecognized (?)',
101};
102D3SymbolTreeMap._NM_SYMBOL_TYPES = '';
103for (var symbol_type in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
104  D3SymbolTreeMap._NM_SYMBOL_TYPES += symbol_type;
105}
106
107/**
108 * Given a symbol type code, look up and return a human-readable description
109 * of that symbol type. If the symbol type does not match one of the known
110 * types, the unrecognized description (corresponding to symbol type '?') is
111 * returned instead of null or undefined.
112 */
113D3SymbolTreeMap._getSymbolDescription = function(type) {
114  var result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS[type];
115  if (result === undefined) {
116    result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS['?'];
117  }
118  return result;
119}
120
121// Qualitative 12-value pastel Brewer palette.
122D3SymbolTreeMap._colorArray = [
123  'rgb(141,211,199)',
124  'rgb(255,255,179)',
125  'rgb(190,186,218)',
126  'rgb(251,128,114)',
127  'rgb(128,177,211)',
128  'rgb(253,180,98)',
129  'rgb(179,222,105)',
130  'rgb(252,205,229)',
131  'rgb(217,217,217)',
132  'rgb(188,128,189)',
133  'rgb(204,235,197)',
134  'rgb(255,237,111)'];
135
136D3SymbolTreeMap._initColorMap = function() {
137  var map = {};
138  var numColors = D3SymbolTreeMap._colorArray.length;
139  var count = 0;
140  for (var key in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
141    var index = count++ % numColors;
142    map[key] = d3.rgb(D3SymbolTreeMap._colorArray[index]);
143  }
144  D3SymbolTreeMap._colorMap = map;
145}
146D3SymbolTreeMap._initColorMap();
147
148D3SymbolTreeMap.getColorForType = function(type) {
149  var result = D3SymbolTreeMap._colorMap[type];
150  if (result === undefined) return d3.rgb('rgb(255,255,255)');
151  return result;
152}
153
154D3SymbolTreeMap.prototype.init = function() {
155  this.infobox = this._createInfoBox();
156  this._mapContainer = d3.select('body').append('div')
157      .style('position', 'relative')
158      .style('width', this._mapWidth)
159      .style('height', this._mapHeight)
160      .style('padding', 0)
161      .style('margin', 0)
162      .style('box-shadow', '5px 5px 5px #888');
163  this._layout = this._createTreeMapLayout();
164  this._setData(tree_data); // TODO: Don't use global 'tree_data'
165}
166
167/**
168 * Sets the data displayed by the treemap and layint out the map.
169 */
170D3SymbolTreeMap.prototype._setData = function(data) {
171  this._treeData = data;
172  console.time('_crunchStats');
173  this._crunchStats(data);
174  console.timeEnd('_crunchStats');
175  this._currentRoot = this._treeData;
176  this._currentNodes = this._layout.nodes(this._currentRoot);
177  this._currentMaxDepth = this._maxLevelsToShow;
178  this._doLayout();
179}
180
181/**
182 * Recursively traverses the entire tree starting from the specified node,
183 * computing statistics and recording metadata as it goes. Call this method
184 * only once per imported tree.
185 */
186D3SymbolTreeMap.prototype._crunchStats = function(node) {
187  var stack = [];
188  stack.idCounter = 0;
189  this._crunchStatsHelper(stack, node);
190}
191
192/**
193 * Invoke the specified visitor function on all data elements currently shown
194 * in the treemap including any and all of their children, starting at the
195 * currently-displayed root and descening recursively. The function will be
196 * passed the datum element representing each node. No traversal guarantees
197 * are made.
198 */
199D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) {
200  this._visit(this._currentRoot, visitor);
201}
202
203/**
204 * Helper function for visit functions.
205 */
206D3SymbolTreeMap.prototype._visit = function(datum, visitor) {
207  visitor.call(this, datum);
208  if (datum.children) for (var i = 0; i < datum.children.length; i++) {
209    this._visit(datum.children[i], visitor);
210  }
211}
212
213D3SymbolTreeMap.prototype._crunchStatsHelper = function(stack, node) {
214  // Only overwrite the node ID if it isn't already set.
215  // This allows stats to be crunched multiple times on subsets of data
216  // without breaking the data-to-ID bindings. New nodes get new IDs.
217  if (node.id === undefined) node.id = stack.idCounter++;
218  if (node.children === undefined) {
219    // Leaf node (symbol); accumulate stats.
220    for (var i = 0; i < stack.length; i++) {
221      var ancestor = stack[i];
222      if (!ancestor.symbol_stats) ancestor.symbol_stats = {};
223      if (ancestor.symbol_stats[node.t] === undefined) {
224        // New symbol type we haven't seen before, just record.
225        ancestor.symbol_stats[node.t] = {'count': 1,
226                                         'size': node.value};
227      } else {
228        // Existing symbol type, increment.
229        ancestor.symbol_stats[node.t].count++;
230        ancestor.symbol_stats[node.t].size += node.value;
231      }
232    }
233  } else for (var i = 0; i < node.children.length; i++) {
234    stack.push(node);
235    this._crunchStatsHelper(stack, node.children[i]);
236    stack.pop();
237  }
238}
239
240D3SymbolTreeMap.prototype._createTreeMapLayout = function() {
241  var result = d3.layout.treemap()
242      .padding([this.boxPadding.t, this.boxPadding.r,
243                this.boxPadding.b, this.boxPadding.l])
244      .size([this._mapWidth, this._mapHeight]);
245  return result;
246}
247
248D3SymbolTreeMap.prototype.resize = function(width, height) {
249  this._mapWidth = width;
250  this._mapHeight = height;
251  this._mapContainer.style('width', width).style('height', height);
252  this._layout.size([this._mapWidth, this._mapHeight]);
253  this._currentNodes = this._layout.nodes(this._currentRoot);
254  this._doLayout();
255}
256
257D3SymbolTreeMap.prototype._zoomDatum = function(datum) {
258  if (this._currentRoot === datum) return; // already here
259  this._hideHighlight(datum);
260  this._hideInfoBox(datum);
261  this._currentRoot = datum;
262  this._currentNodes = this._layout.nodes(this._currentRoot);
263  this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
264  console.log('zooming into datum ' + this._currentRoot.n);
265  this._doLayout();
266}
267
268D3SymbolTreeMap.prototype.setMaxLevels = function(levelsToShow) {
269  this._maxLevelsToShow = levelsToShow;
270  this._currentNodes = this._layout.nodes(this._currentRoot);
271  this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
272  console.log('setting max levels to show: ' + this._maxLevelsToShow);
273  this._doLayout();
274}
275
276/**
277 * Clone the specified tree, returning an independent copy of the data.
278 * Only the original attributes expected to exist prior to invoking
279 * _crunchStatsHelper are retained, with the exception of the 'id' attribute
280 * (which must be retained for proper transitions).
281 * If the optional filter parameter is provided, it will be called with 'this'
282 * set to this treemap instance and passed the 'datum' object as an argument.
283 * When specified, the copy will retain only the data for which the filter
284 * function returns true.
285 */
286D3SymbolTreeMap.prototype._clone = function(datum, filter) {
287  var trackingStats = false;
288  if (this.__cloneState === undefined) {
289    console.time('_clone');
290    trackingStats = true;
291    this.__cloneState = {'accepted': 0, 'rejected': 0,
292                         'forced': 0, 'pruned': 0};
293  }
294
295  // Must go depth-first. All parents of children that are accepted by the
296  // filter must be preserved!
297  var copy = {'n': datum.n, 'k': datum.k};
298  var childAccepted = false;
299  if (datum.children !== undefined) {
300    for (var i = 0; i < datum.children.length; i++) {
301      var copiedChild = this._clone(datum.children[i], filter);
302      if (copiedChild !== undefined) {
303        childAccepted = true; // parent must also be accepted.
304        if (copy.children === undefined) copy.children = [];
305        copy.children.push(copiedChild);
306      }
307    }
308  }
309
310  // Ignore nodes that don't match the filter, when present.
311  var accept = false;
312  if (childAccepted) {
313    // Parent of an accepted child must also be accepted.
314    this.__cloneState.forced++;
315    accept = true;
316  } else if (filter !== undefined && filter.call(this, datum) !== true) {
317    this.__cloneState.rejected++;
318  } else if (datum.children === undefined) {
319    // Accept leaf nodes that passed the filter
320    this.__cloneState.accepted++;
321    accept = true;
322  } else {
323    // Non-leaf node. If no children are accepted, prune it.
324    this.__cloneState.pruned++;
325  }
326
327  if (accept) {
328    if (datum.id !== undefined) copy.id = datum.id;
329    if (datum.lastPathElement !== undefined) {
330      copy.lastPathElement = datum.lastPathElement;
331    }
332    if (datum.t !== undefined) copy.t = datum.t;
333    if (datum.value !== undefined && datum.children === undefined) {
334      copy.value = datum.value;
335    }
336  } else {
337    // Discard the copy we were going to return
338    copy = undefined;
339  }
340
341  if (trackingStats === true) {
342    // We are the fist call in the recursive chain.
343    console.timeEnd('_clone');
344    var totalAccepted = this.__cloneState.accepted +
345                        this.__cloneState.forced;
346    console.log(
347        totalAccepted + ' nodes retained (' +
348        this.__cloneState.forced + ' forced by accepted children, ' +
349        this.__cloneState.accepted + ' accepted on their own merits), ' +
350        this.__cloneState.rejected + ' nodes (and their children) ' +
351                                     'filtered out,' +
352        this.__cloneState.pruned + ' nodes pruned because because no ' +
353                                   'children remained.');
354    delete this.__cloneState;
355  }
356  return copy;
357}
358
359D3SymbolTreeMap.prototype.filter = function(filter) {
360  // Ensure we have a copy of the original root.
361  if (this._backupTree === undefined) this._backupTree = this._treeData;
362  this._mapContainer.selectAll('div').remove();
363  this._setData(this._clone(this._backupTree, filter));
364}
365
366D3SymbolTreeMap.prototype._doLayout = function() {
367  console.time('_doLayout');
368  this._handleInodes();
369  this._handleLeaves();
370  this._firstTransition = false;
371  console.timeEnd('_doLayout');
372}
373
374D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) {
375  this._showHighlight(datum, selection);
376}
377
378D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) {
379  this._hideHighlight(datum, selection);
380}
381
382D3SymbolTreeMap.prototype._handleInodes = function() {
383  console.time('_handleInodes');
384  var thisTreeMap = this;
385  var inodes = this._currentNodes.filter(function(datum){
386    return (datum.depth <= thisTreeMap._currentMaxDepth) &&
387            datum.children !== undefined;
388  });
389  var cellsEnter = this._mapContainer.selectAll('div.inode')
390      .data(inodes, function(datum) { return datum.id; })
391      .enter()
392      .append('div').attr('class', 'inode').attr('id', function(datum){
393          return 'node-' + datum.id;});
394
395
396  // Define enter/update/exit for inodes
397  cellsEnter
398      .append('div')
399      .attr('class', 'rect inode_rect_entering')
400      .style('z-index', function(datum) { return datum.id * 2; })
401      .style('position', 'absolute')
402      .style('left', function(datum) { return datum.x; })
403      .style('top', function(datum){ return datum.y; })
404      .style('width', function(datum){ return datum.dx; })
405      .style('height', function(datum){ return datum.dy; })
406      .style('opacity', '0')
407      .style('border', '1px solid black')
408      .style('background-image', function(datum) {
409        return thisTreeMap._makeSymbolBucketBackgroundImage.call(
410               thisTreeMap, datum);
411      })
412      .style('background-color', function(datum) {
413        if (datum.t === undefined) return 'rgb(220,220,220)';
414        return D3SymbolTreeMap.getColorForType(datum.t).toString();
415      })
416      .on('mouseover', function(datum){
417        thisTreeMap._highlightElement.call(
418            thisTreeMap, datum, d3.select(this));
419        thisTreeMap._showInfoBox.call(thisTreeMap, datum);
420      })
421      .on('mouseout', function(datum){
422        thisTreeMap._unhighlightElement.call(
423            thisTreeMap, datum, d3.select(this));
424        thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
425      })
426      .on('mousemove', function(){
427          thisTreeMap._moveInfoBox.call(thisTreeMap, event);
428      })
429      .on('dblclick', function(datum){
430        if (datum !== thisTreeMap._currentRoot) {
431          // Zoom into the selection
432          thisTreeMap._zoomDatum(datum);
433        } else if (datum.parent) {
434          console.log('event.shiftKey=' + event.shiftKey);
435          if (event.shiftKey === true) {
436            // Back to root
437            thisTreeMap._zoomDatum(thisTreeMap._treeData);
438          } else {
439            // Zoom out of the selection
440            thisTreeMap._zoomDatum(datum.parent);
441          }
442        }
443      });
444  cellsEnter
445      .append('div')
446      .attr('class', 'label inode_label_entering')
447      .style('z-index', function(datum) { return (datum.id * 2) + 1; })
448      .style('position', 'absolute')
449      .style('left', function(datum){ return datum.x; })
450      .style('top', function(datum){ return datum.y; })
451      .style('width', function(datum) { return datum.dx; })
452      .style('height', function(datum) { return thisTreeMap.boxPadding.t; })
453      .style('opacity', '0')
454      .style('pointer-events', 'none')
455      .style('-webkit-user-select', 'none')
456      .style('overflow', 'hidden') // required for ellipsis
457      .style('white-space', 'nowrap') // required for ellipsis
458      .style('text-overflow', 'ellipsis')
459      .style('text-align', 'center')
460      .style('vertical-align', 'top')
461      .style('visibility', function(datum) {
462        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
463      })
464      .text(function(datum) {
465        var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']'
466        var text;
467        if (datum.k === 'b') { // bucket
468          if (datum === thisTreeMap._currentRoot) {
469            text = thisTreeMap.pathFor(datum) + ': '
470                + D3SymbolTreeMap._getSymbolDescription(datum.t)
471          } else {
472            text = D3SymbolTreeMap._getSymbolDescription(datum.t);
473          }
474        } else if (datum === thisTreeMap._currentRoot) {
475          // The top-most level should always show the complete path
476          text = thisTreeMap.pathFor(datum);
477        } else {
478          // Anything that isn't a bucket or a leaf (symbol) or the
479          // current root should just show its name.
480          text = datum.n;
481        }
482        return text + sizeish;
483      }
484  );
485
486  // Complicated transition logic:
487  // For nodes that are entering, we want to fade them in in-place AFTER
488  // any adjusting nodes have resized and moved around. That way, new nodes
489  // seamlessly appear in the right spot after their containers have resized
490  // and moved around.
491  // To do this we do some trickery:
492  // 1. Define a '_entering' class on the entering elements
493  // 2. Use this to select only the entering elements and apply the opacity
494  //    transition.
495  // 3. Use the same transition to drop the '_entering' suffix, so that they
496  //    will correctly update in later zoom/resize/whatever operations.
497  // 4. The update transition is achieved by selecting the elements without
498  //    the '_entering_' suffix and applying movement and resizing transition
499  //    effects.
500  this._mapContainer.selectAll('div.inode_rect_entering').transition()
501      .duration(thisTreeMap._enterDuration).delay(
502          this._firstTransition ? 0 : thisTreeMap._exitDuration +
503              thisTreeMap._updateDuration)
504      .attr('class', 'rect inode_rect')
505      .style('opacity', '1')
506  this._mapContainer.selectAll('div.inode_label_entering').transition()
507      .duration(thisTreeMap._enterDuration).delay(
508          this._firstTransition ? 0 : thisTreeMap._exitDuration +
509              thisTreeMap._updateDuration)
510      .attr('class', 'label inode_label')
511      .style('opacity', '1')
512  this._mapContainer.selectAll('div.inode_rect').transition()
513      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
514      .style('opacity', '1')
515      .style('background-image', function(datum) {
516        return thisTreeMap._makeSymbolBucketBackgroundImage.call(
517            thisTreeMap, datum);
518      })
519      .style('left', function(datum) { return datum.x; })
520      .style('top', function(datum){ return datum.y; })
521      .style('width', function(datum){ return datum.dx; })
522      .style('height', function(datum){ return datum.dy; });
523  this._mapContainer.selectAll('div.inode_label').transition()
524      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
525      .style('opacity', '1')
526      .style('visibility', function(datum) {
527        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
528      })
529      .style('left', function(datum){ return datum.x; })
530      .style('top', function(datum){ return datum.y; })
531      .style('width', function(datum) { return datum.dx; })
532      .style('height', function(datum) { return thisTreeMap.boxPadding.t; })
533      .text(function(datum) {
534        var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']'
535        var text;
536        if (datum.k === 'b') {
537          if (datum === thisTreeMap._currentRoot) {
538            text = thisTreeMap.pathFor(datum) + ': ' +
539                D3SymbolTreeMap._getSymbolDescription(datum.t)
540          } else {
541            text = D3SymbolTreeMap._getSymbolDescription(datum.t);
542          }
543        } else if (datum === thisTreeMap._currentRoot) {
544          // The top-most level should always show the complete path
545          text = thisTreeMap.pathFor(datum);
546        } else {
547          // Anything that isn't a bucket or a leaf (symbol) or the
548          // current root should just show its name.
549          text = datum.n;
550        }
551        return text + sizeish;
552      });
553  var exit = this._mapContainer.selectAll('div.inode')
554      .data(inodes, function(datum) { return 'inode-' + datum.id; })
555      .exit();
556  exit.selectAll('div.inode_rect').transition().duration(
557      thisTreeMap._exitDuration).style('opacity', 0);
558  exit.selectAll('div.inode_label').transition().duration(
559      thisTreeMap._exitDuration).style('opacity', 0);
560  exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
561
562  console.log(inodes.length + ' inodes layed out.');
563  console.timeEnd('_handleInodes');
564}
565
566D3SymbolTreeMap.prototype._handleLeaves = function() {
567  console.time('_handleLeaves');
568  var color_fn = d3.scale.category10();
569  var thisTreeMap = this;
570  var leaves = this._currentNodes.filter(function(datum){
571    return (datum.depth <= thisTreeMap._currentMaxDepth) &&
572        datum.children === undefined; });
573  var cellsEnter = this._mapContainer.selectAll('div.leaf')
574      .data(leaves, function(datum) { return datum.id; })
575      .enter()
576      .append('div').attr('class', 'leaf').attr('id', function(datum){
577        return 'node-' + datum.id;
578      });
579
580  // Define enter/update/exit for leaves
581  cellsEnter
582      .append('div')
583      .attr('class', 'rect leaf_rect_entering')
584      .style('z-index', function(datum) { return datum.id * 2; })
585      .style('position', 'absolute')
586      .style('left', function(datum){ return datum.x; })
587      .style('top', function(datum){ return datum.y; })
588      .style('width', function(datum){ return datum.dx; })
589      .style('height', function(datum){ return datum.dy; })
590      .style('opacity', '0')
591      .style('background-color', function(datum) {
592        if (datum.t === undefined) return 'rgb(220,220,220)';
593        return D3SymbolTreeMap.getColorForType(datum.t)
594            .darker(0.3).toString();
595      })
596      .style('border', '1px solid black')
597      .on('mouseover', function(datum){
598        thisTreeMap._highlightElement.call(
599            thisTreeMap, datum, d3.select(this));
600        thisTreeMap._showInfoBox.call(thisTreeMap, datum);
601      })
602      .on('mouseout', function(datum){
603        thisTreeMap._unhighlightElement.call(
604            thisTreeMap, datum, d3.select(this));
605        thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
606      })
607      .on('mousemove', function(){ thisTreeMap._moveInfoBox.call(
608        thisTreeMap, event);
609      });
610  cellsEnter
611      .append('div')
612      .attr('class', 'label leaf_label_entering')
613      .style('z-index', function(datum) { return (datum.id * 2) + 1; })
614      .style('position', 'absolute')
615      .style('left', function(datum){ return datum.x; })
616      .style('top', function(datum){ return datum.y; })
617      .style('width', function(datum) { return datum.dx; })
618      .style('height', function(datum) { return datum.dy; })
619      .style('opacity', '0')
620      .style('pointer-events', 'none')
621      .style('-webkit-user-select', 'none')
622      .style('overflow', 'hidden') // required for ellipsis
623      .style('white-space', 'nowrap') // required for ellipsis
624      .style('text-overflow', 'ellipsis')
625      .style('text-align', 'center')
626      .style('vertical-align', 'middle')
627      .style('visibility', function(datum) {
628        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
629      })
630      .text(function(datum) { return datum.n; });
631
632  // Complicated transition logic: See note in _handleInodes()
633  this._mapContainer.selectAll('div.leaf_rect_entering').transition()
634      .duration(thisTreeMap._enterDuration).delay(
635          this._firstTransition ? 0 : thisTreeMap._exitDuration +
636              thisTreeMap._updateDuration)
637      .attr('class', 'rect leaf_rect')
638      .style('opacity', '1')
639  this._mapContainer.selectAll('div.leaf_label_entering').transition()
640      .duration(thisTreeMap._enterDuration).delay(
641          this._firstTransition ? 0 : thisTreeMap._exitDuration +
642              thisTreeMap._updateDuration)
643      .attr('class', 'label leaf_label')
644      .style('opacity', '1')
645  this._mapContainer.selectAll('div.leaf_rect').transition()
646      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
647      .style('opacity', '1')
648      .style('left', function(datum){ return datum.x; })
649      .style('top', function(datum){ return datum.y; })
650      .style('width', function(datum){ return datum.dx; })
651      .style('height', function(datum){ return datum.dy; });
652  this._mapContainer.selectAll('div.leaf_label').transition()
653      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
654      .style('opacity', '1')
655      .style('visibility', function(datum) {
656          return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
657      })
658      .style('left', function(datum){ return datum.x; })
659      .style('top', function(datum){ return datum.y; })
660      .style('width', function(datum) { return datum.dx; })
661      .style('height', function(datum) { return datum.dy; });
662  var exit = this._mapContainer.selectAll('div.leaf')
663      .data(leaves, function(datum) { return 'leaf-' + datum.id; })
664      .exit();
665  exit.selectAll('div.leaf_rect').transition()
666      .duration(thisTreeMap._exitDuration)
667      .style('opacity', 0);
668  exit.selectAll('div.leaf_label').transition()
669      .duration(thisTreeMap._exitDuration)
670      .style('opacity', 0);
671  exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
672
673  console.log(leaves.length + ' leaves layed out.');
674  console.timeEnd('_handleLeaves');
675}
676
677D3SymbolTreeMap.prototype._makeSymbolBucketBackgroundImage = function(datum) {
678  if (!(datum.t === undefined && datum.depth == this._currentMaxDepth)) {
679    return 'none';
680  }
681  var text = '';
682  var lastStop = 0;
683  for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
684    symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
685    var stats = datum.symbol_stats[symbol_type];
686    if (stats !== undefined) {
687      if (text.length !== 0) {
688        text += ', ';
689      }
690      var percent = 100 * (stats.size / datum.value);
691      var nowStop = lastStop + percent;
692      var tempcolor = D3SymbolTreeMap.getColorForType(symbol_type);
693      var color = d3.rgb(tempcolor).toString();
694      text += color + ' ' + lastStop + '%, ' + color + ' ' +
695          nowStop + '%';
696      lastStop = nowStop;
697    }
698  }
699  return 'linear-gradient(' + (datum.dx > datum.dy ? 'to right' :
700                               'to bottom') + ', ' + text + ')';
701}
702
703D3SymbolTreeMap.prototype.pathFor = function(datum) {
704  if (datum.__path) return datum.__path;
705  parts=[];
706  node = datum;
707  while (node) {
708    if (node.k === 'p') { // path node
709      if(node.n !== '/') parts.unshift(node.n);
710    }
711    node = node.parent;
712  }
713  datum.__path = '/' + parts.join('/');
714  return datum.__path;
715}
716
717D3SymbolTreeMap.prototype._createHighlight = function(datum, selection) {
718  var x = parseInt(selection.style('left'));
719  var y = parseInt(selection.style('top'));
720  var w = parseInt(selection.style('width'));
721  var h = parseInt(selection.style('height'));
722  datum.highlight = this._mapContainer.append('div')
723      .attr('id', 'h-' + datum.id)
724      .attr('class', 'highlight')
725      .style('pointer-events', 'none')
726      .style('-webkit-user-select', 'none')
727      .style('z-index', '999999')
728      .style('position', 'absolute')
729      .style('top', y-2)
730      .style('left', x-2)
731      .style('width', w+4)
732      .style('height', h+4)
733      .style('margin', 0)
734      .style('padding', 0)
735      .style('border', '4px outset rgba(250,40,200,0.9)')
736      .style('box-sizing', 'border-box')
737      .style('opacity', 0.0);
738}
739
740D3SymbolTreeMap.prototype._showHighlight = function(datum, selection) {
741  if (datum === this._currentRoot) return;
742  if (datum.highlight === undefined) {
743    this._createHighlight(datum, selection);
744  }
745  datum.highlight.transition().duration(200).style('opacity', 1.0);
746}
747
748D3SymbolTreeMap.prototype._hideHighlight = function(datum, selection) {
749  if (datum.highlight === undefined) return;
750  datum.highlight.transition().duration(750)
751      .style('opacity', 0)
752      .each('end', function(){
753        if (datum.highlight) datum.highlight.remove();
754        delete datum.highlight;
755      });
756}
757
758D3SymbolTreeMap.prototype._createInfoBox = function() {
759  return d3.select('body')
760      .append('div')
761      .attr('id', 'infobox')
762      .style('z-index', '2147483647') // (2^31) - 1: Hopefully safe :)
763      .style('position', 'absolute')
764      .style('visibility', 'hidden')
765      .style('background-color', 'rgba(255,255,255, 0.9)')
766      .style('border', '1px solid black')
767      .style('padding', '10px')
768      .style('-webkit-user-select', 'none')
769      .style('box-shadow', '3px 3px rgba(70,70,70,0.5)')
770      .style('border-radius', '10px')
771      .style('white-space', 'nowrap');
772}
773
774D3SymbolTreeMap.prototype._showInfoBox = function(datum) {
775  this.infobox.text('');
776  var numSymbols = 0;
777  var sizeish = D3SymbolTreeMap._pretty(datum.value) + ' bytes (' +
778      D3SymbolTreeMap._byteify(datum.value) + ')';
779  if (datum.k === 'p' || datum.k === 'b') { // path or bucket
780    if (datum.symbol_stats) { // can be empty if filters are applied
781      for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
782        symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
783        var stats = datum.symbol_stats[symbol_type];
784        if (stats !== undefined) numSymbols += stats.count;
785      }
786    }
787  } else if (datum.k === 's') { // symbol
788    numSymbols = 1;
789  }
790
791  if (datum.k === 'p' && !datum.lastPathElement) {
792    this.infobox.append('div').text('Directory: ' + this.pathFor(datum))
793    this.infobox.append('div').text('Size: ' + sizeish);
794  } else {
795    if (datum.k === 'p') { // path
796      this.infobox.append('div').text('File: ' + this.pathFor(datum))
797      this.infobox.append('div').text('Size: ' + sizeish);
798    } else if (datum.k === 'b') { // bucket
799      this.infobox.append('div').text('Symbol Bucket: ' +
800          D3SymbolTreeMap._getSymbolDescription(datum.t));
801      this.infobox.append('div').text('Count: ' + numSymbols);
802      this.infobox.append('div').text('Size: ' + sizeish);
803      this.infobox.append('div').text('Location: ' + this.pathFor(datum))
804    } else if (datum.k === 's') { // symbol
805      this.infobox.append('div').text('Symbol: ' + datum.n);
806      this.infobox.append('div').text('Type: ' +
807          D3SymbolTreeMap._getSymbolDescription(datum.t));
808      this.infobox.append('div').text('Size: ' + sizeish);
809      this.infobox.append('div').text('Location: ' + this.pathFor(datum))
810    }
811  }
812  if (datum.k === 'p') {
813    this.infobox.append('div')
814        .text('Number of symbols: ' + D3SymbolTreeMap._pretty(numSymbols));
815    if (datum.symbol_stats) { // can be empty if filters are applied
816      var table = this.infobox.append('table')
817          .attr('border', 1).append('tbody');
818      var header = table.append('tr');
819      header.append('th').text('Type');
820      header.append('th').text('Count');
821      header.append('th')
822          .style('white-space', 'nowrap')
823          .text('Total Size (Bytes)');
824      for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
825        symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
826        var stats = datum.symbol_stats[symbol_type];
827        if (stats !== undefined) {
828          var tr = table.append('tr');
829          tr.append('td')
830              .style('white-space', 'nowrap')
831              .text(D3SymbolTreeMap._getSymbolDescription(
832                  symbol_type));
833          tr.append('td').text(D3SymbolTreeMap._pretty(stats.count));
834          tr.append('td').text(D3SymbolTreeMap._pretty(stats.size));
835        }
836      }
837    }
838  }
839  this.infobox.style('visibility', 'visible');
840}
841
842D3SymbolTreeMap.prototype._hideInfoBox = function(datum) {
843  this.infobox.style('visibility', 'hidden');
844}
845
846D3SymbolTreeMap.prototype._moveInfoBox = function(event) {
847  var element = document.getElementById('infobox');
848  var w = element.offsetWidth;
849  var h = element.offsetHeight;
850  var offsetLeft = 10;
851  var offsetTop = 10;
852
853  var rightLimit = window.innerWidth;
854  var rightEdge = event.pageX + offsetLeft + w;
855  if (rightEdge > rightLimit) {
856    // Too close to screen edge, reflect around the cursor
857    offsetLeft = -1 * (w + offsetLeft);
858  }
859
860  var bottomLimit = window.innerHeight;
861  var bottomEdge = event.pageY + offsetTop + h;
862  if (bottomEdge > bottomLimit) {
863    // Too close ot screen edge, reflect around the cursor
864    offsetTop = -1 * (h + offsetTop);
865  }
866
867  this.infobox.style('top', (event.pageY + offsetTop) + 'px')
868      .style('left', (event.pageX + offsetLeft) + 'px');
869}
870
871D3SymbolTreeMap.prototype.biggestSymbols = function(maxRecords) {
872  var result = undefined;
873  var smallest = undefined;
874  var sortFunction = function(a,b) {
875    var result = b.value - a.value;
876    if (result !== 0) return result; // sort by size
877    var pathA = treemap.pathFor(a); // sort by path
878    var pathB = treemap.pathFor(b);
879    if (pathA > pathB) return 1;
880    if (pathB > pathA) return -1;
881    return a.n - b.n; // sort by symbol name
882  };
883  this.visitFromDisplayedRoot(function(datum) {
884    if (datum.children) return; // ignore non-leaves
885    if (!result) { // first element
886      result = [datum];
887      smallest = datum.value;
888      return;
889    }
890    if (result.length < maxRecords) { // filling the array
891      result.push(datum);
892      return;
893    }
894    if (datum.value > smallest) { // array is already full
895      result.push(datum);
896      result.sort(sortFunction);
897      result.pop(); // get rid of smallest element
898      smallest = result[maxRecords - 1].value; // new threshold for entry
899    }
900  });
901  result.sort(sortFunction);
902  return result;
903}
904
905D3SymbolTreeMap.prototype.biggestPaths = function(maxRecords) {
906  var result = undefined;
907  var smallest = undefined;
908  var sortFunction = function(a,b) {
909    var result = b.value - a.value;
910    if (result !== 0) return result; // sort by size
911    var pathA = treemap.pathFor(a); // sort by path
912    var pathB = treemap.pathFor(b);
913    if (pathA > pathB) return 1;
914    if (pathB > pathA) return -1;
915    console.log('warning, multiple entries for the same path: ' + pathA);
916    return 0; // should be impossible
917  };
918  this.visitFromDisplayedRoot(function(datum) {
919    if (!datum.lastPathElement) return; // ignore non-files
920    if (!result) { // first element
921      result = [datum];
922      smallest = datum.value;
923      return;
924    }
925    if (result.length < maxRecords) { // filling the array
926      result.push(datum);
927      return;
928    }
929    if (datum.value > smallest) { // array is already full
930      result.push(datum);
931      result.sort(sortFunction);
932      result.pop(); // get rid of smallest element
933      smallest = result[maxRecords - 1].value; // new threshold for entry
934    }
935  });
936  result.sort(sortFunction);
937  return result;
938}
939