Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: tools/binary_size/experimental_template/D3SymbolTreeMap.js

Issue 231803002: New binary size tool visualization options. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Update README Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tools/binary_size/README.txt ('k') | tools/binary_size/experimental_template/index.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « tools/binary_size/README.txt ('k') | tools/binary_size/experimental_template/index.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698