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.
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;
34 * Make a number pretty, with comma separators.
36 D3SymbolTreeMap._pretty = function(num) {
37 var asString = String(num);
40 for (var x = asString.length - 1; x >= 0; x--) {
43 result = ',' + result;
46 result = asString.charAt(x) + result;
52 * Express a number in terms of KiB, MiB, GiB, etc.
53 * Note that these are powers of 2, not of 10.
55 D3SymbolTreeMap._byteify = function(num) {
58 if (num >= 1024 * 1024 * 1024) {
60 num = num / (1024 * 1024 * 1024);
61 } else if (num >= 1024 * 1024) {
63 num = num / (1024 * 1024);
64 } else if (num >= 1024) {
68 return num.toFixed(2) + ' ' + suffix;
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)',
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)',
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.
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['?'];
121 // Qualitative 12-value pastel Brewer palette.
122 D3SymbolTreeMap._colorArray = [
136 D3SymbolTreeMap._initColorMap = function() {
138 var numColors = D3SymbolTreeMap._colorArray.length;
140 for (var key in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
141 var index = count++ % numColors;
142 map[key] = d3.rgb(D3SymbolTreeMap._colorArray[index]);
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)');
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)
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.
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;
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.
186 D3SymbolTreeMap.prototype._crunchStats = function(node) {
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
199 D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) {
200 this._visit(this._currentRoot, visitor);
204 * Helper function for visit functions.
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);
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,
228 // Existing symbol type, increment.
229 ancestor.symbol_stats[node.t].count++;
230 ancestor.symbol_stats[node.t].size += node.value;
233 } else for (var i = 0; i < node.children.length; i++) {
235 this._crunchStatsHelper(stack, node.children[i]);
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]);
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);
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);
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);
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.
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};
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);
310 // Ignore nodes that don't match the filter, when present.
313 // Parent of an accepted child must also be accepted.
314 this.__cloneState.forced++;
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++;
323 // Non-leaf node. If no children are accepted, prune it.
324 this.__cloneState.pruned++;
328 if (datum.id !== undefined) copy.id = datum.id;
329 if (datum.lastPathElement !== undefined) {
330 copy.lastPathElement = datum.lastPathElement;
332 if (datum.t !== undefined) copy.t = datum.t;
333 if (datum.value !== undefined && datum.children === undefined) {
334 copy.value = datum.value;
337 // Discard the copy we were going to return
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;
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) ' +
352 this.__cloneState.pruned + ' nodes pruned because because no ' +
353 'children remained.');
354 delete this.__cloneState;
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;
389 var cellsEnter = this._mapContainer.selectAll('div.inode')
390 .data(inodes, function(datum) { return datum.id; })
392 .append('div').attr('class', 'inode').attr('id', function(datum){
393 return 'node-' + datum.id;});
396 // Define enter/update/exit for inodes
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(
412 .style('background-color', function(datum) {
413 if (datum.t === undefined) return 'rgb(220,220,220)';
414 return D3SymbolTreeMap.getColorForType(datum.t).toString();
416 .on('mouseover', function(datum){
417 thisTreeMap._highlightElement.call(
418 thisTreeMap, datum, d3.select(this));
419 thisTreeMap._showInfoBox.call(thisTreeMap, datum);
421 .on('mouseout', function(datum){
422 thisTreeMap._unhighlightElement.call(
423 thisTreeMap, datum, d3.select(this));
424 thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
426 .on('mousemove', function(){
427 thisTreeMap._moveInfoBox.call(thisTreeMap, event);
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) {
437 thisTreeMap._zoomDatum(thisTreeMap._treeData);
439 // Zoom out of the selection
440 thisTreeMap._zoomDatum(datum.parent);
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';
464 .text(function(datum) {
465 var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']'
467 if (datum.k === 'b') { // bucket
468 if (datum === thisTreeMap._currentRoot) {
469 text = thisTreeMap.pathFor(datum) + ': '
470 + D3SymbolTreeMap._getSymbolDescription(datum.t)
472 text = D3SymbolTreeMap._getSymbolDescription(datum.t);
474 } else if (datum === thisTreeMap._currentRoot) {
475 // The top-most level should always show the complete path
476 text = thisTreeMap.pathFor(datum);
478 // Anything that isn't a bucket or a leaf (symbol) or the
479 // current root should just show its name.
482 return text + sizeish;
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
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
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
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(
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';
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) + ']'
536 if (datum.k === 'b') {
537 if (datum === thisTreeMap._currentRoot) {
538 text = thisTreeMap.pathFor(datum) + ': ' +
539 D3SymbolTreeMap._getSymbolDescription(datum.t)
541 text = D3SymbolTreeMap._getSymbolDescription(datum.t);
543 } else if (datum === thisTreeMap._currentRoot) {
544 // The top-most level should always show the complete path
545 text = thisTreeMap.pathFor(datum);
547 // Anything that isn't a bucket or a leaf (symbol) or the
548 // current root should just show its name.
551 return text + sizeish;
553 var exit = this._mapContainer.selectAll('div.inode')
554 .data(inodes, function(datum) { return 'inode-' + datum.id; })
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; })
576 .append('div').attr('class', 'leaf').attr('id', function(datum){
577 return 'node-' + datum.id;
580 // Define enter/update/exit for leaves
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();
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);
602 .on('mouseout', function(datum){
603 thisTreeMap._unhighlightElement.call(
604 thisTreeMap, datum, d3.select(this));
605 thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
607 .on('mousemove', function(){ thisTreeMap._moveInfoBox.call(
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';
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';
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; })
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)) {
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) {
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 + ' ' +
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;
708 if (node.k === 'p') { // path node
709 if(node.n !== '/') parts.unshift(node.n);
713 datum.__path = '/' + parts.join('/');
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')
732 .style('height', h+4)
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);
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)
752 .each('end', function(){
753 if (datum.highlight) datum.highlight.remove();
754 delete datum.highlight;
758 D3SymbolTreeMap.prototype._createInfoBox = function() {
759 return d3.select('body')
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('');
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;
787 } else if (datum.k === 's') { // symbol
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);
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))
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');
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');
830 .style('white-space', 'nowrap')
831 .text(D3SymbolTreeMap._getSymbolDescription(
833 tr.append('td').text(D3SymbolTreeMap._pretty(stats.count));
834 tr.append('td').text(D3SymbolTreeMap._pretty(stats.size));
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;
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);
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);
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
883 this.visitFromDisplayedRoot(function(datum) {
884 if (datum.children) return; // ignore non-leaves
885 if (!result) { // first element
887 smallest = datum.value;
890 if (result.length < maxRecords) { // filling the array
894 if (datum.value > smallest) { // array is already full
896 result.sort(sortFunction);
897 result.pop(); // get rid of smallest element
898 smallest = result[maxRecords - 1].value; // new threshold for entry
901 result.sort(sortFunction);
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
918 this.visitFromDisplayedRoot(function(datum) {
919 if (!datum.lastPathElement) return; // ignore non-files
920 if (!result) { // first element
922 smallest = datum.value;
925 if (result.length < maxRecords) { // filling the array
929 if (datum.value > smallest) { // array is already full
931 result.sort(sortFunction);
932 result.pop(); // get rid of smallest element
933 smallest = result[maxRecords - 1].value; // new threshold for entry
936 result.sort(sortFunction);