// This contains the code that renders and controls the visualization. var get_sunburst_render_params = function get_sunburst_render_params() { // 80% of the smallest window dimension var width = 0.8 * Math.min(window.innerHeight, window.innerWidth); var height = width; var radius = width / 2; var partition = d3.layout.partition() .size([2 * Math.PI, radius * radius]) .value(function(d) { return d.time; }); // By default D3 makes the y size proportional to some area, // so y is a transformation from ~area to a linear scale // so that all arcs have the same radial size. var y = d3.scale.linear().domain([0, radius * radius]).range([0, radius]); var arc = d3.svg.arc() .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, d.x)); }) .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, d.x + d.dx)); }) .innerRadius(function(d) { return y(d.y); }) .outerRadius(function(d) { return y(d.y + d.dy); }); return { "width": width, "height": height, "radius": radius, "transform": "translate(" + radius + "," + radius + ")", "partition": partition, "arc": arc }; }; var get_icicle_render_params = function get_icicle_render_params() { var width = window.innerWidth * 0.75; var height = window.innerHeight * 0.8; var leftMargin = 90; var topMargin = 60; var partition = d3.layout.partition() .size([width - leftMargin, height - topMargin]) .value(function(d) { return d.time; }); return { "width": width, "height": height, "leftMargin": leftMargin, "topMargin": topMargin, "transform": "translate(" + leftMargin + "," + topMargin + ")", "partition": partition }; }; var get_render_params = function get_render_params(style) { if (style === "sunburst") { return get_sunburst_render_params(); } else if (style === "icicle") { return get_icicle_render_params(); } else { throw new Error("Unknown rendering style '" + style + "'."); } }; // Colors. var scale = d3.scale.category20c(); // should make it so that a given function is always the same color var color = function color(d) { return scale(d.name); }; var make_vis_obj = function make_vis_obj (style) { var params = get_render_params(style); return d3.select("#chart") .style('margin-left', 'auto') .style('margin-right', 'auto') .append("svg:svg") .attr("width", params["width"]) .attr("height", params["height"]) .append("svg:g") .attr("id", "container") .attr("transform", params["transform"]); }; var vis = make_vis_obj("sunburst"); var reset_vis = function reset_vis (style) { // Remove the current figure d3.select('svg').remove(); // Make and draw the new svg container vis = make_vis_obj(style); }; // This is the function that runs whenever the user clicks on an SVG // element to trigger zooming. var click = function click(d) { // check whether we need to do anything // (e.g. that the user hasn't clicked on the original root node) if (d.name === sv_root_func_name) { return; } var stack_last = _.last(sv_call_stack); if (d.name === stack_last) { // need to go up a level in the call stack sv_call_stack.pop(); var new_root = _.last(sv_call_stack); } else { var new_root = d.name; // need to construct a new call stack // go up the tree until we hit the tip of the call stack var this_node = d; var local_stack = [new_root]; while (this_node.parent != null) { if (this_node.parent.name === stack_last) { // extend the call stack with what we've accumulated local_stack.reverse(); sv_call_stack = sv_call_stack.concat(local_stack); break; } else { local_stack.push(this_node.parent.name); this_node = this_node.parent; } } } //figure out the new parent name if (sv_call_stack.length === 1) { var new_parent_name = null; } else { var new_parent_name = _.first(_.last(sv_call_stack, 2)); } // Create new JSON for drawing a vis from a new root sv_draw_vis(new_root, new_parent_name); sv_update_call_stack_list(); // Activate the reset button if we aren't already at the root node // And deactivate it if this is the root node if (new_root !== sv_root_func_name) { $('#resetbutton-zoom').prop('disabled', false); } else { $('#resetbutton-zoom').prop('disabled', true); } }; var sv_info_tpl = _.template( ['
Name:
', '
<%- name %>
', '
Cumulative Time:
', '
<%= cumulative %> s (<%= cumulative_percent %> %)
', '
File:
', '
<%- file %>
', '
Line:
', '
<%= line %>
', '
Directory:
', '
<%- directory %>
' ].join('\n')); var sv_update_info_div = function sv_update_info_div (d) { var re = /^(.*):(\d+)\((.*)\)$/; var result = re.exec(d.name); var file = result[1]; var directory = ''; var slash = file.lastIndexOf('/'); if (slash !== -1) { directory = file.slice(0, slash + 1); file = file.slice(slash + 1); } var info = { 'file': file, 'directory': directory, 'line': result[2], 'name': result[3], 'cumulative': d.cumulative.toPrecision(3), 'cumulative_percent': (d.cumulative / sv_total_time * 100).toFixed(2) }; var style = $('#sv-style-select').val(); var div = $('#sv-info-div'); div.html(sv_info_tpl(info)); var radius = get_sunburst_render_params()["radius"]; if ((style === "sunburst") & (!div.hasClass('sunburst'))) { div .addClass('sunburst') .removeClass('icicle') .height(radius * 1.5) .width(($('body').width() - (2 * radius)) / 2.1); } else if ((style === "icicle") & (!div.hasClass('icicle'))) { div .addClass('icicle') .removeClass('sunburst') .height(radius * 1.5) .width(200); } }; var apply_mouseover = function apply_mouseover (selection) { selection.on('mouseover', function (d, i) { // select all the nodes that represent this exact function // and highlight them by darkening their color var thisname = d.name; var thispath = selection.filter(function(d, i) { return d.name === thisname; }); var thiscolor = d3.rgb('#ff00ff'); thispath.style('fill', thiscolor.toString()); sv_update_info_div(d); sv_show_info_div(); }) .on('mouseout', function(d, i){ // reset nodes to their original color var thisname = d.name; var thispath = selection.filter(function(d, i) { return d.name === thisname;}); thispath.style('fill', color(d)); }); }; // This is having D3 do its thing. var drawSunburst = function drawSunburst(json) { var params = get_render_params("sunburst"); // For efficiency, filter nodes to keep only those large enough to see. var nodes = params["partition"].nodes(json).filter(function(d) { return (d.dx > 0.005); // 0.005 radians = 0.29 degrees. }); // Bounding circle underneath the sunburst, to make it easier to detect // when the mouse leaves the parent g. vis.append("svg:circle") .attr("r", params["radius"]) .style("opacity", 0); var path = vis.data([json]).selectAll("path") .data(nodes) .enter().append("svg:path") .attr("id", function(d, i) { return "path-" + i; }) .attr("d", params["arc"]) .attr("fill-rule", "evenodd") .style("fill", color) .style("stroke", "#fff") .on('click', click) .call(apply_mouseover); }; var drawIcicle = function drawIcicle(json) { params = get_render_params("icicle"); var nodes = params["partition"].nodes(json).filter(function(d) { return (d.dx > 0.5); // at least half-a-pixel wide to be visible. }); var x = d3.scale.linear() .domain([0, nodes[0].dx]) .range([0, params["width"] - params["leftMargin"]]); var y = d3.scale.linear() .domain([0, nodes[0].dy * $('#sv-depth-select').val()]) .range([0, params["height"] - params["topMargin"]]); var rect = vis.data([json]).selectAll("rect") .data(nodes) .enter().append("rect") .attr("id", function(d, i) { return "path-" + i; }) .attr("x", function(d) { return x(d.x); }) .attr("y", function(d) { return y(d.y); }) .attr("width", function(d) { return x(d.dx); }) .attr("height", function(d) { return y(d.dy); }) .attr("fill-rule", "evenodd") .attr("fill", color) .attr("stroke", "#FFF") .on('click', click) .call(apply_mouseover); var labels = vis.data([json]).selectAll("text") .data(nodes) .enter().append("text") .attr("x", function(d) { return x(d.x + (d.dx / 2.0)); }) .attr("y", function(d) { return y(d.y + (d.dy / 2.0)); }) .attr("width", function(d) { return x(d.dx); }) .attr("height", function(d) { return y(d.dy); }) .attr("font-family", "sans-serif") .attr("font-size", "15px") .attr("fill", "black") .attr("text-anchor", "middle") .attr("pointer-events", "none"); // Append the function name labels.append("tspan") .text(function(d) { return d.display_name; }) .attr("text-anchor", "middle") .attr("x", function(d) { return x(d.x + (d.dx / 2.0)); }); // Append the time labels.append("tspan") .text(function(d) { return d.cumulative.toPrecision(3) + " s"; }) .attr("text-anchor", "middle") .attr("x", function(d) { return x(d.x + (d.dx / 2.0)); }) .attr("dy", "1.2em"); // Remove labels that don't fit d3.selectAll("text") .each(function(d, a, b) { // var text = d3.selectd(this); var bbox = this.getBBox(); if (bbox.width > x(d.dx)) { this.remove(); } }); }; // Clear and redraw the visualization var redraw_vis = function redraw_vis(json) { var style = $('#sv-style-select').val(); reset_vis(style); if (style === "sunburst") { drawSunburst(json); } else if (style === "icicle") { drawIcicle(json); } d3.select('#container') .on('mouseenter', sv_show_info_div) .on('mouseleave', sv_hide_info_div); }; // Reset the visualization to its original state starting from the // main root function. var resetVis = function resetViz() { sv_draw_vis(sv_root_func_name); // Reset the call stack sv_call_stack = [sv_root_func_name]; sv_update_call_stack_list(); $('#resetbutton-zoom').prop('disabled', true); }; $('#resetbutton-zoom').on('click', resetVis); var resetRoot = function resetRoot() { // originally set in the setup code in viz.html sv_root_func_name = sv_root_func_name__cached; resetVis(); $('#resetbutton-root').prop('disabled', true); }; $('#resetbutton-root').on('click', resetRoot); // The handler for when the user changes the depth selection dropdown. var sv_selects_changed = function sv_selects_changed() { sv_cycle_worker(); var parent_name = null; if (sv_call_stack.length > 1) { parent_name = sv_call_stack[sv_call_stack.length - 2]; } sv_hide_error_msg(); sv_draw_vis(_.last(sv_call_stack), parent_name); }; d3.select('#sv-style-select').on('change', sv_selects_changed); d3.select('#sv-depth-select').on('change', sv_selects_changed); d3.select('#sv-cutoff-select').on('change', sv_selects_changed);