blob: f98d8ce8f2ebb45869367678712789cf91d4a4a1 [file] [log] [blame]
// Copyright 2014 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function D3BlobTreeMap(mapWidth, mapHeight, levelsToShow) {
this._mapContainer = undefined;
this._mapWidth = mapWidth;
this._mapHeight = mapHeight;
this.boxPadding = {'l': 5, 'r': 5, 't': 20, 'b': 5};
this.infobox = undefined;
this._maskContainer = undefined;
this._highlightContainer = undefined;
// Transition in this order:
// 1. Exiting items go away.
// 2. Updated items move.
// 3. New items enter.
this._exitDuration=500;
this._updateDuration=500;
this._enterDuration=500;
this._firstTransition=true;
this._layout = undefined;
this._currentRoot = undefined;
this._currentNodes = undefined;
this._treeData = undefined;
this._maxLevelsToShow = levelsToShow;
this._currentMaxDepth = this._maxLevelsToShow;
}
/**
* Make a number pretty, with comma separators.
*/
D3BlobTreeMap._pretty = function(num) {
var asString = String(num);
var result = '';
var counter = 0;
for (var x = asString.length - 1; x >= 0; x--) {
counter++;
if (counter === 4) {
result = ',' + result;
counter = 1;
}
result = asString.charAt(x) + result;
}
return result;
}
/**
* Express a number in terms of KiB, MiB, GiB, etc.
* Note that these are powers of 2, not of 10.
*/
D3BlobTreeMap._byteify = function(num) {
var suffix;
if (num >= 1024) {
if (num >= 1024 * 1024 * 1024) {
suffix = 'GiB';
num = num / (1024 * 1024 * 1024);
} else if (num >= 1024 * 1024) {
suffix = 'MiB';
num = num / (1024 * 1024);
} else if (num >= 1024) {
suffix = 'KiB'
num = num / 1024;
}
return num.toFixed(2) + ' ' + suffix;
}
return num + ' B';
}
D3BlobTreeMap._BLOB_TYPE_DESCRIPTIONS = {
'dart': 'Shared Dart',
'?': 'Shared Unrecognized',
'unique': 'Unique',
'uniDart': 'Unique Dart',
};
D3BlobTreeMap._BLOB_TYPES = [];
for (var blob_type in D3BlobTreeMap._BLOB_TYPE_DESCRIPTIONS) {
D3BlobTreeMap._BLOB_TYPES.push(blob_type);
}
/**
* Given a blob type code, look up and return a human-readable description
* of that blob type. If the blob type does not match one of the known
* types, the unrecognized description (corresponding to blob type '?') is
* returned instead of null or undefined.
*/
D3BlobTreeMap._getblobDescription = function(type) {
var result = D3BlobTreeMap._BLOB_TYPE_DESCRIPTIONS[type];
if (result === undefined) {
result = D3BlobTreeMap._BLOB_TYPE_DESCRIPTIONS['?'];
}
return result;
}
D3BlobTreeMap._colorArray = [
'rgb(148,116,204)', // Shared Dart - Deep Purple 300
'rgb(255,213,79)', // Unique - Amber 300
'rgb(77,208,225)', // Shared Other - Cyan 300
'rgb(161,136,127)', // Unique Dart - Brown 300
];
D3BlobTreeMap._initColorMap = function() {
var map = {};
var numColors = D3BlobTreeMap._colorArray.length;
var count = 0;
for (var key in D3BlobTreeMap._BLOB_TYPE_DESCRIPTIONS) {
var index = count++ % numColors;
map[key] = d3.rgb(D3BlobTreeMap._colorArray[index]);
}
D3BlobTreeMap._colorMap = map;
}
D3BlobTreeMap._initColorMap();
D3BlobTreeMap.getColorForType = function(type) {
var result = D3BlobTreeMap._colorMap[type];
if (result === undefined) return d3.rgb('rgb(255,255,255)');
return result;
}
D3BlobTreeMap.prototype.init = function() {
this.infobox = this._createInfoBox();
this._mapContainer = d3.select('body').append('div')
.style('position', 'relative')
.style('width', this._mapWidth)
.style('height', this._mapHeight)
.style('padding', 0)
.style('margin', 0)
.style('box-shadow', '5px 5px 5px #888');
this._layout = this._createTreeMapLayout();
this._setData(tree_data); // TODO: Don't use global 'tree_data'
}
/**
* Sets the data displayed by the treemap and lays out the map.
*/
D3BlobTreeMap.prototype._setData = function(data) {
this._treeData = data;
console.time('_crunchStats');
this._crunchStats(data);
console.timeEnd('_crunchStats');
this._currentRoot = this._treeData;
this._currentNodes = this._layout.nodes(this._currentRoot);
this._currentMaxDepth = this._maxLevelsToShow;
this._doLayout();
}
/**
* Recursively traverses the entire tree starting from the specified node,
* computing statistics and recording metadata as it goes. Call this method
* only once per imported tree.
*/
D3BlobTreeMap.prototype._crunchStats = function(node) {
var stack = [];
stack.idCounter = 0;
this._crunchStatsHelper(stack, node);
}
/**
* Invoke the specified visitor function on all data elements currently shown
* in the treemap including any and all of their children, starting at the
* currently-displayed root and descending recursively. The function will be
* passed the datum element representing each node. No traversal guarantees
* are made.
*/
D3BlobTreeMap.prototype.visitFromDisplayedRoot = function(visitor) {
this._visit(this._currentRoot, visitor);
}
/**
* Helper function for visit functions.
*/
D3BlobTreeMap.prototype._visit = function(datum, visitor) {
visitor.call(this, datum);
if (datum.children) for (var i = 0; i < datum.children.length; i++) {
this._visit(datum.children[i], visitor);
}
}
D3BlobTreeMap.prototype._crunchStatsHelper = function(stack, node) {
// Only overwrite the node ID if it isn't already set.
// This allows stats to be crunched multiple times on subsets of data
// without breaking the data-to-ID bindings. New nodes get new IDs.
if (node.id === undefined) node.id = stack.idCounter++;
if (node.children === undefined) {
// Leaf node (blob); accumulate stats.
for (var i = 0; i < stack.length; i++) {
var ancestor = stack[i];
if (!ancestor.blob_stats) ancestor.blob_stats = {};
if (ancestor.blob_stats[node.t] === undefined) {
// New blob type we haven't seen before, just record.
ancestor.blob_stats[node.t] = {'count': 1,
'size': node.value};
} else {
// Existing blob type, increment.
ancestor.blob_stats[node.t].count++;
ancestor.blob_stats[node.t].size += node.value;
}
}
} else for (var i = 0; i < node.children.length; i++) {
stack.push(node);
this._crunchStatsHelper(stack, node.children[i]);
stack.pop();
}
}
D3BlobTreeMap.prototype._createTreeMapLayout = function() {
var result = d3.layout.treemap()
.padding([this.boxPadding.t, this.boxPadding.r,
this.boxPadding.b, this.boxPadding.l])
.size([this._mapWidth, this._mapHeight]);
return result;
}
D3BlobTreeMap.prototype.resize = function(width, height) {
this._mapWidth = width;
this._mapHeight = height;
this._mapContainer.style('width', width).style('height', height);
this._layout.size([this._mapWidth, this._mapHeight]);
this._currentNodes = this._layout.nodes(this._currentRoot);
this._doLayout();
}
D3BlobTreeMap.prototype._zoomDatum = function(datum) {
if (this._currentRoot === datum) return; // already here
this._hideHighlight(datum);
this._hideInfoBox(datum);
this._currentRoot = datum;
this._currentNodes = this._layout.nodes(this._currentRoot);
this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
console.log('zooming into datum ' + this._currentRoot.n);
this._doLayout();
}
D3BlobTreeMap.prototype.setMaxLevels = function(levelsToShow) {
this._maxLevelsToShow = levelsToShow;
this._currentNodes = this._layout.nodes(this._currentRoot);
this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
console.log('setting max levels to show: ' + this._maxLevelsToShow);
this._doLayout();
}
/**
* Clone the specified tree, returning an independent copy of the data.
* Only the original attributes expected to exist prior to invoking
* _crunchStatsHelper are retained, with the exception of the 'id' attribute
* (which must be retained for proper transitions).
* If the optional filter parameter is provided, it will be called with 'this'
* set to this treemap instance and passed the 'datum' object as an argument.
* When specified, the copy will retain only the data for which the filter
* function returns true.
*/
D3BlobTreeMap.prototype._clone = function(datum, filter) {
var trackingStats = false;
if (this.__cloneState === undefined) {
console.time('_clone');
trackingStats = true;
this.__cloneState = {'accepted': 0, 'rejected': 0,
'forced': 0, 'pruned': 0};
}
// Must go depth-first. All parents of children that are accepted by the
// filter must be preserved!
var copy = {'n': datum.n, 'k': datum.k};
var childAccepted = false;
if (datum.children !== undefined) {
for (var i = 0; i < datum.children.length; i++) {
var copiedChild = this._clone(datum.children[i], filter);
if (copiedChild !== undefined) {
childAccepted = true; // parent must also be accepted.
if (copy.children === undefined) copy.children = [];
copy.children.push(copiedChild);
}
}
}
// Ignore nodes that don't match the filter, when present.
var accept = false;
if (childAccepted) {
// Parent of an accepted child must also be accepted.
this.__cloneState.forced++;
accept = true;
} else if (filter !== undefined && filter.call(this, datum) !== true) {
this.__cloneState.rejected++;
} else if (datum.children === undefined) {
// Accept leaf nodes that passed the filter
this.__cloneState.accepted++;
accept = true;
} else {
// Non-leaf node. If no children are accepted, prune it.
this.__cloneState.pruned++;
}
if (accept) {
if (datum.id !== undefined) copy.id = datum.id;
if (datum.lastPathElement !== undefined) {
copy.lastPathElement = datum.lastPathElement;
}
if (datum.t !== undefined) copy.t = datum.t;
if (datum.value !== undefined && datum.children === undefined) {
copy.value = datum.value;
}
} else {
// Discard the copy we were going to return
copy = undefined;
}
if (trackingStats === true) {
// We are the fist call in the recursive chain.
console.timeEnd('_clone');
var totalAccepted = this.__cloneState.accepted +
this.__cloneState.forced;
console.log(
totalAccepted + ' nodes retained (' +
this.__cloneState.forced + ' forced by accepted children, ' +
this.__cloneState.accepted + ' accepted on their own merits), ' +
this.__cloneState.rejected + ' nodes (and their children) ' +
'filtered out,' +
this.__cloneState.pruned + ' nodes pruned because because no ' +
'children remained.');
delete this.__cloneState;
}
return copy;
}
D3BlobTreeMap.prototype.filter = function(filter) {
// Ensure we have a copy of the original root.
if (this._backupTree === undefined) this._backupTree = this._treeData;
this._mapContainer.selectAll('div').remove();
this._setData(this._clone(this._backupTree, filter));
}
D3BlobTreeMap.prototype._doLayout = function() {
console.time('_doLayout');
this._handleInodes();
this._handleLeaves();
this._firstTransition = false;
console.timeEnd('_doLayout');
}
D3BlobTreeMap.prototype._highlightElement = function(datum, selection) {
this._showHighlight(datum, selection);
}
D3BlobTreeMap.prototype._unhighlightElement = function(datum, selection) {
this._hideHighlight(datum, selection);
}
D3BlobTreeMap.prototype._handleInodes = function() {
console.time('_handleInodes');
var thisTreeMap = this;
var inodes = this._currentNodes.filter(function(datum){
return (datum.depth <= thisTreeMap._currentMaxDepth) &&
datum.children !== undefined;
});
var cellsEnter = this._mapContainer.selectAll('div.inode')
.data(inodes, function(datum) { return datum.id; })
.enter()
.append('div').attr('class', 'inode').attr('id', function(datum){
return 'node-' + datum.id;});
// Define enter/update/exit for inodes
cellsEnter
.append('div')
.attr('class', 'rect inode_rect_entering')
.style('z-index', function(datum) { return datum.id * 2; })
.style('position', 'absolute')
.style('left', function(datum) { return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum){ return datum.dx; })
.style('height', function(datum){ return datum.dy; })
.style('opacity', '0')
.style('border', '1px solid black')
.style('background-image', function(datum) {
return thisTreeMap._makeblobBucketBackgroundImage.call(
thisTreeMap, datum);
})
.style('background-color', function(datum) {
if (datum.t === undefined) return 'rgb(220,220,220)';
return D3BlobTreeMap.getColorForType(datum.t).toString();
})
.on('mouseover', function(datum){
thisTreeMap._highlightElement.call(
thisTreeMap, datum, d3.select(this));
thisTreeMap._showInfoBox.call(thisTreeMap, datum);
})
.on('mouseout', function(datum){
thisTreeMap._unhighlightElement.call(
thisTreeMap, datum, d3.select(this));
thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
})
.on('mousemove', function(){
thisTreeMap._moveInfoBox.call(thisTreeMap, event);
})
.on('dblclick', function(datum){
if (datum !== thisTreeMap._currentRoot) {
// Zoom into the selection
thisTreeMap._zoomDatum(datum);
} else if (datum.parent) {
console.log('event.shiftKey=' + event.shiftKey);
if (event.shiftKey === true) {
// Back to root
thisTreeMap._zoomDatum(thisTreeMap._treeData);
} else {
// Zoom out of the selection
thisTreeMap._zoomDatum(datum.parent);
}
}
});
cellsEnter
.append('div')
.attr('class', 'label inode_label_entering')
.style('z-index', function(datum) { return (datum.id * 2) + 1; })
.style('position', 'absolute')
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum) { return datum.dx; })
.style('height', function(datum) { return thisTreeMap.boxPadding.t; })
.style('opacity', '0')
.style('pointer-events', 'none')
.style('-webkit-user-select', 'none')
.style('overflow', 'hidden') // required for ellipsis
.style('white-space', 'nowrap') // required for ellipsis
.style('text-overflow', 'ellipsis')
.style('text-align', 'center')
.style('vertical-align', 'top')
.style('visibility', function(datum) {
return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
})
.text(function(datum) {
var sizeish = ' [' + D3BlobTreeMap._byteify(datum.value) + ']'
var text;
if (datum === thisTreeMap._currentRoot) {
// The top-most level should always show the complete path
text = thisTreeMap.pathFor(datum);
} else {
// Anything that isn't a bucket or a leaf (blob) or the
// current root should just show its name.
text = datum.n;
}
return text + sizeish;
}
);
// Complicated transition logic:
// For nodes that are entering, we want to fade them in in-place AFTER
// any adjusting nodes have resized and moved around. That way, new nodes
// seamlessly appear in the right spot after their containers have resized
// and moved around.
// To do this we do some trickery:
// 1. Define a '_entering' class on the entering elements
// 2. Use this to select only the entering elements and apply the opacity
// transition.
// 3. Use the same transition to drop the '_entering' suffix, so that they
// will correctly update in later zoom/resize/whatever operations.
// 4. The update transition is achieved by selecting the elements without
// the '_entering_' suffix and applying movement and resizing transition
// effects.
this._mapContainer.selectAll('div.inode_rect_entering').transition()
.duration(thisTreeMap._enterDuration).delay(
this._firstTransition ? 0 : thisTreeMap._exitDuration +
thisTreeMap._updateDuration)
.attr('class', 'rect inode_rect')
.style('opacity', '1')
this._mapContainer.selectAll('div.inode_label_entering').transition()
.duration(thisTreeMap._enterDuration).delay(
this._firstTransition ? 0 : thisTreeMap._exitDuration +
thisTreeMap._updateDuration)
.attr('class', 'label inode_label')
.style('opacity', '1')
this._mapContainer.selectAll('div.inode_rect').transition()
.duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
.style('opacity', '1')
.style('background-image', function(datum) {
return thisTreeMap._makeblobBucketBackgroundImage.call(
thisTreeMap, datum);
})
.style('left', function(datum) { return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum){ return datum.dx; })
.style('height', function(datum){ return datum.dy; });
this._mapContainer.selectAll('div.inode_label').transition()
.duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
.style('opacity', '1')
.style('visibility', function(datum) {
return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
})
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum) { return datum.dx; })
.style('height', function(datum) { return thisTreeMap.boxPadding.t; })
.text(function(datum) {
var sizeish = ' [' + D3BlobTreeMap._byteify(datum.value) + ']'
var text;
if (datum === thisTreeMap._currentRoot) {
// The top-most level should always show the complete path
text = thisTreeMap.pathFor(datum);
} else {
// Anything that isn't a bucket or a leaf (blob) or the
// current root should just show its name.
text = datum.n;
}
return text + sizeish;
});
var exit = this._mapContainer.selectAll('div.inode')
.data(inodes, function(datum) { return 'inode-' + datum.id; })
.exit();
exit.selectAll('div.inode_rect').transition().duration(
thisTreeMap._exitDuration).style('opacity', 0);
exit.selectAll('div.inode_label').transition().duration(
thisTreeMap._exitDuration).style('opacity', 0);
exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
console.log(inodes.length + ' inodes layed out.');
console.timeEnd('_handleInodes');
}
D3BlobTreeMap.prototype._handleLeaves = function() {
console.time('_handleLeaves');
var color_fn = d3.scale.category10();
var thisTreeMap = this;
var leaves = this._currentNodes.filter(function(datum){
return (datum.depth <= thisTreeMap._currentMaxDepth) &&
datum.children === undefined; });
var cellsEnter = this._mapContainer.selectAll('div.leaf')
.data(leaves, function(datum) { return datum.id; })
.enter()
.append('div').attr('class', 'leaf').attr('id', function(datum){
return 'node-' + datum.id;
});
// Define enter/update/exit for leaves
cellsEnter
.append('div')
.attr('class', 'rect leaf_rect_entering')
.style('z-index', function(datum) { return datum.id * 2; })
.style('position', 'absolute')
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum){ return datum.dx; })
.style('height', function(datum){ return datum.dy; })
.style('opacity', '0')
.style('background-color', function(datum) {
if (datum.t === undefined) return 'rgb(220,220,220)';
return D3BlobTreeMap.getColorForType(datum.t)
.darker(0.3).toString();
})
.style('border', '1px solid black')
.on('mouseover', function(datum){
thisTreeMap._highlightElement.call(
thisTreeMap, datum, d3.select(this));
thisTreeMap._showInfoBox.call(thisTreeMap, datum);
})
.on('mouseout', function(datum){
thisTreeMap._unhighlightElement.call(
thisTreeMap, datum, d3.select(this));
thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
})
.on('mousemove', function(){ thisTreeMap._moveInfoBox.call(
thisTreeMap, event);
});
cellsEnter
.append('div')
.attr('class', 'label leaf_label_entering')
.style('z-index', function(datum) { return (datum.id * 2) + 1; })
.style('position', 'absolute')
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum) { return datum.dx; })
.style('height', function(datum) { return datum.dy; })
.style('opacity', '0')
.style('pointer-events', 'none')
.style('-webkit-user-select', 'none')
.style('overflow', 'hidden') // required for ellipsis
.style('white-space', 'nowrap') // required for ellipsis
.style('text-overflow', 'ellipsis')
.style('text-align', 'center')
.style('vertical-align', 'middle')
.style('visibility', function(datum) {
return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
})
.text(function(datum) { return datum.n; });
// Complicated transition logic: See note in _handleInodes()
this._mapContainer.selectAll('div.leaf_rect_entering').transition()
.duration(thisTreeMap._enterDuration).delay(
this._firstTransition ? 0 : thisTreeMap._exitDuration +
thisTreeMap._updateDuration)
.attr('class', 'rect leaf_rect')
.style('opacity', '1')
this._mapContainer.selectAll('div.leaf_label_entering').transition()
.duration(thisTreeMap._enterDuration).delay(
this._firstTransition ? 0 : thisTreeMap._exitDuration +
thisTreeMap._updateDuration)
.attr('class', 'label leaf_label')
.style('opacity', '1')
this._mapContainer.selectAll('div.leaf_rect').transition()
.duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
.style('opacity', '1')
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum){ return datum.dx; })
.style('height', function(datum){ return datum.dy; });
this._mapContainer.selectAll('div.leaf_label').transition()
.duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
.style('opacity', '1')
.style('visibility', function(datum) {
return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
})
.style('left', function(datum){ return datum.x; })
.style('top', function(datum){ return datum.y; })
.style('width', function(datum) { return datum.dx; })
.style('height', function(datum) { return datum.dy; });
var exit = this._mapContainer.selectAll('div.leaf')
.data(leaves, function(datum) { return 'leaf-' + datum.id; })
.exit();
exit.selectAll('div.leaf_rect').transition()
.duration(thisTreeMap._exitDuration)
.style('opacity', 0);
exit.selectAll('div.leaf_label').transition()
.duration(thisTreeMap._exitDuration)
.style('opacity', 0);
exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
console.log(leaves.length + ' leaves layed out.');
console.timeEnd('_handleLeaves');
}
D3BlobTreeMap.prototype._makeblobBucketBackgroundImage = function(datum) {
if (!(datum.t === undefined && datum.depth == this._currentMaxDepth)) {
return 'none';
}
var text = '';
var lastStop = 0;
for (var x = 0; x < D3BlobTreeMap._BLOB_TYPES.length; x++) {
blob_type = D3BlobTreeMap._BLOB_TYPES[x];
var stats = datum.blob_stats[blob_type];
if (stats !== undefined) {
if (text.length !== 0) {
text += ', ';
}
var percent = 100 * (stats.size / datum.value);
var nowStop = lastStop + percent;
var tempcolor = D3BlobTreeMap.getColorForType(blob_type);
var color = d3.rgb(tempcolor).toString();
text += color + ' ' + lastStop + '%, ' + color + ' ' +
nowStop + '%';
lastStop = nowStop;
}
}
return 'linear-gradient(' + (datum.dx > datum.dy ? 'to right' :
'to bottom') + ', ' + text + ')';
}
D3BlobTreeMap.prototype.pathFor = function(datum) {
if (datum.__path) return datum.__path;
parts=[];
node = datum;
while (node) {
if (node.k === 'p') { // path node
if(node.n !== '/') parts.unshift(node.n);
}
node = node.parent;
}
datum.__path = '/' + parts.join('/');
return datum.__path;
}
D3BlobTreeMap.prototype._createHighlight = function(datum, selection) {
var x = parseInt(selection.style('left'));
var y = parseInt(selection.style('top'));
var w = parseInt(selection.style('width'));
var h = parseInt(selection.style('height'));
datum.highlight = this._mapContainer.append('div')
.attr('id', 'h-' + datum.id)
.attr('class', 'highlight')
.style('pointer-events', 'none')
.style('-webkit-user-select', 'none')
.style('z-index', '999999')
.style('position', 'absolute')
.style('top', y-2)
.style('left', x-2)
.style('width', w+4)
.style('height', h+4)
.style('margin', 0)
.style('padding', 0)
.style('border', '4px outset rgba(250,40,200,0.9)')
.style('box-sizing', 'border-box')
.style('opacity', 0.0);
}
D3BlobTreeMap.prototype._showHighlight = function(datum, selection) {
if (datum === this._currentRoot) return;
if (datum.highlight === undefined) {
this._createHighlight(datum, selection);
}
datum.highlight.transition().duration(200).style('opacity', 1.0);
}
D3BlobTreeMap.prototype._hideHighlight = function(datum, selection) {
if (datum.highlight === undefined) return;
datum.highlight.transition().duration(750)
.style('opacity', 0)
.each('end', function(){
if (datum.highlight) datum.highlight.remove();
delete datum.highlight;
});
}
D3BlobTreeMap.prototype._createInfoBox = function() {
return d3.select('body')
.append('div')
.attr('id', 'infobox')
.style('z-index', '2147483647') // (2^31) - 1: Hopefully safe :)
.style('position', 'absolute')
.style('visibility', 'hidden')
.style('background-color', 'rgba(255,255,255, 0.9)')
.style('border', '1px solid black')
.style('padding', '10px')
.style('-webkit-user-select', 'none')
.style('box-shadow', '3px 3px rgba(70,70,70,0.5)')
.style('border-radius', '10px')
.style('white-space', 'nowrap');
}
D3BlobTreeMap.prototype._showInfoBox = function(datum) {
this.infobox.text('');
var numBlobs = 0;
var sizeish = D3BlobTreeMap._pretty(datum.value) + ' bytes (' +
D3BlobTreeMap._byteify(datum.value) + ')';
if (datum.k === 'p') { // path
if (datum.blob_stats) { // can be empty if filters are applied
for (var x = 0; x < D3BlobTreeMap._BLOB_TYPES.length; x++) {
blob_type = D3BlobTreeMap._BLOB_TYPES[x];
var stats = datum.blob_stats[blob_type];
if (stats !== undefined) numBlobs += stats.count;
}
}
} else if (datum.k === 's') { // blob
numBlobs = 1;
}
if (datum.k === 'p' && !datum.lastPathElement) {
this.infobox.append('div').text('Package: ' + this.pathFor(datum))
this.infobox.append('div').text('Size: ' + sizeish);
} else {
if (datum.k === 'p') { // path
this.infobox.append('div').text('File: ' + this.pathFor(datum))
this.infobox.append('div').text('Size: ' + sizeish);
} else if (datum.k === 's') { // blob
this.infobox.append('div').text('blob: ' + datum.n);
this.infobox.append('div').text('Type: ' +
D3BlobTreeMap._getblobDescription(datum.t));
this.infobox.append('div').text('Size (proportional): ' + sizeish);
this.infobox.append('div').text('Reference count: ' + datum.c);
this.infobox.append('div').text('Location: ' + this.pathFor(datum))
}
}
if (datum.k === 'p') {
this.infobox.append('div')
.text('Number of blobs: ' + D3BlobTreeMap._pretty(numBlobs));
if (datum.blob_stats) { // can be empty if filters are applied
var table = this.infobox.append('table')
.attr('border', 1).append('tbody');
var header = table.append('tr');
header.append('th').text('Type');
header.append('th').text('Count');
header.append('th')
.style('white-space', 'nowrap')
.text('Total Size (Bytes)');
for (var x = 0; x < D3BlobTreeMap._BLOB_TYPES.length; x++) {
blob_type = D3BlobTreeMap._BLOB_TYPES[x];
var stats = datum.blob_stats[blob_type];
if (stats !== undefined) {
var tr = table.append('tr');
tr.append('td')
.style('white-space', 'nowrap')
.text(D3BlobTreeMap._getblobDescription(
blob_type));
tr.append('td').text(D3BlobTreeMap._pretty(stats.count));
tr.append('td').text(D3BlobTreeMap._pretty(stats.size));
}
}
}
}
this.infobox.style('visibility', 'visible');
}
D3BlobTreeMap.prototype._hideInfoBox = function(datum) {
this.infobox.style('visibility', 'hidden');
}
D3BlobTreeMap.prototype._moveInfoBox = function(event) {
var element = document.getElementById('infobox');
var w = element.offsetWidth;
var h = element.offsetHeight;
var offsetLeft = 10;
var offsetTop = 10;
var rightLimit = window.innerWidth;
var rightEdge = event.pageX + offsetLeft + w;
if (rightEdge > rightLimit) {
// Too close to screen edge, reflect around the cursor
offsetLeft = -1 * (w + offsetLeft);
}
var bottomLimit = window.innerHeight;
var bottomEdge = event.pageY + offsetTop + h;
if (bottomEdge > bottomLimit) {
// Too close ot screen edge, reflect around the cursor
offsetTop = -1 * (h + offsetTop);
}
this.infobox.style('top', (event.pageY + offsetTop) + 'px')
.style('left', (event.pageX + offsetLeft) + 'px');
}
D3BlobTreeMap.prototype.biggestblobs = function(maxRecords) {
var result = undefined;
var smallest = undefined;
var sortFunction = function(a,b) {
var result = b.value - a.value;
if (result !== 0) return result; // sort by size
var pathA = treemap.pathFor(a); // sort by path
var pathB = treemap.pathFor(b);
if (pathA > pathB) return 1;
if (pathB > pathA) return -1;
return a.n - b.n; // sort by blob name
};
this.visitFromDisplayedRoot(function(datum) {
if (datum.children) return; // ignore non-leaves
if (!result) { // first element
result = [datum];
smallest = datum.value;
return;
}
if (result.length < maxRecords) { // filling the array
result.push(datum);
return;
}
if (datum.value > smallest) { // array is already full
result.push(datum);
result.sort(sortFunction);
result.pop(); // get rid of smallest element
smallest = result[maxRecords - 1].value; // new threshold for entry
}
});
result.sort(sortFunction);
return result;
}
D3BlobTreeMap.prototype.biggestPaths = function(maxRecords) {
var result = undefined;
var smallest = undefined;
var sortFunction = function(a,b) {
var result = b.value - a.value;
if (result !== 0) return result; // sort by size
var pathA = treemap.pathFor(a); // sort by path
var pathB = treemap.pathFor(b);
if (pathA > pathB) return 1;
if (pathB > pathA) return -1;
console.log('warning, multiple entries for the same path: ' + pathA);
return 0; // should be impossible
};
this.visitFromDisplayedRoot(function(datum) {
if (!datum.lastPathElement) return; // ignore non-files
if (!result) { // first element
result = [datum];
smallest = datum.value;
return;
}
if (result.length < maxRecords) { // filling the array
result.push(datum);
return;
}
if (datum.value > smallest) { // array is already full
result.push(datum);
result.sort(sortFunction);
result.pop(); // get rid of smallest element
smallest = result[maxRecords - 1].value; // new threshold for entry
}
});
result.sort(sortFunction);
return result;
}