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--) { | |
bulach
2014/04/16 16:32:16
nit:s/x=as/x = as/
Andrew Hayden (chromium.org)
2014/04/16 17:40:16
Done.
| |
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) { | |
bulach
2014/04/16 16:32:16
nit: spaces around * here and below
Andrew Hayden (chromium.org)
2014/04/16 17:40:16
Done.
| |
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_TYPES = 'ABbCDdGgiNpRrSsTtUuVvWw@-?'; | |
bulach
2014/04/16 16:32:16
nit: rather than duplicate, perhaps define below:
Andrew Hayden (chromium.org)
2014/04/16 17:40:16
Done.
| |
74 D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS = { | |
75 // Definitions concisely derived from the nm 'man' page | |
76 'A': 'Global absolute (A)', | |
77 'B': 'Global uninitialized data (B)', | |
78 'b': 'Local uninitialized data (b)', | |
79 'C': 'Global uninitialized common (C)', | |
80 'D': 'Global initialized data (D)', | |
81 'd': 'Local initialized data (d)', | |
82 'G': 'Global small initialized data (G)', | |
83 'g': 'Local small initialized data (g)', | |
84 'i': 'Indirect function (i)', | |
85 'N': 'Debugging (N)', | |
86 'p': 'Stack unwind (p)', | |
87 'R': 'Global read-only data (R)', | |
88 'r': 'Local read-only data (r)', | |
89 'S': 'Global small uninitialized data (S)', | |
90 's': 'Local small uninitialized data (s)', | |
91 'T': 'Global code (T)', | |
92 't': 'Local code (t)', | |
93 'U': 'Undefined (U)', | |
94 'u': 'Unique (u)', | |
95 'V': 'Global weak object (V)', | |
96 'v': 'Local weak object (v)', | |
97 'W': 'Global weak symbol (W)', | |
98 'w': 'Local weak symbol (w)', | |
99 '@': 'Vtable entry (@)', // non-standard, hack. | |
100 '-': 'STABS debugging (-)', | |
101 '?': 'Unrecognized (?)', | |
102 }; | |
103 /** | |
104 * Given a symbol type code, look up and return a human-readable description | |
105 * of that symbol type. If the symbol type does not match one of the known | |
106 * types, the unrecognized description (corresponding to symbol type '?') is | |
107 * returned instead of null or undefined. | |
108 */ | |
109 D3SymbolTreeMap._getSymbolDescription = function(type) { | |
110 var result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS[type]; | |
111 if (result === undefined) { | |
112 result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS['?']; | |
113 } | |
114 return result; | |
115 }; | |
116 | |
117 // Qualitative 12-value pastel Brewer palette. | |
118 D3SymbolTreeMap._colorArray = [ | |
119 'rgb(141,211,199)', | |
120 'rgb(255,255,179)', | |
121 'rgb(190,186,218)', | |
122 'rgb(251,128,114)', | |
123 'rgb(128,177,211)', | |
124 'rgb(253,180,98)', | |
125 'rgb(179,222,105)', | |
126 'rgb(252,205,229)', | |
127 'rgb(217,217,217)', | |
128 'rgb(188,128,189)', | |
129 'rgb(204,235,197)', | |
130 'rgb(255,237,111)']; | |
131 | |
132 D3SymbolTreeMap._initColorMap = function() { | |
133 var map = {}; | |
134 var numColors = D3SymbolTreeMap._colorArray.length; | |
135 for (var x=0; x<D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { | |
bulach
2014/04/16 16:32:16
nit: s/x=0/x = 0/
also, as above, this could be:
Andrew Hayden (chromium.org)
2014/04/16 17:40:16
Done.
| |
136 var key = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); | |
137 var index = x % numColors; | |
138 map[key] = d3.rgb(D3SymbolTreeMap._colorArray[index]); | |
139 } | |
140 D3SymbolTreeMap._colorMap = map; | |
141 } | |
142 D3SymbolTreeMap._initColorMap(); | |
143 | |
144 D3SymbolTreeMap.getColorForType = function(type) { | |
145 var result = D3SymbolTreeMap._colorMap[type]; | |
146 if (result === undefined) return d3.rgb('rgb(255,255,255)'); | |
147 return result; | |
148 } | |
149 | |
150 D3SymbolTreeMap.prototype.init = function() { | |
151 this.infobox = this._createInfoBox(); | |
152 this._mapContainer = d3.select('body').append('div') | |
153 .style('position', 'relative') | |
154 .style('width', this._mapWidth) | |
155 .style('height', this._mapHeight) | |
156 .style('padding', 0) | |
157 .style('margin', 0) | |
158 .style('box-shadow', '5px 5px 5px #888'); | |
159 this._createHoverMask(); | |
160 this._layout = this._createTreeMapLayout(); | |
161 this._setData(tree_data); // TODO: Don't use global 'tree_data' | |
162 } | |
163 | |
164 /** | |
165 * Sets the data displayed by the treemap and layint out the map. | |
166 */ | |
167 D3SymbolTreeMap.prototype._setData = function(data) { | |
168 this._treeData = data; | |
169 console.time('_crunchStats'); | |
170 this._crunchStats(data); | |
171 console.timeEnd('_crunchStats'); | |
172 this._currentRoot = this._treeData; | |
173 this._currentNodes = this._layout.nodes(this._currentRoot); | |
174 this._currentMaxDepth = this._maxLevelsToShow; | |
175 this._doLayout(); | |
176 } | |
177 | |
178 /** | |
179 * Recursively traverses the entire tree starting from the specified node, | |
180 * computing statistics and recording metadata as it goes. Call this method | |
181 * only once per imported tree. | |
182 */ | |
183 D3SymbolTreeMap.prototype._crunchStats = function(node) { | |
184 var stack = []; | |
185 stack.idCounter = 0; | |
186 this._crunchStatsHelper(stack, node); | |
187 } | |
188 | |
189 /** | |
190 * Invoke the specified visitor function on all data elements currently shown | |
191 * in the treemap including any and all of their children, starting at the | |
192 * currently-displayed root and descening recursively. The function will be | |
193 * passed the datum element representing each node. No traversal guarantees | |
194 * are made. | |
195 */ | |
196 D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) { | |
197 this._visit(this._currentRoot, visitor); | |
198 } | |
199 | |
200 /** | |
201 * Helper function for visit functions. | |
202 */ | |
203 D3SymbolTreeMap.prototype._visit = function(datum, visitor) { | |
204 visitor.call(this, datum); | |
205 if (datum.children) for (var i=0; i<datum.children.length; i++) | |
206 this._visit(datum.children[i], visitor); | |
207 } | |
208 | |
209 D3SymbolTreeMap.prototype._crunchStatsHelper = function(stack, node) { | |
210 // Only overwrite the node ID if it isn't already set. | |
211 // This allows stats to be crunched multiple times on subsets of data | |
212 // without breaking the data-to-ID bindings. New nodes get new IDs. | |
213 if (node.id === undefined) node.id = stack.idCounter++; | |
214 if (node.children === undefined) { | |
215 // Leaf node (symbol); accumulate stats. | |
216 for (var i=0; i<stack.length; i++) { | |
217 var ancestor = stack[i]; | |
218 if (!ancestor.symbol_stats) ancestor.symbol_stats = {}; | |
219 if (ancestor.symbol_stats[node.t] === undefined) { | |
220 // New symbol type we haven't seen before, just record. | |
221 ancestor.symbol_stats[node.t] = {'count': 1, | |
222 'size': node.value}; | |
223 } else { | |
224 // Existing symbol type, increment. | |
225 ancestor.symbol_stats[node.t].count++; | |
226 ancestor.symbol_stats[node.t].size += node.value; | |
227 } | |
228 } | |
229 } else for (var i=0; i<node.children.length; i++) { | |
230 stack.push(node); | |
231 this._crunchStatsHelper(stack, node.children[i]); | |
232 stack.pop(); | |
233 } | |
234 } | |
235 | |
236 D3SymbolTreeMap.prototype._createTreeMapLayout = function() { | |
237 var result = d3.layout.treemap() | |
238 .padding([this.boxPadding.t, this.boxPadding.r, | |
239 this.boxPadding.b, this.boxPadding.l]) | |
240 .size([this._mapWidth, this._mapHeight]); | |
241 return result; | |
242 } | |
243 | |
244 D3SymbolTreeMap.prototype.resize = function(width, height) { | |
245 this._mapWidth = width; | |
246 this._mapHeight = height; | |
247 this._mapContainer.style('width', width).style('height', height); | |
248 this._layout.size([this._mapWidth, this._mapHeight]); | |
249 this._currentNodes = this._layout.nodes(this._currentRoot); | |
250 this._doLayout(); | |
251 } | |
252 | |
253 D3SymbolTreeMap.prototype._zoomDatum = function(datum) { | |
254 if (this._currentRoot === datum) { | |
255 return; // already here | |
256 } | |
257 this._hideHighlight(datum); | |
258 this._hideInfoBox(datum); | |
259 this._currentRoot = datum; | |
260 this._currentNodes = this._layout.nodes(this._currentRoot); | |
261 this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow; | |
262 console.log('zooming into datum ' + this._currentRoot.n); | |
263 this._doLayout(); | |
264 } | |
265 | |
266 D3SymbolTreeMap.prototype.setMaxLevels = function(levelsToShow) { | |
267 this._maxLevelsToShow = levelsToShow; | |
268 this._currentNodes = this._layout.nodes(this._currentRoot); | |
269 this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow; | |
270 console.log('setting max levels to show: ' + this._maxLevelsToShow); | |
271 this._doLayout(); | |
272 } | |
273 | |
274 /** | |
275 * Clone the specified tree, returning an independent copy of the data. | |
276 * Only the original attributes expected to exist prior to invoking | |
277 * _crunchStatsHelper are retained, with the exception of the 'id' attribute | |
278 * (which must be retained for proper transitions). | |
279 * If the optional filter parameter is provided, it will be called with 'this' | |
280 * set to this treemap instance and passed the 'datum' object as an argument. | |
281 * When specified, the copy will retain only the data for which the filter | |
282 * function returns true. | |
283 */ | |
284 D3SymbolTreeMap.prototype._clone = function(datum, filter) { | |
285 var trackingStats = false; | |
286 if (this.__cloneState === undefined) { | |
287 console.time('_clone'); | |
288 trackingStats = true; | |
289 this.__cloneState = {'accepted': 0, 'rejected': 0, | |
290 'forced': 0, 'pruned': 0}; | |
291 } | |
292 | |
293 // Must go depth-first. All parents of children that are accepted by the | |
294 // filter must be preserved! | |
295 var copy = {'n': datum.n, 'k': datum.k}; | |
296 var childAccepted = false; | |
297 if (datum.children !== undefined) { | |
298 for (var i=0; i<datum.children.length; i++) { | |
299 var copiedChild = this._clone(datum.children[i], filter); | |
300 if (copiedChild !== undefined) { | |
301 childAccepted = true; // parent must also be accepted. | |
302 if (copy.children === undefined) copy.children = []; | |
303 copy.children.push(copiedChild); | |
304 } | |
305 } | |
306 } | |
307 | |
308 // Ignore nodes that don't match the filter, when present. | |
309 var accept = false; | |
310 if (childAccepted) { | |
311 // Parent of an accepted child must also be accepted. | |
312 this.__cloneState.forced++; | |
313 accept = true; | |
314 } else if (filter !== undefined && filter.call(this, datum) !== true) { | |
315 this.__cloneState.rejected++; | |
316 } else if (datum.children === undefined) { | |
317 // Accept leaf nodes that passed the filter | |
318 this.__cloneState.accepted++; | |
319 accept = true; | |
320 } else { | |
321 // Non-leaf node. If no children are accepted, prune it. | |
322 this.__cloneState.pruned++; | |
323 } | |
324 | |
325 if (accept) { | |
326 if (datum.id !== undefined) copy.id = datum.id; | |
327 if (datum.lastPathElement !== undefined) { | |
328 copy.lastPathElement = datum.lastPathElement; | |
329 } | |
330 if (datum.t !== undefined) copy.t = datum.t; | |
331 if (datum.value !== undefined && datum.children === undefined) { | |
332 copy.value = datum.value; | |
333 } | |
334 } else { | |
335 // Discard the copy we were going to return | |
336 copy = undefined; | |
337 } | |
338 | |
339 if (trackingStats === true) { | |
340 // We are the fist call in the recursive chain. | |
341 console.timeEnd('_clone'); | |
342 var totalAccepted = this.__cloneState.accepted + | |
343 this.__cloneState.forced; | |
344 console.log( | |
345 totalAccepted + ' nodes retained (' + | |
346 this.__cloneState.forced + ' forced by accepted children, ' + | |
347 this.__cloneState.accepted + ' accepted on their own merits), ' + | |
348 this.__cloneState.rejected + ' nodes (and their children) ' + | |
349 'filtered out,' + | |
350 this.__cloneState.pruned + ' nodes pruned because because no ' + | |
351 'children remained.'); | |
352 delete this.__cloneState; | |
353 } | |
354 return copy; | |
355 } | |
356 | |
357 D3SymbolTreeMap.prototype.filter = function(filter) { | |
358 // Ensure we have a copy of the original root. | |
359 if (this._backupTree === undefined) this._backupTree = this._treeData; | |
360 this._mapContainer.selectAll('div').remove(); | |
361 this._setData(this._clone(this._backupTree, filter)); | |
362 } | |
363 | |
364 D3SymbolTreeMap.prototype._doLayout = function() { | |
365 console.time('_doLayout'); | |
366 this._handleInodes(); | |
367 this._handleLeaves(); | |
368 this._firstTransition = false; | |
369 console.timeEnd('_doLayout'); | |
370 } | |
371 | |
372 D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) { | |
373 this._showHighlight(datum, selection); | |
374 //this._showHoverMask(selection); | |
375 } | |
376 | |
377 D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) { | |
378 this._hideHighlight(datum, selection); | |
379 //this._hideHoverMask(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 var text = ''; | |
680 var lastStop = 0; | |
681 for (var x=0; x<D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { | |
682 symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); | |
683 var stats = datum.symbol_stats[symbol_type]; | |
684 if (stats !== undefined) { | |
685 if (text.length !== 0) { | |
686 text += ', '; | |
687 } | |
688 var percent = 100 * (stats.size / datum.value); | |
689 var nowStop = lastStop + percent; | |
690 var tempcolor = D3SymbolTreeMap.getColorForType(symbol_type); | |
691 var color = d3.rgb(tempcolor).toString(); | |
692 text += color + ' ' + lastStop + '%, ' + color + ' ' + | |
693 nowStop + '%'; | |
694 lastStop = nowStop; | |
695 } | |
696 } | |
697 return 'linear-gradient(' + (datum.dx > datum.dy ? 'to right' : | |
698 'to bottom') + ', ' + text + ')'; | |
699 } else { | |
700 return 'none'; | |
701 } | |
702 } | |
703 | |
704 D3SymbolTreeMap.prototype.pathFor = function(datum) { | |
705 if (datum.__path) return datum.__path; | |
706 parts=[]; | |
707 node = datum; | |
708 while (node) { | |
709 if (node.k === 'p') { // path node | |
710 if(node.n !== '/') parts.unshift(node.n); | |
711 } | |
712 node = node.parent; | |
713 } | |
714 datum.__path = '/' + parts.join('/'); | |
715 return datum.__path; | |
716 } | |
717 | |
718 D3SymbolTreeMap.prototype._createHighlight = function(datum, selection) { | |
719 var x = parseInt(selection.style('left')); | |
720 var y = parseInt(selection.style('top')); | |
721 var w = parseInt(selection.style('width')); | |
722 var h = parseInt(selection.style('height')); | |
723 datum.highlight = this._mapContainer.append('div') | |
724 .attr('id', 'h-' + datum.id) | |
725 .attr('class', 'highlight') | |
726 .style('pointer-events', 'none') | |
727 .style('-webkit-user-select', 'none') | |
728 .style('z-index', '999999') | |
729 .style('position', 'absolute') | |
730 .style('top', y-2) | |
731 .style('left', x-2) | |
732 .style('width', w+4) | |
733 .style('height', h+4) | |
734 .style('margin', 0) | |
735 .style('padding', 0) | |
736 .style('border', '4px outset rgba(250,40,200,0.9)') | |
737 .style('box-sizing', 'border-box') | |
738 .style('opacity', 0.0); | |
739 } | |
740 | |
741 D3SymbolTreeMap.prototype._showHighlight = function(datum, selection) { | |
742 if (datum === this._currentRoot) return; | |
743 if (datum.highlight === undefined) { | |
744 this._createHighlight(datum, selection); | |
745 } | |
746 datum.highlight.transition().duration(200).style('opacity', 1.0); | |
747 /* | |
748 var ancestors = []; | |
749 var node = datum.parent; | |
750 var boostedZ = 999999; | |
751 while (node && node !== this._currentRoot) { | |
752 ancestors.unshift(node); | |
753 var container = this._mapContainer.select('#node-' + node.id); | |
754 container.select('.label').style('background-color', 'white'); | |
755 node = node.parent; | |
756 } | |
757 this._highlightContainer.selectionParents = ancestors; | |
758 */ | |
759 } | |
760 | |
761 D3SymbolTreeMap.prototype._hideHighlight = function(datum, selection) { | |
762 if (datum.highlight === undefined) return; | |
763 datum.highlight.transition().duration(750) | |
764 .style('opacity', 0) | |
765 .each('end', function(){ | |
766 if (datum.highlight) datum.highlight.remove(); | |
767 delete datum.highlight; | |
768 }); | |
769 /* | |
770 while(this._highlightContainer.selectionParents && | |
771 this._highlightContainer.selectionParents.length > 0) { | |
772 var node = this._highlightContainer.selectionParents.shift(); | |
773 var container = this._mapContainer.select('#node-' + node.id); | |
774 container.select('.label').style('background-color', null); | |
775 } | |
776 this._highlightContainer.style('visibility', 'hidden'); | |
777 */ | |
778 } | |
779 | |
780 D3SymbolTreeMap.prototype._createHoverMask = function() { | |
781 // There are basically two ways to do this: | |
782 // 1. SVG with a mask that defines a 'cutout' area. | |
783 // 2. A grid of DIVs. | |
784 // The SVG approach has potentially nasty z-index problems and would be | |
785 // the only SVG in the app and is arguably as complex as the divs approach. | |
786 // The DIVs must be resized whenever the treemap changes size. | |
787 this._maskContainer = this._mapContainer.append('div') | |
788 .attr('id', 'mask') | |
789 .style('pointer-events', 'none') | |
790 .style('-webkit-user-select', 'none') | |
791 .style('z-index', '999999') | |
792 .style('position', 'absolute') | |
793 .style('top', 0) | |
794 .style('left', 0) | |
795 .style('width', this._mapWidth) | |
796 .style('height', this._mapHeight) | |
797 .style('visibility', 'visible'); | |
798 | |
799 var quadrants=['nw','n','ne','e','se','s','sw','w']; | |
800 this._maskContainer.quadrants = {}; | |
801 for (var i=0; i<quadrants.length; i++) { | |
802 this._maskContainer.quadrants[quadrants[i]] = | |
803 this._maskContainer.append('div') | |
804 .attr('id', 'mask_' + quadrants[i]) | |
805 .style('pointer-events', 'none') | |
806 .style('-webkit-user-select', 'none') | |
807 .style('background-color', 'white') | |
808 .style('opacity', 0.5) | |
809 .style('margin', 0) | |
810 .style('padding', 0) | |
811 .style('position', 'absolute') | |
812 .style('visibility', 'hidden') | |
813 .style('top', 0) | |
814 .style('left', 0) | |
815 .style('width', 0) | |
816 .style('height', 0); | |
817 } | |
818 } | |
819 | |
820 D3SymbolTreeMap.prototype._showHoverMask = function(selection) { | |
821 // Resize and reposition each quadrant | |
822 var x = parseInt(selection.style('left')); | |
823 var y = parseInt(selection.style('top')); | |
824 var width = parseInt(selection.style('width')); | |
825 var height = parseInt(selection.style('height')); | |
826 | |
827 var dimension = function(el,x,y,w,h) { | |
828 return el.style('top', y) | |
829 .style('left', x) | |
830 .style('width', w) | |
831 .style('height', h) | |
832 .style('visibility', 'visible'); | |
833 } | |
834 var quads = this._maskContainer.quadrants; // for brevity below | |
835 dimension(quads['nw'],0,0,x,y); | |
836 dimension(quads['n'],x,0,width,y); | |
837 dimension(quads['ne'],x+width,0,this._mapWidth - (x+width), y); | |
838 dimension(quads['e'],x+width,y,this._mapWidth - (x+width), height); | |
839 dimension(quads['se'],x+width,y+height,this._mapWidth - | |
840 (x+width), this._mapHeight - (y+height)); | |
841 dimension(quads['s'],x,y+height,width,this._mapHeight - (y+height)); | |
842 dimension(quads['sw'],0,y+height,x,this._mapHeight - (y+height)); | |
843 dimension(quads['w'],0,y,x,height); | |
844 this._maskContainer.style('visibility', 'visible'); | |
845 } | |
846 | |
847 D3SymbolTreeMap.prototype._hideHoverMask = function() { | |
848 this._maskContainer.style('visibility', 'hidden'); | |
849 var quadrants=['nw','n','ne','e','se','s','sw','w']; | |
850 for (var i=0; i<quadrants.length; i++) { | |
851 this._maskContainer | |
852 .quadrants[quadrants[i]] | |
853 .style('visibility', 'hidden'); | |
854 } | |
855 } | |
856 | |
857 D3SymbolTreeMap.prototype._createInfoBox = function() { | |
858 return d3.select('body') | |
859 .append('div') | |
860 .attr('id', 'infobox') | |
861 .style('z-index', '2147483647') // (2^31) - 1: Hopefully safe :) | |
862 .style('position', 'absolute') | |
863 .style('visibility', 'hidden') | |
864 .style('background-color', 'rgba(255,255,255, 0.9)') | |
865 .style('border', '1px solid black') | |
866 .style('padding', '10px') | |
867 .style('-webkit-user-select', 'none') | |
868 .style('box-shadow', '3px 3px rgba(70,70,70,0.5)') | |
869 .style('border-radius', '10px') | |
870 .style('white-space', 'nowrap'); | |
871 } | |
872 | |
873 D3SymbolTreeMap.prototype._showInfoBox = function(datum) { | |
874 this.infobox.text(''); | |
875 var numSymbols = 0; | |
876 var sizeish = D3SymbolTreeMap._pretty(datum.value) + ' bytes (' + | |
877 D3SymbolTreeMap._byteify(datum.value) + ')'; | |
878 if (datum.k === 'p' || datum.k === 'b') { // path or bucket | |
879 if (datum.symbol_stats) { // can be empty if filters are applied | |
880 for (var x=0; x<D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { | |
881 symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); | |
882 var stats = datum.symbol_stats[symbol_type]; | |
883 if (stats !== undefined) numSymbols += stats.count; | |
884 } | |
885 } | |
886 } else if (datum.k === 's') { // symbol | |
887 numSymbols = 1; | |
888 } | |
889 | |
890 if (datum.k === 'p' && !datum.lastPathElement) { | |
891 this.infobox.append('div').text('Directory: ' + this.pathFor(datum)) | |
892 this.infobox.append('div').text('Size: ' + sizeish); | |
893 } else { | |
894 if (datum.k === 'p') { // path | |
895 this.infobox.append('div').text('File: ' + this.pathFor(datum)) | |
896 this.infobox.append('div').text('Size: ' + sizeish); | |
897 } else if (datum.k === 'b') { // bucket | |
898 this.infobox.append('div').text('Symbol Bucket: ' + | |
899 D3SymbolTreeMap._getSymbolDescription(datum.t)); | |
900 this.infobox.append('div').text('Count: ' + numSymbols); | |
901 this.infobox.append('div').text('Size: ' + sizeish); | |
902 this.infobox.append('div').text('Location: ' + this.pathFor(datum)) | |
903 } else if (datum.k === 's') { // symbol | |
904 this.infobox.append('div').text('Symbol: ' + datum.n); | |
905 this.infobox.append('div').text('Type: ' + | |
906 D3SymbolTreeMap._getSymbolDescription(datum.t)); | |
907 this.infobox.append('div').text('Size: ' + sizeish); | |
908 this.infobox.append('div').text('Location: ' + this.pathFor(datum)) | |
909 } | |
910 } | |
911 if (datum.k === 'p') { | |
912 this.infobox.append('div') | |
913 .text('Number of symbols: ' + D3SymbolTreeMap._pretty(numSymbols)); | |
914 if (datum.symbol_stats) { // can be empty if filters are applied | |
915 var table = this.infobox.append('table') | |
916 .attr('border', 1).append('tbody'); | |
917 var header = table.append('tr'); | |
918 header.append('th').text('Type'); | |
919 header.append('th').text('Count'); | |
920 header.append('th') | |
921 .style('white-space', 'nowrap') | |
922 .text('Total Size (Bytes)'); | |
923 for (var x=0; x<D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { | |
924 symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); | |
925 var stats = datum.symbol_stats[symbol_type]; | |
926 if (stats !== undefined) { | |
927 var tr = table.append('tr'); | |
928 tr.append('td') | |
929 .style('white-space', 'nowrap') | |
930 .text(D3SymbolTreeMap._getSymbolDescription( | |
931 symbol_type)); | |
932 tr.append('td').text(D3SymbolTreeMap._pretty(stats.count)); | |
933 tr.append('td').text(D3SymbolTreeMap._pretty(stats.size)); | |
934 } | |
935 } | |
936 } | |
937 } | |
938 this.infobox.style('visibility', 'visible'); | |
939 } | |
940 | |
941 D3SymbolTreeMap.prototype._hideInfoBox = function(datum) { | |
942 this.infobox.style('visibility', 'hidden'); | |
943 } | |
944 | |
945 D3SymbolTreeMap.prototype._moveInfoBox = function(event) { | |
946 var element = document.getElementById('infobox'); | |
947 var w = element.offsetWidth; | |
948 var h = element.offsetHeight; | |
949 var offsetLeft = 10; | |
950 var offsetTop = 10; | |
951 | |
952 var rightLimit = window.innerWidth; | |
953 var rightEdge = event.pageX + offsetLeft + w; | |
954 if (rightEdge > rightLimit) { | |
955 // Too close to screen edge, reflect around the cursor | |
956 offsetLeft = -1 * (w + offsetLeft); | |
957 } | |
958 | |
959 var bottomLimit = window.innerHeight; | |
960 var bottomEdge = event.pageY + offsetTop + h; | |
961 if (bottomEdge > bottomLimit) { | |
962 // Too close ot screen edge, reflect around the cursor | |
963 offsetTop = -1 * (h + offsetTop); | |
964 } | |
965 | |
966 this.infobox.style('top', (event.pageY + offsetTop) + 'px') | |
967 .style('left', (event.pageX + offsetLeft) + 'px'); | |
968 } | |
969 | |
970 D3SymbolTreeMap.prototype.biggestSymbols = function(maxRecords) { | |
971 var result = undefined; | |
972 var smallest = undefined; | |
973 var sortFunction = function(a,b) { | |
974 var result = b.value - a.value; | |
975 if (result !== 0) return result; // sort by size | |
976 var pathA = treemap.pathFor(a); // sort by path | |
977 var pathB = treemap.pathFor(b); | |
978 if (pathA > pathB) return 1; | |
979 if (pathB > pathA) return -1; | |
980 return a.n - b.n; // sort by symbol name | |
981 }; | |
982 this.visitFromDisplayedRoot(function(datum) { | |
983 if (datum.children) return; // ignore non-leaves | |
984 if (!result) { // first element | |
985 result = [datum]; | |
986 smallest = datum.value; | |
987 return; | |
988 } | |
989 if (result.length < maxRecords) { // filling the array | |
990 result.push(datum); | |
991 return; | |
992 } | |
993 if (datum.value > smallest) { // array is already full | |
994 result.push(datum); | |
995 result.sort(sortFunction); | |
996 result.pop(); // get rid of smallest element | |
997 smallest = result[maxRecords - 1].value; // new threshold for entry | |
998 } | |
999 }); | |
1000 result.sort(sortFunction); | |
1001 return result; | |
1002 } | |
1003 | |
1004 D3SymbolTreeMap.prototype.biggestPaths = function(maxRecords) { | |
1005 var result = undefined; | |
1006 var smallest = undefined; | |
1007 var sortFunction = function(a,b) { | |
1008 var result = b.value - a.value; | |
1009 if (result !== 0) return result; // sort by size | |
1010 var pathA = treemap.pathFor(a); // sort by path | |
1011 var pathB = treemap.pathFor(b); | |
1012 if (pathA > pathB) return 1; | |
1013 if (pathB > pathA) return -1; | |
1014 console.log('warning, multiple entries for the same path: ' + pathA); | |
1015 return 0; // should be impossible | |
1016 }; | |
1017 this.visitFromDisplayedRoot(function(datum) { | |
1018 if (!datum.lastPathElement) return; // ignore non-files | |
1019 if (!result) { // first element | |
1020 result = [datum]; | |
1021 smallest = datum.value; | |
1022 return; | |
1023 } | |
1024 if (result.length < maxRecords) { // filling the array | |
1025 result.push(datum); | |
1026 return; | |
1027 } | |
1028 if (datum.value > smallest) { // array is already full | |
1029 result.push(datum); | |
1030 result.sort(sortFunction); | |
1031 result.pop(); // get rid of smallest element | |
1032 smallest = result[maxRecords - 1].value; // new threshold for entry | |
1033 } | |
1034 }); | |
1035 result.sort(sortFunction); | |
1036 return result; | |
1037 } | |
OLD | NEW |