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 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; 31 } 32 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; 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 */ 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'; 71 } 72 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 (?)', 101 }; 102 D3SymbolTreeMap._NM_SYMBOL_TYPES = ''; 103 for (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 */ 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; 119 } 120 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)']; 135 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; 145 } 146 D3SymbolTreeMap._initColorMap(); 147 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; 152 } 153 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' 165 } 166 167 /** 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(); 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 */ 186 D3SymbolTreeMap.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 */ 199 D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) { 200 this._visit(this._currentRoot, visitor); 201 } 202 203 /** 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 } 211 } 212 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 } 238 } 239 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; 246 } 247 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(); 255 } 256 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(); 266 } 267 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(); 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 */ 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 } 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 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)); 364 } 365 366 D3SymbolTreeMap.prototype._doLayout = function() { 367 console.time('_doLayout'); 368 this._handleInodes(); 369 this._handleLeaves(); 370 this._firstTransition = false; 371 console.timeEnd('_doLayout'); 372 } 373 374 D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) { 375 this._showHighlight(datum, selection); 376 } 377 378 D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) { 379 this._hideHighlight(datum, selection); 380 } 381 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;}); 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 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 }); 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 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 + ')'; 701 } 702 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; 715 } 716 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); 738 } 739 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); 746 } 747 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 }); 756 } 757 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'); 772 } 773 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 } 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 842 D3SymbolTreeMap.prototype._hideInfoBox = function(datum) { 843 this.infobox.style('visibility', 'hidden'); 844 } 845 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; 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 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; 903 } 904 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; 938 } 939