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