UNPKG

d3-flame-graphs

Version:

D3.js plugin for rendering flame graphs

531 lines (508 loc) 17.7 kB
(function() { var d3, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; d3 = this.d3 ? this.d3 : require('d3'); if (!d3) { throw new Error("d3.js needs to be loaded"); } d3.flameGraphUtils = { augment: function(node, location) { var childSum, children; children = node.children; if (node.augmented) { return node; } node.originalValue = node.value; node.level = node.children ? 1 : 0; node.hidden = []; node.location = location; if (!(children != null ? children.length : void 0)) { node.augmented = true; return node; } childSum = children.reduce((function(sum, child) { return sum + child.value; }), 0); if (childSum < node.value) { children.push({ value: node.value - childSum, filler: true }); } children.forEach(function(child, idx) { return d3.flameGraphUtils.augment(child, location + "." + idx); }); node.level += children.reduce((function(max, child) { return Math.max(child.level, max); }), 0); node.augmented = true; return node; }, partition: function(data) { return d3.layout.partition().sort(function(a, b) { if (a.filler) { return 1; } if (b.filler) { return -1; } return a.name.localeCompare(b.name); }).nodes(data); }, hide: function(nodes, unhide) { var process, processChildren, processParents, remove, sum; if (unhide == null) { unhide = false; } sum = function(arr) { return arr.reduce((function(acc, val) { return acc + val; }), 0); }; remove = function(arr, val) { var pos; pos = arr.indexOf(val); if (pos >= 0) { return arr.splice(pos, 1); } }; process = function(node, val) { if (unhide) { remove(node.hidden, val); } else { node.hidden.push(val); } return node.value = Math.max(node.originalValue - sum(node.hidden), 0); }; processChildren = function(node, val) { if (!node.children) { return; } return node.children.forEach(function(child) { process(child, val); return processChildren(child, val); }); }; processParents = function(node, val) { var results; results = []; while (node.parent) { process(node.parent, val); results.push(node = node.parent); } return results; }; return nodes.forEach(function(node) { var val; val = node.originalValue; processParents(node, val); process(node, val); return processChildren(node, val); }); } }; d3.flameGraph = function(selector, root, debug) { var FlameGraph, getClassAndMethodName, hash; if (debug == null) { debug = false; } getClassAndMethodName = function(fqdn) { var tokens; if (!fqdn) { return ""; } tokens = fqdn.split("."); return tokens.slice(tokens.length - 2).join("."); }; hash = function(name) { var i, j, maxHash, mod, ref, ref1, result, weight; ref = [0, 0, 1, 10], result = ref[0], maxHash = ref[1], weight = ref[2], mod = ref[3]; name = getClassAndMethodName(name).slice(0, 6); for (i = j = 0, ref1 = name.length - 1; 0 <= ref1 ? j <= ref1 : j >= ref1; i = 0 <= ref1 ? ++j : --j) { result += weight * (name.charCodeAt(i) % mod); maxHash += weight * (mod - 1); weight *= 0.7; } if (maxHash > 0) { return result / maxHash; } else { return result; } }; FlameGraph = (function() { function FlameGraph(selector, root) { this._selector = selector; this._generateAccessors(['margin', 'cellHeight', 'zoomEnabled', 'zoomAction', 'tooltip', 'tooltipPlugin', 'color']); this._ancestors = []; if (debug) { this.console = window.console; } else { this.console = { log: function() {}, time: function() {}, timeEnd: function() {} }; } this._size = [1200, 800]; this._cellHeight = 20; this._margin = { top: 0, right: 0, bottom: 0, left: 0 }; this._color = function(d) { var b, g, r, val; val = hash(d.name); r = 200 + Math.round(55 * val); g = 0 + Math.round(230 * (1 - val)); b = 0 + Math.round(55 * (1 - val)); return "rgb(" + r + ", " + g + ", " + b + ")"; }; this._tooltipEnabled = true; this._zoomEnabled = true; if (this._tooltipEnabled && d3.tip) { this._tooltipPlugin = d3.tip(); } this.console.time('augment'); this.original = d3.flameGraphUtils.augment(root, '0'); this.console.timeEnd('augment'); this.root(this.original); } FlameGraph.prototype.size = function(size) { if (!size) { return this._size; } this._size = size; d3.select(this._selector).select('.flame-graph').attr('width', this._size[0]).attr('height', this._size[1]); return this; }; FlameGraph.prototype.root = function(root) { if (!root) { return this._root; } this.console.time('partition'); this._root = root; this._data = d3.flameGraphUtils.partition(this._root); this.console.timeEnd('partition'); return this; }; FlameGraph.prototype.hide = function(predicate, unhide) { var matches; if (unhide == null) { unhide = false; } matches = this.select(predicate, false); if (!matches.length) { return; } d3.flameGraphUtils.hide(matches, unhide); this._data = d3.flameGraphUtils.partition(this._root); return this.render(); }; FlameGraph.prototype.zoom = function(node, event) { if (!this.zoomEnabled()) { throw new Error("Zoom is disabled!"); } if (this.tip) { this.tip.hide(); } if (indexOf.call(this._ancestors, node) >= 0) { this._ancestors = this._ancestors.slice(0, this._ancestors.indexOf(node)); } else { this._ancestors.push(this._root); } this.root(node).render(); if (typeof this._zoomAction === "function") { this._zoomAction(node, event); } return this; }; FlameGraph.prototype.width = function() { return this.size()[0] - (this.margin().left + this.margin().right); }; FlameGraph.prototype.height = function() { return this.size()[1] - (this.margin().top + this.margin().bottom); }; FlameGraph.prototype.label = function(d) { var label; if (!(d != null ? d.name : void 0)) { return ""; } label = getClassAndMethodName(d.name); return label.substr(0, Math.round(this.x(d.dx) / (this.cellHeight() / 10 * 4))); }; FlameGraph.prototype.select = function(predicate, onlyVisible) { var result; if (onlyVisible == null) { onlyVisible = true; } if (onlyVisible) { return this.container.selectAll('.node').filter(predicate); } else { result = d3.flameGraphUtils.partition(this.original).filter(predicate); return result; } }; FlameGraph.prototype.render = function() { var data, existingContainers, maxLevels, newContainers, ref, renderNode, visibleCells; if (!this._selector) { throw new Error("No DOM element provided"); } this.console.time('render'); if (!this.container) { this._createContainer(); } this.fontSize = (this.cellHeight() / 10) * 0.4; this.x = d3.scale.linear().domain([ 0, d3.max(this._data, function(d) { return d.x + d.dx; }) ]).range([0, this.width()]); visibleCells = Math.floor(this.height() / this.cellHeight()); maxLevels = this._root.level; this.y = d3.scale.quantize().domain([ d3.max(this._data, function(d) { return d.y; }), 0 ]).range(d3.range(maxLevels).map((function(_this) { return function(cell) { return ((cell + visibleCells) - (_this._ancestors.length + maxLevels)) * _this.cellHeight(); }; })(this))); data = this._data.filter((function(_this) { return function(d) { return _this.x(d.dx) > 0.4 && _this.y(d.y) >= 0 && !d.filler; }; })(this)); renderNode = { x: (function(_this) { return function(d) { return _this.x(d.x); }; })(this), y: (function(_this) { return function(d) { return _this.y(d.y); }; })(this), width: (function(_this) { return function(d) { return _this.x(d.dx); }; })(this), height: (function(_this) { return function(d) { return _this.cellHeight(); }; })(this), text: (function(_this) { return function(d) { if (d.name && _this.x(d.dx) > 40) { return _this.label(d); } }; })(this) }; existingContainers = this.container.selectAll('.node').data(data, function(d) { return d.location; }).attr('class', 'node'); this._renderNodes(existingContainers, renderNode); newContainers = existingContainers.enter().append('g').attr('class', 'node'); this._renderNodes(newContainers, renderNode, true); existingContainers.exit().remove(); if (this.zoomEnabled()) { this._renderAncestors()._enableNavigation(); } if (this.tooltip()) { this._renderTooltip(); } this.console.timeEnd('render'); this.console.log("Processed " + this._data.length + " items"); this.console.log("Rendered " + ((ref = this.container.selectAll('.node')[0]) != null ? ref.length : void 0) + " elements"); return this; }; FlameGraph.prototype._createContainer = function() { var offset, svg; d3.select(this._selector).select('svg').remove(); svg = d3.select(this._selector).append('svg').attr('class', 'flame-graph').attr('width', this._size[0]).attr('height', this._size[1]); offset = "translate(" + (this.margin().left) + ", " + (this.margin().top) + ")"; this.container = svg.append('g').attr('transform', offset); return svg.append('rect').attr('width', this._size[0] - (this._margin.left + this._margin.right)).attr('height', this._size[1] - (this._margin.top + this._margin.bottom)).attr('transform', offset).attr('class', 'border-rect'); }; FlameGraph.prototype._renderNodes = function(containers, attrs, enter) { var targetLabels, targetRects; if (enter == null) { enter = false; } if (!enter) { targetRects = containers.selectAll('rect'); } if (enter) { targetRects = containers.append('rect'); } targetRects.attr('fill', (function(_this) { return function(d) { return _this._color(d); }; })(this)).transition().attr('width', attrs.width).attr('height', this.cellHeight()).attr('x', attrs.x).attr('y', attrs.y); if (!enter) { targetLabels = containers.selectAll('text'); } if (enter) { targetLabels = containers.append('text'); } containers.selectAll('text').attr('class', 'label').style('font-size', this.fontSize + "em").transition().attr('dy', (this.fontSize / 2) + "em").attr('x', (function(_this) { return function(d) { return attrs.x(d) + 2; }; })(this)).attr('y', (function(_this) { return function(d, idx) { return attrs.y(d, idx) + _this.cellHeight() / 2; }; })(this)).text(attrs.text); return this; }; FlameGraph.prototype._renderTooltip = function() { if (!this._tooltipPlugin || !this._tooltipEnabled) { return this; } this.tip = this._tooltipPlugin.attr('class', 'd3-tip').html(this.tooltip()).direction((function(_this) { return function(d) { if (_this.x(d.x) + _this.x(d.dx) / 2 > _this.width() - 100) { return 'w'; } if (_this.x(d.x) + _this.x(d.dx) / 2 < 100) { return 'e'; } return 's'; }; })(this)).offset((function(_this) { return function(d) { var x, xOffset, yOffset; x = _this.x(d.x) + _this.x(d.dx) / 2; xOffset = Math.max(Math.ceil(_this.x(d.dx) / 2), 5); yOffset = Math.ceil(_this.cellHeight() / 2); if (_this.width() - 100 < x) { return [0, -xOffset]; } if (x < 100) { return [0, xOffset]; } return [yOffset, 0]; }; })(this)); this.container.call(this.tip); this.container.selectAll('.node').on('mouseover', (function(_this) { return function(d) { return _this.tip.show(d, d3.event.currentTarget); }; })(this)).on('mouseout', this.tip.hide).selectAll('.label').on('mouseover', (function(_this) { return function(d) { return _this.tip.show(d, d3.event.currentTarget.parentNode); }; })(this)).on('mouseout', this.tip.hide); return this; }; FlameGraph.prototype._renderAncestors = function() { var ancestor, ancestorData, ancestors, idx, j, len, newAncestors, prev, renderAncestor; if (!this._ancestors.length) { ancestors = this.container.selectAll('.ancestor').remove(); return this; } ancestorData = this._ancestors.map(function(ancestor, idx) { return { name: ancestor.name, value: idx + 1, location: ancestor.location }; }); for (idx = j = 0, len = ancestorData.length; j < len; idx = ++j) { ancestor = ancestorData[idx]; prev = ancestorData[idx - 1]; if (prev) { prev.children = [ancestor]; } } renderAncestor = { x: (function(_this) { return function(d) { return 0; }; })(this), y: (function(_this) { return function(d) { return _this.height() - (d.value * _this.cellHeight()); }; })(this), width: this.width(), height: this.cellHeight(), text: (function(_this) { return function(d) { return "↩ " + (getClassAndMethodName(d.name)); }; })(this) }; ancestors = this.container.selectAll('.ancestor').data(d3.layout.partition().nodes(ancestorData[0]), function(d) { return d.location; }); this._renderNodes(ancestors, renderAncestor); newAncestors = ancestors.enter().append('g').attr('class', 'ancestor'); this._renderNodes(newAncestors, renderAncestor, true); ancestors.exit().remove(); return this; }; FlameGraph.prototype._enableNavigation = function() { var clickable; clickable = (function(_this) { return function(d) { var ref; return Math.round(_this.width() - _this.x(d.dx)) > 0 && ((ref = d.children) != null ? ref.length : void 0); }; })(this); this.container.selectAll('.node').classed('clickable', (function(_this) { return function(d) { return clickable(d); }; })(this)).on('click', (function(_this) { return function(d) { if (_this.tip) { _this.tip.hide(); } if (clickable(d)) { return _this.zoom(d, d3.event); } }; })(this)); this.container.selectAll('.ancestor').on('click', (function(_this) { return function(d, idx) { if (_this.tip) { _this.tip.hide(); } return _this.zoom(_this._ancestors[idx], d3.event); }; })(this)); return this; }; FlameGraph.prototype._generateAccessors = function(accessors) { var accessor, j, len, results; results = []; for (j = 0, len = accessors.length; j < len; j++) { accessor = accessors[j]; results.push(this[accessor] = (function(accessor) { return function(newValue) { if (!arguments.length) { return this["_" + accessor]; } this["_" + accessor] = newValue; return this; }; })(accessor)); } return results; }; return FlameGraph; })(); return new FlameGraph(selector, root); }; }).call(this);