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 // 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 } |
OLD | NEW |