Blink roll 25b6bd3a7a131ffe68d809546ad1a20707915cdc:3a503f41ae42e5b79cfcd2ff10e65afde...
[chromium-blink-merge.git] / tools / binary_size / template / D3SymbolTreeMap.js
blob4bbe82f1a32624b35abdaa148f9ded73dfe08384
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.
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
9 function 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;
33 /**
34  * Make a number pretty, with comma separators.
35  */
36 D3SymbolTreeMap._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;
51 /**
52  * Express a number in terms of KiB, MiB, GiB, etc.
53  * Note that these are powers of 2, not of 10.
54  */
55 D3SymbolTreeMap._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';
73 D3SymbolTreeMap._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 (?)',
102 D3SymbolTreeMap._NM_SYMBOL_TYPES = '';
103 for (var symbol_type in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
104   D3SymbolTreeMap._NM_SYMBOL_TYPES += symbol_type;
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  */
113 D3SymbolTreeMap._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;
121 // Qualitative 12-value pastel Brewer palette.
122 D3SymbolTreeMap._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)'];
136 D3SymbolTreeMap._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;
146 D3SymbolTreeMap._initColorMap();
148 D3SymbolTreeMap.getColorForType = function(type) {
149   var result = D3SymbolTreeMap._colorMap[type];
150   if (result === undefined) return d3.rgb('rgb(255,255,255)');
151   return result;
154 D3SymbolTreeMap.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'
168  * Sets the data displayed by the treemap and layint out the map.
169  */
170 D3SymbolTreeMap.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();
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  */
186 D3SymbolTreeMap.prototype._crunchStats = function(node) {
187   var stack = [];
188   stack.idCounter = 0;
189   this._crunchStatsHelper(stack, node);
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  */
199 D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) {
200   this._visit(this._currentRoot, visitor);
204  * Helper function for visit functions.
205  */
206 D3SymbolTreeMap.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   }
213 D3SymbolTreeMap.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   }
240 D3SymbolTreeMap.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;
248 D3SymbolTreeMap.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();
257 D3SymbolTreeMap.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();
268 D3SymbolTreeMap.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();
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  */
286 D3SymbolTreeMap.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   }
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   }
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   }
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   }
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;
359 D3SymbolTreeMap.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));
366 D3SymbolTreeMap.prototype._doLayout = function() {
367   console.time('_doLayout');
368   this._handleInodes();
369   this._handleLeaves();
370   this._firstTransition = false;
371   console.timeEnd('_doLayout');
374 D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) {
375   this._showHighlight(datum, selection);
378 D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) {
379   this._hideHighlight(datum, selection);
382 D3SymbolTreeMap.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;});
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   );
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();
562   console.log(inodes.length + ' inodes layed out.');
563   console.timeEnd('_handleInodes');
566 D3SymbolTreeMap.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       });
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; });
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();
673   console.log(leaves.length + ' leaves layed out.');
674   console.timeEnd('_handleLeaves');
677 D3SymbolTreeMap.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 + ')';
703 D3SymbolTreeMap.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;
717 D3SymbolTreeMap.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);
740 D3SymbolTreeMap.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);
748 D3SymbolTreeMap.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       });
758 D3SymbolTreeMap.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');
774 D3SymbolTreeMap.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   }
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');
842 D3SymbolTreeMap.prototype._hideInfoBox = function(datum) {
843   this.infobox.style('visibility', 'hidden');
846 D3SymbolTreeMap.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;
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   }
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   }
867   this.infobox.style('top', (event.pageY + offsetTop) + 'px')
868       .style('left', (event.pageX + offsetLeft) + 'px');
871 D3SymbolTreeMap.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;
905 D3SymbolTreeMap.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;