// Created by Irene Chen (2013) // look at js/pytutor.js for guide on refactoring object methods in JS! function HolisticVisualizer(domRootID, dat, params) { var myViz = this; myViz.domRoot = $('#' + domRootID); myViz.domRootD3 = d3.select('#' + domRootID); myViz.domRoot.html('
\

Step N/A

\
\
Step N/A\
'); myViz.domRoot = myViz.domRoot.find('div.HolisticVisualizer'); myViz.domRootD3 = myViz.domRootD3.select('div.HolisticVisualizer'); var pregeneratedHTML = '
\
\ \ \
\ Connecting line\
\ Debug panel\
\ \
\
\
\
\ \
\
\

Debug: step N/A

\
\
\
'; myViz.domRoot.html(pregeneratedHTML); params.hideCode = true; myViz.altVisualizer = new ExecutionVisualizer('altVisual', dat, params); myViz.tooltipVisualizer = new ExecutionVisualizer('holisticTooltipVisual', dat, params); /* * ================================= * DATASET * ================================= */ var trace = dat; // build dataset from trace var dataset = []; for (var i = 0; i < trace.trace.length; i++) { // we want indexing to start at 0 - offset of -1 dataset.push(trace.trace[i].line - 1); } /* * ================================= * CONSTANTS * ================================= */ var NO_VALUE = '~'; var max_value = Math.max.apply(Math, dataset); var num_rows = max_value + 1; // width and height var padding = 17; var col_w = 26; var col_mid = col_w / 2; var row_h = 24; var row_mid = 12; var w = dataset.length * col_w; var h = num_rows * row_h; var final_h = h + row_h; // track alt visual data var altVisualStep = 0; /* * ================================= * CODE TABLE * ================================= */ var code = trace.code.split('\n'); var table = myViz.domRoot.find('#code'); for (var i = 0; i < code.length; i++) { table.append('' + '
' + code[i] + '
' + '
' + (i+1) + ' 
' + ''); } // adjust width and height of table myViz.domRoot.find('table').attr('height',h); myViz.domRoot.find('td').attr('height',row_h); myViz.domRoot.find('tr').attr('height',row_h); /* * ================================= * VAR-SELECT MENU * ================================= */ // globals var global_group = document.createElement('optgroup'); global_group.setAttribute('label', 'Globals'); var globals = {}; for (var i = 0; i < trace.trace.length; i++) { for (var j = 0; j < trace.trace[i].ordered_globals.length; j++) { var x = trace.trace[i].ordered_globals[j]; if (!(x in globals)) { var option = document.createElement('option'); option.text = option.value = x; global_group.appendChild(option, null); globals[x] = 1; } } } myViz.domRoot.find('#var-select').append(global_group); // functions var functions = {}; for (var i = 0; i < trace.trace.length; i++) { if (trace.trace[i].func_name != "") { var func = trace.trace[i].func_name; if (!(func in functions)) { var function_group = document.createElement('optgroup'); function_group.setAttribute('label', trace.trace[i].func_name); functions[trace.trace[i].func_name] = 1; var function_vars = {}; var n = trace.trace[i].stack_to_render.length; if (n > 0) { // pgbovine - added guard for (var j = 0; j < trace.trace[i].stack_to_render[n-1].ordered_varnames.length; j++) { var x = trace.trace[i].stack_to_render[n-1].ordered_varnames[j]; if (!(x in function_vars) && x != '__return__') { var option = document.createElement('option'); option.text = option.value = x; function_group.appendChild(option, null); function_vars[x] = 1; } } } myViz.domRoot.find('#var-select').append(function_group); } } } /* * ================================= * SVG CONSTRUCTION * ================================= */ // construct code trace svg var d3image = myViz.domRootD3.select("#slider"); var svg = d3image.append("svg:svg") .attr("width", w) .attr("height", final_h); // scales var xScale = d3.scale.linear() .domain([0, dataset.length-1]) .range([col_mid, w - col_mid]); // xScale(i) = (i * col_w) + col_mid var yScale = d3.scale.linear() .domain([0, max_value]) .range([row_mid, h - row_mid]); // yScale(d) = (d * row_h) + row_mid /* * ================================= * BACKGROUND GROUP * ================================= */ // var backgroundGroup = svg.append("svg:g"); // var rows = backgroundGroup.selectAll("rect") // .data(d3.range(num_rows + 1)) // .enter() // .append("svg:rect"); // rows.attr("x", 0) // .attr("y", function(d, i) { // return (d * row_h); // }) // .attr("width", w) // .attr("height", row_h) // .attr("fill", function(d, i) { // if (d % 2 == 0) { // return "#D5F1F1"; // } else { // return "white"; // } // }); /* * ================================= * TEXT GROUP * ================================= */ var textGroup = svg.append("svg:g"); var values = textGroup.selectAll("text") .data(dataset) .enter() .append("text"); values.text( function(d, i) { var val = myViz.domRoot.find('#var-select').val(); if (val == 'default') { return NO_VALUE; } if (myViz.domRoot.find('#var-select').parent().attr('label') == 'globals') { if (val in trace.trace[i].globals) { return trace.trace[i].globals[val]; } else { return NO_VALUE; } } else { var n = trace.trace[i].stack_to_render.length; if (n > 0 && val in trace.trace[i].stack_to_render[n-1].encoded_locals && myViz.domRoot.find('#var-select').parent().attr('label') == trace.trace[i].stack_to_render[n-1].func_name) { return trace.trace[i].stack_to_render[n-1].encoded_locals[val]; } else { return NO_VALUE; } } }) .attr("x", function(d, i) { return xScale(i) - 4; }) .attr("y", function(d, i) { return yScale(d) + 4; }) .style("visibility", "hidden"); /* * ================================= * CONNECTOR GROUP * ================================= */ var connectorGroup = svg.append("svg:g"); // create a line function that can convert data[] into x and y points var line = d3.svg.line() .x(function(d,i) { return xScale(d[0]); }) .y(function(d) { return yScale(d[1]); }) .interpolate('linear'); // TODO: this doesn't interpolate the way i want - let's do it manually instead var curvedLine = d3.svg.line() .x(function(d,i) { return xScale(d[0]); }) .y(function(d) { return yScale(d[1]); }) .interpolate('basis'); // tweak path data to include curves var straightData = []; var callData = []; var callTextData = []; var returnData = []; var specialSegmentsData = []; for (var i = 0; i < dataset.length; i++) { if (trace.trace[i].event == 'call') { if ((i-1) > 0) { specialSegmentsData.push([i-1, i, dataset[i], 'call']); } } else if (trace.trace[i].event == 'return') { if ((i+1) < dataset.length) { specialSegmentsData.push([i, i+1, dataset[i], 'return']); } } } specialSegmentsData.push([dataset.length, dataset.length, dataset[-1], 'end']); for (var i = 0; i < specialSegmentsData.length; i++) { if (i > 0) { var start = specialSegmentsData[i-1][1]; } else { var start = 0; } // need to include original indices var slice = dataset.slice(start,specialSegmentsData[i][1]); var sliceData = []; for (var j = 0; j < slice.length; j++) { sliceData.push([start + j, slice[j]]); } // push to pathData straightData.push(line(sliceData)); if (specialSegmentsData[i][3] == 'call') { var headIndex = specialSegmentsData[i][0]; var tailIndex = specialSegmentsData[i][1]; var mid = headIndex; var magic = (0.3*dataset[headIndex] + 0.7*dataset[tailIndex]); var arrowData = [[headIndex, dataset[headIndex]], [mid, magic], [tailIndex, dataset[tailIndex]]]; var arrowPathData = "M"+xScale(headIndex)+","+yScale(dataset[headIndex])+ " Q"+xScale(mid)+","+yScale(magic)+" "+xScale(tailIndex)+","+yScale(dataset[tailIndex]); callData.push(arrowPathData); callTextData.push([headIndex, dataset[headIndex], tailIndex, dataset[tailIndex]]); } else if (specialSegmentsData[i][3] == 'return') { var headIndex = specialSegmentsData[i][0]; var tailIndex = specialSegmentsData[i][1]; var mid = tailIndex; var magic = (0.3*dataset[headIndex] + 0.7*dataset[tailIndex]); var arrowData = [[headIndex, dataset[headIndex]], [mid, magic], [tailIndex, dataset[tailIndex]]]; var arrowPathData = "M"+xScale(headIndex)+","+yScale(dataset[headIndex])+ " Q"+xScale(mid)+","+yScale(magic)+" "+xScale(tailIndex)+","+yScale(dataset[tailIndex]); returnData.push(arrowPathData); } } // arrow head definition svg.append("defs").append("marker") .attr("id", "call-arrowhead") .attr("refX", 6 + 3) /*must be smarter way to calculate shift*/ .attr("refY", 4) .attr("markerWidth", 10) .attr("markerHeight", 10) .attr("orient", "auto") .append("path") .attr("d", "M 0,0 V 8 L6,4 Z") .style("stroke", "red") .style("fill", "red"); //this is actual shape for arrowhead svg.append("defs").append("marker") .attr("id", "return-arrowhead") .attr("refX", 6 + 3) /*must be smarter way to calculate shift*/ .attr("refY", 4) .attr("markerWidth", 10) .attr("markerHeight", 10) .attr("orient", "auto") .append("path") .attr("d", "M 0,0 V 8 L6,4 Z") .style("stroke", "lime") .style("fill", "lime"); //this is actual shape for arrowhead var straightConnector = connectorGroup.selectAll(".straight") .data(straightData) .enter() .append("svg:path") .attr("d", function (d) { return d; }) .style("visibility", "visible"); var callConnector = connectorGroup.selectAll(".call") .data(callData) .enter() .append("svg:path") .attr("d", function (d) { return d; }) .style("stroke", "red") .style("visibility", "visible") .attr("marker-end", "url(#call-arrowhead)"); var returnConnector = connectorGroup.selectAll(".return") .data(returnData) .enter() .append("svg:path") .attr("d", function (d) { return d; }) .style("stroke", "lime") .style("visibility", "visible") .attr("marker-end", "url(#return-arrowhead)"); var functionText = connectorGroup.selectAll("text") .data(callTextData) .enter() .append("text"); functionText.text(function(d, i) { return trace.trace[d[2]].func_name; }) .attr("x", function(d, i) { return xScale(d[0]) + 10; }) .attr("y", function(d, i) { var mid = 0.5 * (d[1]+d[3]); return yScale(mid) + 4; }) .style("visibility", "visible") .style("font-family", "monospace") .style("font-size", "10pt"); /* * ================================= * DEFAULT GROUP * ================================= */ var defaultGroup = svg.append("svg:g"); var circles = defaultGroup.selectAll("circle") .data(dataset) .enter() .append("circle"); circles.attr("cx", function(d, i) { return xScale(i); }) .attr("cy", function(d, i) { return yScale(d); }) .attr("r", "4") .attr("fill", function(d, i) { return 'black'; }) .style("visibility", "visible"); /* * ================================= * DELIMITER GROUP * ================================= */ // build function call dataset (need to grab returns as well!) var functionCallDataset = []; for (var i = 0; i < dataset.length; i++) { if (trace.trace[i].event == 'call') { functionCallDataset.push([i, dataset[i], 'call']); } else if (trace.trace[i].event == 'return') { functionCallDataset.push([i, dataset[i], 'return']); } } var delimitingGroup = svg.append("svg:g"); var delimiters = delimitingGroup.selectAll("line") .data(functionCallDataset) .enter() .append("line"); delimiters.attr("x1", function(d, i) { if (d[2] == 'call') { return xScale(d[0]) - col_mid; } else { return xScale(d[0]) + col_mid; } }) .attr("y1", function(d, i) { return 0; }) .attr("x2", function(d, i) { if (d[2] == 'call') { return xScale(d[0]) - col_mid; } else { return xScale(d[0]) + col_mid; } }) .attr("y2", function(d, i) { return h; }) .style("visibility", "hidden") .classed("delimiter", true); /* * ================================= * GUIDE GROUP * ================================= */ var guideGroup = svg.append("svg:g"); var v_guides = guideGroup.selectAll("line") .data(dataset) .enter() .append("line"); v_guides.attr("x1", function(d, i) { return xScale(i); }) .attr("y1", function(d, i) { return 0; }) .attr("x2", function(d, i) { return xScale(i); }) .attr("y2", function(d, i) { return h; }) .style("visibility", "hidden") .classed("v-guide", true); var v_hovers = guideGroup.selectAll("rect") .data(dataset) .enter() .append("rect"); // save "top" position for debug panel var topPosition = myViz.domRoot.find('#debug').scrollTop(); v_hovers.attr("x", function(d, i) { return (i * col_w); }) .attr("y", function(d, i) { return 0; }) .attr("width", function(d, i) { return col_w; }) .attr("height", function(d, i) { return h; }) .classed("v-hover", true) .on("mouseover", function (d, i) { var row = table[0].rows[d]; var cell = row.cells[0]; myViz.domRoot.find('#code tr:eq(' + d + ') td:first').addClass("v-hover"); myViz.domRoot.find('#code tr:eq(' + d + ') td:last').addClass("v-hover"); v_guides.filter( function(data, index) { return index == i; }) .style("visibility", "visible"); circles.filter( function(data, index) { return index == i; }); var svgOffset = myViz.domRoot.find('div#slider svg').offset(); // hide old tooltip hideTooltip(); if (d3.mouse(this)[1] > (yScale(d)-row_mid) && d3.mouse(this)[1] < (yScale(d+1)-row_mid) ) { $('#tooltip-step-id').text(i); $('#holisticTooltip').show(); myViz.tooltipVisualizer.renderStep(i); changeTooltipPosition(xScale(i) + svgOffset.left, yScale(d) + svgOffset.top); } }) .on("mousemove", function (d, i) { var row = table[0].rows[d]; var cell = row.cells[0]; myViz.domRoot.find('#code tr:eq(' + d + ') td:first').addClass("v-hover"); myViz.domRoot.find('#code tr:eq(' + d + ') td:last').addClass("v-hover"); v_guides.filter( function(data, index) { return index == i; }) .style("visibility", "visible"); var svgOffset = myViz.domRoot.find('div#slider svg').offset(); // hide old tooltip hideTooltip(); if (d3.mouse(this)[1] > (yScale(d)-row_mid) && d3.mouse(this)[1] < (yScale(d+1)-row_mid) ) { $('#tooltip-step-id').text(i); $('#holisticTooltip').show(); myViz.tooltipVisualizer.renderStep(i); changeTooltipPosition(xScale(i) + svgOffset.left, yScale(d) + svgOffset.top); } }) .on("mouseout", function (d, i) { var row = table[0].rows[d]; var cell = row.cells[0]; myViz.domRoot.find('#code tr:eq(' + d + ') td:first').removeClass("v-hover"); myViz.domRoot.find('#code tr:eq(' + d + ') td:last').removeClass("v-hover"); v_guides.filter( function(data, index) { return index == i; }) .style("visibility", "hidden"); circles.filter( function(data, index) { return index == i; }); hideTooltip(); }) .on("click", function (d, i) { // debug panel myViz.domRoot.find('#debug-title').text(i); myViz.domRoot.find('#debug').html("
"+JSON.stringify(trace.trace[i], undefined, 2)+"
"); myViz.domRoot.find('#debug').scrollTop(topPosition); if (d3.mouse(this)[1] > (yScale(d)-row_mid) && d3.mouse(this)[1] < (yScale(d+1)-row_mid) ) { // ExecutionVisualizer panel // use $('#altVisual').is(":visible") to check if visible if ($('#altContainer').is(":visible")) { if (altVisualStep == i) { $('#altContainer').hide(); } else { $('#step-id').text(i); altVisualStep = i; myViz.altVisualizer.renderStep(altVisualStep); } } else { $('#step-id').text(i); altVisualStep = i; // have to draw arrows after the div is shown! $('#altContainer').show(); myViz.altVisualizer.renderStep(altVisualStep); } } }); // tooltip helper function var changeTooltipPosition = function(x, y) { var tooltipX = x - 8; var tooltipY = y + 8; var tooltipWidth = $('#holisticTooltip').width(); // TODO: adjust for tooltip height if ((tooltipX + tooltipWidth) > $(window).width()) { $('#holisticTooltip').css({top: tooltipY, left: tooltipX - tooltipWidth}); } else { $('#holisticTooltip').css({top: tooltipY, left: tooltipX}); } }; var hideTooltip = function() { $('#holisticTooltip').hide(); }; /* * ================================= * AXIS GROUP * ================================= */ var axisGroup = svg.append("svg:g"); var xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .ticks(dataset.length); axisGroup.attr("class", "axis") .attr("transform", "translate(0," + (final_h - padding) + ")") .call(xAxis); /* * ================================= * EVENT HANDLERS * ================================= */ myViz.domRoot.find("#var-select").change(function(e) { if (myViz.domRoot.find("#var-select").val() !== 'default') { circles.style("visibility", "hidden"); v_hovers.style("visibility", "hidden"); values.style("visibility", "visible"); values.text(function(d, i) { var val = myViz.domRoot.find('#var-select').val(); if (val == 'default') { return "~"; } if (myViz.domRoot.find(':selected').parent().attr('label') == 'Globals') { if (val in trace.trace[i].globals) { return trace.trace[i].globals[val]; } else { return "~"; } } else { var n = trace.trace[i].stack_to_render.length; if (n > 0 && val in trace.trace[i].stack_to_render[n-1].encoded_locals && myViz.domRoot.find(':selected').parent().attr('label') == trace.trace[i].stack_to_render[n-1].func_name) { return trace.trace[i].stack_to_render[n-1].encoded_locals[val]; } else { return "~"; } } }); } else { circles.style("visibility", "visible"); v_hovers.style("visibility", "visible"); values.style("visibility", "hidden"); } }); myViz.domRoot.find('#connector-select').click(function() { if($(this).is(':checked')){ connector.style("visibility", "visible"); } else { connector.style("visibility", "hidden"); } }); myViz.domRoot.find('#debugPanel').hide(); myViz.domRoot.find('#debug-select').click(function() { if($(this).is(':checked')){ myViz.domRoot.find('#debugPanel').show(); } else { myViz.domRoot.find('#debugPanel').hide(); } }); myViz.domRoot.find('#delimiter-select').click(function() { if($(this).is(':checked')){ delimiters.style("visibility", "visible"); } else { delimiters.style("visibility", "hidden"); } }); } // stubs for unimplemented interface methods HolisticVisualizer.prototype.updateOutput = function() {}; HolisticVisualizer.prototype.redrawConnectors = function() { this.altVisualizer.redrawConnectors(); }; HolisticVisualizer.prototype.destroyAllAnnotationBubbles = function() {};