| Index: tools/telemetry/telemetry/core/platform/profiler/perf_vis/perf-vis.js
|
| diff --git a/tools/telemetry/telemetry/core/platform/profiler/perf_vis/perf-vis.js b/tools/telemetry/telemetry/core/platform/profiler/perf_vis/perf-vis.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..bcd54d3908f34ee89bb9a3c944e848725f8e1c34
|
| --- /dev/null
|
| +++ b/tools/telemetry/telemetry/core/platform/profiler/perf_vis/perf-vis.js
|
| @@ -0,0 +1,814 @@
|
| +// Dimensions of sunburst.
|
| +var width = 800;
|
| +var height = 800;
|
| +var radius = Math.min(width, height) / 2;
|
| +
|
| +// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
|
| +var b = {
|
| + w: 1600, h: 16, s: 1, t: 10
|
| +};
|
| +
|
| +// Mapping of step names to colors.
|
| +var colors = {
|
| + "Chrome": "#5687d1",
|
| + "Kernel": "#7b615c",
|
| + "GPU Driver": "#cc0000",
|
| + "Java": "#de783b",
|
| + "Android": "#6ab975",
|
| + "Thread": "#aaaaaa",
|
| + "Standard Lib": "#bbbbbb",
|
| +};
|
| +
|
| +/*
|
| +var x = d3.scale.linear()
|
| + .range([0, 2 * Math.PI]);
|
| +
|
| +var y = d3.scale.sqrt()
|
| + .range([50, radius*1.5]);
|
| +
|
| +var partition = d3.layout.partition()
|
| + .size([1, 1]) // radius * radius
|
| + .value(function(d) { return d.size; });
|
| +
|
| +var arc = d3.svg.arc()
|
| + .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
|
| + .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
|
| + .innerRadius(function(d) { return Math.max(0, y((d.y))); })
|
| + .outerRadius(function(d) { return Math.max(0, y((d.y + d.dy))); });
|
| +*/
|
| +
|
| +clickedY = 0;
|
| +var min_x = 0.0;
|
| +var max_x = 1.0;
|
| +var min_y = 0.0;
|
| +
|
| +var childTable = null;
|
| +var bottomUpTable = null;
|
| +
|
| +// attach the .equals method to Array's prototype to call it on any array
|
| +Array.prototype.equals = function (array) {
|
| + // if the other array is a falsy value, return
|
| + if (!array)
|
| + return false;
|
| +
|
| + // compare lengths - can save a lot of time
|
| + if (this.length != array.length)
|
| + return false;
|
| +
|
| + for (var i = 0, l=this.length; i < l; i++) {
|
| + // Check if we have nested arrays
|
| + if (this[i] instanceof Array && array[i] instanceof Array) {
|
| + // recurse into the nested arrays
|
| + if (!this[i].equals(array[i]))
|
| + return false;
|
| + }
|
| + else if (this[i] != array[i]) {
|
| + // Warning - two different object instances will never be equal: {x:20} != {x:20}
|
| + return false;
|
| + }
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +$(document).ready(function() {
|
| + //$('#demo').html( '<table cellpadding="0" cellspacing="0" border="0" class="display" id="example"></table>' );
|
| +
|
| + //var myLayout = $('body').layout({ applyDefaultStyles: true });
|
| + //myLayout.options.west.resizable = true;
|
| + //myLayout.sizePane("west", 820);
|
| +
|
| + childTable = $('#example').dataTable( {
|
| + "dom": '<"child_toolbar">frtip',
|
| + "fnRowCallback": function( nRow ) {
|
| + // column index starts with 0 and we check if cells[2] is null to be ultra safe
|
| + if(nRow.cells[3]) nRow.cells[3].noWrap = true;
|
| + return nRow;
|
| + },
|
| + "scrollY": "280",
|
| + "scrollX": true,
|
| + "bAutoWidth": false,
|
| + "paging": false,
|
| + "info": false,
|
| + "order": [[ 2, "desc" ]],
|
| + "columns": [
|
| + { "title": "", "sClass": "center", "width": "15px" },
|
| + { "title": "Time", "sClass": "right", "width": "30px" },
|
| + { "title": "Percent", "sClass": "right", "width": "30px" },
|
| + { "title": "Method" }
|
| + ]
|
| + });
|
| +
|
| + $("div.child_toolbar").html('<b>Cellees:</b>');
|
| +
|
| + bottomUpTable = $('#bottom-up').dataTable( {
|
| + "dom": '<"bottomup_toolbar">frtip',
|
| + "fnRowCallback": function( nRow ) {
|
| + // column index starts with 0 and we check if cells[2] is null to be ultra safe
|
| + if(nRow.cells[3]) nRow.cells[3].noWrap = true;
|
| + return nRow;
|
| + },
|
| + "scrollY": "280",
|
| + "scrollX": true,
|
| + "bAutoWidth": false,
|
| + "paging": false,
|
| + "info": false,
|
| + "order": [[ 2, "desc" ]],
|
| + "columns": [
|
| + { "title": "", "sClass": "center", "width": "15px" },
|
| + { "title": "Time (Incl)", "sClass": "right", "width": "30px" },
|
| + { "title": "Time (Self)", "sClass": "right", "width": "30px" },
|
| + { "title": "Call Sites", "sClass": "right", "width": "30px" },
|
| + { "title": "Method" }
|
| + ]
|
| + });
|
| +
|
| + $("div.bottomup_toolbar").html('<b>All methods:</b>');
|
| +
|
| + createVisualization(json);
|
| +} );
|
| +
|
| +// Main function to draw and set up the visualization, once we have the data.
|
| +function createVisualization(json) {
|
| +
|
| + var legendText = undefined;
|
| + // Basic setup of page elements.
|
| + initializeBreadcrumbTrail();
|
| + //drawLegend();
|
| +
|
| + var vis = d3.select("#chart").append("svg:svg")
|
| + .attr("width", width)
|
| + .attr("height", height)
|
| + .append("svg:g")
|
| + .attr("id", "container")
|
| + .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
|
| +
|
| + var clickedNode = null;
|
| + var click_stack = new Array();
|
| + click_stack.push({
|
| + id: 0,
|
| + });
|
| +
|
| + function zoomout(d) {
|
| + if (click_stack.length > 1) {
|
| + click_stack.pop();
|
| + var hash = click_stack[click_stack.length - 1];
|
| + click_stack.pop();
|
| + location.hash = JSON.stringify(hash);
|
| + }
|
| + }
|
| +
|
| + // Bounding circle underneath the sunburst, to make it easier to detect
|
| + // when the mouse leaves the parent g.
|
| + vis.append("svg:circle")
|
| + .attr("r", radius)
|
| + .style("opacity", 0)
|
| + .on("click", zoomout);
|
| +
|
| + // For efficiency, filter nodes to keep only those large enough to see.
|
| + //var nodes = partition.nodes(json);
|
| + //nodes.forEach(function f(d, i) { d.id = i; })
|
| +
|
| + /*
|
| + $(document).ready(function() {
|
| + $('#example').dataTable( {
|
| + "scrollY": 200,
|
| + "scrollX": true,
|
| + "paging": false,
|
| + "info": false
|
| + } );
|
| + } );
|
| + */
|
| +
|
| + json.comp = 'Thread';
|
| + var stackIdToNode = {};
|
| + var stackIdCounter = 0;
|
| + function setNodeIds(node) {
|
| + node.stackId = stackIdCounter++;
|
| + stackIdToNode[node.stackId] = node;
|
| + if (node.children) {
|
| + d3.values(node.children).forEach(function f(c, i) {
|
| + setNodeIds(c);
|
| + });
|
| + }
|
| + }
|
| + setNodeIds(json);
|
| +
|
| + var nodes = undefined;
|
| + var yDomainMin = undefined;
|
| + var yDomainMax = undefined;
|
| + var xScaler = undefined;
|
| + var yScaler = undefined;
|
| + var arc = undefined;
|
| + var arcTween = undefined;
|
| + var currSelection = {stack: [], hide: [], id: -1};
|
| +
|
| + function initPartition(data) {
|
| + ///////////////////////
|
| + var partition = d3.layout.partition()
|
| + .size([1, 1]) // radius * radius
|
| + .value(function(d) { return d.size; });
|
| + nodes = partition.nodes(data);
|
| + nodes.forEach(function f(node, i) {
|
| + node.id = i;
|
| + });
|
| +
|
| + var depth = 1.0 + d3.max(nodes, function(d) { return d.depth; });
|
| + yDomainMin = 1.0 / depth;
|
| + yDomainMax = yDomainMin + yDomainMin * 40; //Math.min(Math.max(depth, 20), 50) / depth;
|
| +
|
| + xScaler = d3.scale.linear()
|
| + .range([0, 2 * Math.PI]);
|
| +
|
| + yScaler = d3.scale.sqrt()
|
| + .domain([yDomainMin, yDomainMax])
|
| + .range([50, radius]);
|
| +
|
| + arc = d3.svg.arc()
|
| + .startAngle(function(d) {
|
| + return Math.max(0, Math.min(2 * Math.PI, xScaler(d.x)));
|
| + })
|
| + .endAngle(function(d) {
|
| + return Math.max(0, Math.min(2 * Math.PI, xScaler(d.x + d.dx)));
|
| + })
|
| + .innerRadius(function(d) { return Math.max(0, yScaler((d.y))); })
|
| + .outerRadius(function(d) { return Math.max(0, yScaler((d.y + d.dy))); });
|
| +
|
| + // Interpolate the scales!
|
| + arcTween = function (minX, maxX, minY) {
|
| + var xd, yd, yr;
|
| +
|
| + if (minY > 0) {
|
| + xd = d3.interpolate(xScaler.domain(), [minX, maxX]);
|
| + yd = d3.interpolate(yScaler.domain(), [minY, minY + yDomainMin * 40]);
|
| + yr = d3.interpolate(yScaler.range(), [50, radius]);
|
| + }
|
| + else {
|
| + xd = d3.interpolate(xScaler.domain(), [minX, maxX]);
|
| + yd = d3.interpolate(yScaler.domain(), [yDomainMin, yDomainMin + yDomainMin * 40]);
|
| + yr = d3.interpolate(yScaler.range(), [50, radius]);
|
| + }
|
| +
|
| + return function(d, i) {
|
| + return i ? function(t) { return arc(d); }
|
| + : function(t) {
|
| + xScaler.domain(xd(t)); yScaler.domain(yd(t)).range(yr(t)); return arc(d);
|
| + };
|
| + };
|
| + }
|
| + }
|
| +
|
| + initPartition(json);
|
| +
|
| + function getNode(id) {
|
| + for (var i = 0; i < nodes.length; i++) {
|
| + if (nodes[i].id == id)
|
| + return nodes[i];
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + var entityMap = {
|
| + "&": "&",
|
| + "<": "<",
|
| + ">": ">",
|
| + '"': '"',
|
| + "'": ''',
|
| + "/": '/'
|
| + };
|
| +
|
| + function escapeHtml(string) {
|
| + return String(string).replace(/[&<>"'\/]/g, function (s) {
|
| + return entityMap[s];
|
| + });
|
| + }
|
| +
|
| + function zoomto(args) {
|
| + if (!currSelection ||
|
| + !currSelection.stack.equals(args.stack) ||
|
| + !currSelection.ignore.equals(args.ignore) ||
|
| + !currSelection.hide.equals(args.hide) ||
|
| + currSelection.merged != args.merged) {
|
| + // Clear old path
|
| + var path = vis.selectAll("path")
|
| + .data([]);
|
| + path.exit().remove();
|
| + currSelection = args;
|
| + drawLegend();
|
| +
|
| + var mergingDiv = d3.select("#merging");
|
| +
|
| + if (args.stack.length > 1) {
|
| + var newSelectionA = jQuery.extend(true, {}, args);
|
| + var newSelectionB = jQuery.extend(true, {}, args);
|
| + newSelectionA.merged = !newSelectionA.merged;
|
| + newSelectionA.id = 1;
|
| + newSelectionB.merged = false;
|
| + newSelectionB.id = 1;
|
| + newSelectionB.stack = [0];
|
| + mergingDiv.html(
|
| + "<b>Multiple Selection: " + (args.merged ? 'Merged' : 'Filtered') + " View</b><br>" +
|
| + "<a href='#" + escapeHtml(JSON.stringify(newSelectionB)) + "'>Clear Multiple Selection</a><br>" +
|
| + "<a href='#" + escapeHtml(JSON.stringify(newSelectionA)) + "'>Switch to " +
|
| + (args.merged ? 'Filtered' : 'Merged') + ' View</a>');
|
| + }
|
| + else {
|
| + mergingDiv.html('');
|
| + }
|
| +
|
| + // init data
|
| + var rootNode = null;
|
| + if (args.stack.length < 0) {
|
| + rootNode = stackIdToNode[args.stack[0]];
|
| + } else {
|
| + function mergeChild(into, node, inFilter, outFilter) {
|
| + // node is a single node to merge
|
| + // into is an array to merge to
|
| + if (!inFilter)
|
| + inFilter = args.stack.indexOf(node.stackId) >= 0;
|
| +
|
| + // Hide these stacks.
|
| + if (args.hide.indexOf(node.stackId) >= 0)
|
| + outFilter = false;
|
| +
|
| + if (args.ignore.indexOf(node.comp) >= 0)
|
| + return;
|
| + var toNode = undefined;
|
| + for (var i = 0; i < into.length; i++) {
|
| + var child = into[i];
|
| + if (node.name == child.name && node.comp == child.comp) {
|
| + toNode = child;
|
| + break;
|
| + }
|
| + }
|
| + if (!toNode) {
|
| + toNode = {comp: node.comp, name: node.name, stackIds: []};
|
| + into.push(toNode);
|
| + }
|
| +
|
| + toNode.stackIds.push(node.stackId);
|
| +
|
| + if (node.size && args.ignore.indexOf(node.comp) < 0 && inFilter && outFilter) {
|
| + if (!toNode.size)
|
| + toNode.size = node.size;
|
| + else
|
| + toNode.size += node.size;
|
| + }
|
| +
|
| + if (node.children) {
|
| + if (!toNode.children)
|
| + toNode.children = []
|
| + for (var i = 0; i < node.children.length; i++) {
|
| + mergeChild(toNode.children, node.children[i], inFilter, outFilter);
|
| + }
|
| + }
|
| + }
|
| +
|
| + rootNode = {comp: 'root', name: 'root', children: [], stackIds: []};
|
| + var mergeInto;
|
| + if (args.stack.length <= 1 || true) {
|
| + mergeInto = rootNode.children;
|
| + } else {
|
| + rootNode.children.push({
|
| + comp: 'Thread',
|
| + children: [],
|
| + stackIds: args.stack,
|
| + name: '<Multiple Selection: ' + args.stack.length + ' * ' +
|
| + stackIdToNode[args.stack[0]].name + '>'
|
| + });
|
| + mergeInto = rootNode.children[0].children;
|
| + }
|
| +
|
| + if (args.merged) {
|
| + for (var i = 0; i < args.stack.length; i++) {
|
| + mergeChild(mergeInto, stackIdToNode[args.stack[i]], false, true);
|
| + }
|
| + }
|
| + else {
|
| + mergeChild(mergeInto, stackIdToNode[0], false, true);
|
| + }
|
| +
|
| + function fixSelfTimes(node) {
|
| + if (node.children) {
|
| + for (var i = 0; i < node.children.length; i++) {
|
| + if (node.children[i].name == '<self>') {
|
| + if (node.size) {
|
| + node.children[i].size += node.size;
|
| + node.size = undefined;
|
| + }
|
| + } else {
|
| + fixSelfTimes(node.children[i]);
|
| + }
|
| + }
|
| + if (node.size) {
|
| + node.children.push({comp: node.comp, name: '<self>', size: node.size});
|
| + node.size = undefined;
|
| + }
|
| + }
|
| + }
|
| + fixSelfTimes(rootNode);
|
| + }
|
| + initPartition(rootNode);
|
| + }
|
| + currSelection = args;
|
| + click_stack.push(currSelection);
|
| +
|
| + var d = getNode(args.id);
|
| +
|
| + if (d) {
|
| + clickedY = d.y;
|
| + min_x = d.x;
|
| + max_x = d.x + d.dx;
|
| + min_y = d.y;
|
| + }
|
| + else {
|
| + clickedY = -1;
|
| + min_x = 0.0;
|
| + max_x = 1.0;
|
| + min_y = 0.0;
|
| + }
|
| +
|
| + clickedNode = d;
|
| + redraw(min_x, max_x, min_y);
|
| + var path = vis.selectAll("path");
|
| +
|
| + path.transition()
|
| + .duration(750)
|
| + .attrTween("d", arcTween(min_x, max_x, min_y));
|
| +
|
| + showBreadcrumbs(d);
|
| +
|
| +
|
| + /*
|
| + var summary = '';
|
| + summary += '<b>' + d.value.toFixed(3) + 'ms ' + escapeHtml(d.name) + '</b>';
|
| + d.children.forEach(function f(c, i) {
|
| + summary += '<br> > <b>' + c.value.toFixed(3) + 'ms ' + (100 * c.value / d.value).toFixed(3) + '% '
|
| + + '<a href=\"#'+c.id + '\">' + escapeHtml(c.name) + '</a></b>';
|
| + });
|
| + d3.select("#summary")
|
| + .html(summary);
|
| + */
|
| +
|
| + var dataSet = [];
|
| + if (d.children) {
|
| + d.children.forEach(function f(c, i) {
|
| + if (c.value == 0)
|
| + return;
|
| + var newSelection = jQuery.extend(true, {}, currSelection);
|
| + newSelection.id = c.id;
|
| + var hideSelection = jQuery.extend(true, {}, currSelection);
|
| + hideSelection.hide = hideSelection.hide.concat(c.stackIds);
|
| +
|
| + dataSet.push([
|
| + '<a href=\"#'+escapeHtml(JSON.stringify(hideSelection)) + '\">hide</a>',
|
| + c.value.toFixed(3) + 'ms',
|
| + (100 * c.value / d.value).toFixed(3) + '%',
|
| + '<a href=\"#'+escapeHtml(JSON.stringify(newSelection)) + '\">' + escapeHtml(c.name) + '</a>'
|
| + ]);
|
| + });
|
| + } else {
|
| + dataSet.push(['', '', 'No data.']);
|
| + }
|
| +
|
| +
|
| +
|
| + childTable.fnClearTable();
|
| + childTable.fnFilter('');
|
| + childTable.fnAddData(dataSet);
|
| + childTable.fnDraw();
|
| +
|
| + dataSet = [];
|
| + if (d.stackIds[0] > 0) {
|
| + var allFrames = {};
|
| + function procNode(node) {
|
| + var currFrame;
|
| + if (node.id != 0) {
|
| + if (!(node.name in allFrames))
|
| + allFrames[node.name] = {self: 0.0, total: 0.0, stacks: [], mark: 0};
|
| + currFrame = allFrames[node.name];
|
| + if (currFrame.mark == 0) {
|
| + currFrame.total += node.value;
|
| + for (var i = 0; i < node.stackIds.length; i++) {
|
| + currFrame.stacks.push(node.stackIds[i]);
|
| + }
|
| + }
|
| + } else {
|
| + currFrame = {mark: 0};
|
| + }
|
| +
|
| + currFrame.mark++;
|
| + if (node.children) {
|
| + node.children.forEach(function f(c, i) {
|
| + if (c.name == '<self>')
|
| + currFrame.self += c.value;
|
| + else
|
| + procNode(c);
|
| + });
|
| + } else {
|
| + currFrame.self += node.value;
|
| + }
|
| + currFrame.mark--;
|
| + }
|
| + procNode(d);
|
| +
|
| + d3.entries(allFrames).forEach(function (f) {
|
| + if (f.value.total < 0.0005)
|
| + return;
|
| + var mergedSelection = jQuery.extend(true, {}, currSelection);
|
| + mergedSelection.stack = f.value.stacks;
|
| + mergedSelection.id = 0;
|
| + mergedSelection.merged = true;
|
| +
|
| + var hideSelection = jQuery.extend(true, {}, currSelection);
|
| + hideSelection.hide = hideSelection.hide.concat(f.value.stacks);
|
| +
|
| + dataSet.push([
|
| + '<a href=\"#'+escapeHtml(JSON.stringify(hideSelection)) + '\">hide</a>',
|
| + f.value.total.toFixed(3),
|
| + f.value.self.toFixed(3),
|
| + f.value.stacks.length,
|
| + '<a href=\"#'+escapeHtml(JSON.stringify(mergedSelection)) + '\">' + escapeHtml(f.key) + '</a>'
|
| + ]);
|
| + });
|
| + }
|
| + else {
|
| + dataSet.push(['', '', '', '', 'Please select a method or thread.']);
|
| + }
|
| +
|
| + bottomUpTable.fnClearTable();
|
| + bottomUpTable.fnFilter('');
|
| + bottomUpTable.fnAddData(dataSet);
|
| + bottomUpTable.fnDraw();
|
| + }
|
| +
|
| + function click(d) {
|
| + if (d3.event.shiftKey) {
|
| + // Zoom partially onto the selected range
|
| + var diff_x = (max_x - min_x) * 0.5;
|
| + min_x = d.x + d.dx * 0.5 - diff_x * 0.5;
|
| + min_x = min_x < 0.0 ? 0.0 : min_x;
|
| + max_x = min_x + diff_x;
|
| + max_x = max_x > 1.0 ? 1.0 : max_x;
|
| + min_x = max_x - diff_x;
|
| +
|
| + redraw(min_x, max_x, min_y);
|
| +
|
| + var path = vis.selectAll("path");
|
| +
|
| + clickedNode = d;
|
| + path.transition()
|
| + .duration(750)
|
| + .attrTween("d", arcTween(min_x, max_x, min_y));
|
| +
|
| + return;
|
| + }
|
| +
|
| + if (click_stack[click_stack.length-1] != d.id) {
|
| + var newSelection = jQuery.extend(true, {}, currSelection);
|
| + newSelection.id = d.id;
|
| + location.hash = JSON.stringify(newSelection);
|
| + }
|
| + }
|
| +
|
| + // Restore everything to full opacity when moving off the visualization.
|
| + function mouseleave(d) {
|
| + // Hide the breadcrumb trail
|
| + if (clickedNode != null)
|
| + showBreadcrumbs(clickedNode);
|
| + else {
|
| + d3.select("#trail")
|
| + .style("visibility", "hidden");
|
| +
|
| + // Deactivate all segments during transition.
|
| + d3.selectAll("path").on("mouseover", null);
|
| +
|
| + // Transition each segment to full opacity and then reactivate it.
|
| + d3.selectAll("path")
|
| + .transition()
|
| + .duration(300)
|
| + .style("opacity", 1)
|
| + .each("end", function() {
|
| + d3.select(this).on("mouseover", mouseover);
|
| + });
|
| + d3.select("#explanation")
|
| + .transition()
|
| + .duration(300)
|
| + .style("visibility", "hidden");
|
| + }
|
| + }
|
| +
|
| + function redraw(min_x, max_x, min_y) {
|
| + var scale = max_x - min_x;
|
| + var visible_nodes = nodes.filter(function(d) {
|
| + return d.depth &&
|
| + (d.y >= min_y) &&
|
| + (d.x < max_x) &&
|
| + (d.x + d.dx > min_x) &&
|
| + (d.dx / scale > 0.001); // 0.005 radians = 0.29 degrees
|
| + });
|
| + var path = vis.selectAll("path")
|
| + .data(visible_nodes, function(d) { return d.id; });
|
| +
|
| + path.enter().insert("svg:path")
|
| + //.attr("display", function(d) { return d.depth ? null : "none"; })
|
| + .attr("d", arc)
|
| + .attr("fill-rule", "evenodd")
|
| + .style("fill", function(dd) { return colors[dd.comp]; })
|
| + .style("opacity", 0.7)
|
| + .on("mouseover", mouseover)
|
| + .on("click", click);
|
| +
|
| + path.exit().remove();
|
| + return path;
|
| + }
|
| +
|
| + function showBreadcrumbs(d) {
|
| + var scale = max_x - min_x;
|
| + var percentage = (100 * d.dx / scale).toPrecision(3);
|
| + var tot = d.value.toPrecision(3);
|
| + var percentageString = percentage + "%";
|
| + if (percentage < 0.1) {
|
| + percentageString = "< 0.1%";
|
| + }
|
| +
|
| + d3.select("#percentage")
|
| + .text(tot + 'ms ' + percentageString);
|
| +
|
| + d3.select("#explanation")
|
| + .style("visibility", "");
|
| +
|
| + var sequenceArray = getAncestors(d);
|
| + updateBreadcrumbs(sequenceArray, percentageString);
|
| +
|
| + // Fade all the segments.
|
| + d3.selectAll("path")
|
| + .style("opacity", 0.7);
|
| +
|
| + // Then highlight only those that are an ancestor of the current segment.
|
| + vis.selectAll("path")
|
| + .filter(function(node) {
|
| + return (sequenceArray.indexOf(node) >= 0);
|
| + })
|
| + .style("opacity", 1);
|
| + }
|
| + // Fade all but the current sequence, and show it in the breadcrumb trail.
|
| + function mouseover(d) {
|
| + showBreadcrumbs(d);
|
| + }
|
| +
|
| + // Given a node in a partition layout, return an array of all of its ancestor
|
| + // nodes, highest first, but excluding the root.
|
| + function getAncestors(node) {
|
| + var path = [];
|
| + var current = node;
|
| + while (current.parent && path.length < 22) {
|
| + path.unshift(current);
|
| + current = current.parent;
|
| + }
|
| + return path;
|
| + }
|
| +
|
| + function initializeBreadcrumbTrail() {
|
| + // Add the svg area.
|
| + var trail = d3.select("#sequence").append("svg:svg")
|
| + .attr("width", 1200)
|
| + .attr("height", 400)
|
| + .attr("id", "trail");
|
| + // Add the label at the end, for the percentage.
|
| + trail.append("svg:text")
|
| + .attr("id", "endlabel")
|
| + .style("fill", "#000");
|
| + }
|
| +
|
| + // Generate a string that describes the points of a breadcrumb polygon.
|
| + function breadcrumbPoints(d, i) {
|
| + var points = [];
|
| + points.push("0,0");
|
| + points.push(b.w + ",0");
|
| + points.push(b.w + b.t + "," + (b.h / 2));
|
| + points.push(b.w + "," + b.h);
|
| + points.push("0," + b.h);
|
| + //if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
|
| + points.push(b.t + "," + (b.h / 2));
|
| + //}
|
| + return points.join(" ");
|
| + }
|
| +
|
| + // Update the breadcrumb trail to show the current sequence and percentage.
|
| + function updateBreadcrumbs(nodeArray, percentageString) {
|
| +
|
| + // Data join; key function combines name and depth (= position in sequence).
|
| + var g = d3.select("#trail")
|
| + .selectAll("g")
|
| + .data(nodeArray, function(d) { return d.name + d.depth; });
|
| +
|
| + // Add breadcrumb and label for entering nodes.
|
| + var entering = g.enter().append("svg:g");
|
| +
|
| + entering.append("svg:polygon")
|
| + .attr("points", breadcrumbPoints)
|
| + .style("fill", function(d) { return colors[d.comp]; });
|
| +
|
| + entering.append("svg:text")
|
| + .attr("x", 15)
|
| + .attr("y", b.h / 2)
|
| + .attr("dy", "0.35em")
|
| + .attr("text-anchor", "start")
|
| + .text(function(d) { return d.name; });
|
| +
|
| + // Set position for entering and updating nodes.
|
| + g.attr("transform", function(d, i) {
|
| + return "translate(0, " + i * (b.h + b.s) + ")";
|
| + });
|
| +
|
| + // Remove exiting nodes.
|
| + g.exit().remove();
|
| +
|
| + // Make the breadcrumb trail visible, if it's hidden.
|
| + d3.select("#trail")
|
| + .style("visibility", "");
|
| +
|
| + }
|
| +
|
| + function legendClick(d) {
|
| + var newSelection = jQuery.extend(true, {}, currSelection);
|
| + var i = newSelection.ignore.indexOf(d.key);
|
| + if (i >= 0)
|
| + newSelection.ignore.splice(i, 1);
|
| + else
|
| + newSelection.ignore.push(d.key);
|
| + location.hash = JSON.stringify(newSelection);
|
| + }
|
| +
|
| + function drawLegend() {
|
| +
|
| + // Dimensions of legend item: width, height, spacing, radius of rounded rect.
|
| + var li = {
|
| + w: 75, h: 30, s: 3, r: 3
|
| + };
|
| +
|
| + d3.select("#legend").selectAll('svg').remove();
|
| +
|
| + var legend = d3.select("#legend").append("svg:svg")
|
| + .attr("width", li.w)
|
| + .attr("height", d3.keys(colors).length * (li.h + li.s));
|
| +
|
| + var legendGroup = legend.selectAll("g")
|
| + .data(d3.entries(colors))
|
| + .enter().append("svg:g")
|
| + .on("click", legendClick)
|
| + .attr("transform", function(d, i) {
|
| + return "translate(0," + i * (li.h + li.s) + ")";
|
| + });
|
| +
|
| + legendText = legendGroup.append("svg:rect")
|
| + .attr("rx", li.r)
|
| + .attr("ry", li.r)
|
| + .attr("width", li.w)
|
| + .attr("height", li.h)
|
| + .style("fill", function(d) { return d.value; });
|
| +
|
| +
|
| + legendGroup.append("svg:text")
|
| + .attr("x", li.w / 2)
|
| + .attr("y", li.h / 2)
|
| + .attr("dy", "0.35em")
|
| + .attr("text-anchor", "middle")
|
| + .style("text-decoration", function(d) {
|
| + return (currSelection.ignore.indexOf(d.key) >= 0) ? 'line-through' : '';
|
| + })
|
| + .text(function(d) { return d.key; });
|
| + }
|
| +
|
| +
|
| +
|
| + // Client-side routes
|
| + var app = Sammy(function() {
|
| + this.get('#:args', function() {
|
| + var args = jQuery.parseJSON(this.params.args);
|
| + if (!args.stack)
|
| + args.stack = [0];
|
| + if (typeof args.merged === 'undefined')
|
| + args.merged = false;
|
| + if (!args.id)
|
| + args.id = 1;
|
| + if (!args.ignore)
|
| + args.ignore = [];
|
| + if (!args.hide)
|
| + args.hide = [];
|
| +
|
| + clickedNode = getNode(args.id);
|
| + zoomto(args);
|
| + });
|
| +
|
| + this.get('', function() {
|
| + location.hash = JSON.stringify({stack: [0], id: 1, ignore: [], hide: [], merged: false});
|
| + //this.app.runRoute('get', '#' + JSON.stringify({stack: 1, id: 0}))
|
| + });
|
| + }).run();
|
| +
|
| + // Add the mouseleave handler to the bounding circle.
|
| + d3.select("#container")
|
| + .on("mouseleave", mouseleave)
|
| + ;
|
| + };
|
|
|