From afa1440a471c6028e89d56f134663c484052642e Mon Sep 17 00:00:00 2001 From: hmir Date: Mon, 8 Aug 2016 14:52:09 -0400 Subject: [PATCH 1/6] call graph is generated based on branch navigation changes, clicking on blue circles allow nodes to persist in call graph viewport --- src/visualization/call-graph.js | 674 ++++++++++++----------------- src/visualization/vertex.js | 15 - src/visualization/visualization.js | 9 +- 3 files changed, 279 insertions(+), 419 deletions(-) delete mode 100644 src/visualization/vertex.js diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index c06949bb..a91e3664 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -38,428 +38,296 @@ export class CallGraph { errorMessage: null }; + this.rootNode = { + name: "Program", + id: -1, + parentId: null, + ranges: [], + children: [], + isCallback: false, + pinned: true + } + + this.rootCopy = { + name: "Program", + id: -1, + parentId: null, + ranges: [], + children: [], + isCallback: false, + pinned: true + }; } - prepareFx() - { + prepareFx() { let self = this ; - this.formatTraceFx = function makeTree(trace = this.trace) - { - if(!trace) - return ; - //the text starts at line 0 by default, plus one to match natural line numbers - let map = {} ; - let funcs = [] ; - // - funcs = self.findFuncs( trace ) ; - - for( let i = 0 ; i < funcs.length ; i++ ) - map[ funcs[i].name ] = funcs[ i ] ; //try with adjacency list - - map = self.makeMatrixList( funcs , map ) ; - // - let rootsList = [] ; //list of functions that arent called - for( let key in map ) - { - //console.log( key ) ; - if( map[ key ].parents.length === 0 ) - rootsList.push( map[ key ] ) ; - } - // - // console.log( "funcs" ) ; - // console.log( funcs ) ; - // console.log( "map" ) ; - // console.log( map ) ; - - // console.log( map[ "alpha()" ].children[ 1 ] === map[ "alpha()" ].children[ 2 ] ) ; - //turns matrix into tree - - let masterHead = new Vertex("Program", "Program"); - - // rootsList.map(function(e) { - // masterHead.children.push(e); - // }) - - // return rootsList[ 0 ] ; - return rootsList[0]; - }; - - this.renderFx = function renderFx(formattedTrace, divElement, query, queryType, aceUtils, aceMarkerManager) { - if (!formattedTrace){ - return; - } + this.formatTraceFx = function() { return null; } - if(query !== null && (query == undefined || query.trim() === "")) { - query = null; - } + this.renderFx = function renderFx(formattedTrace, divElement, branches, query, queryType, aceUtils, aceMarkerManager) { + if (!branches || branches.length === 0) { + return; + } - function scrubLeaves(root, hasLeaves=0) { - if(root === undefined || root.children === undefined) { - return hasLeaves; - } + self.rootNode = JSON.parse(JSON.stringify(self.rootCopy)); - let children = root.children; + self.removeUnpinned(); + self.addToGraph(self.createBranchHierarchy(branches)); - for(let i = 0; i < children.length; i++) { - if(children[i].children.length === 0 && !children[i].name.includes(query)) { - hasLeaves++; - root.children.splice(i, 1); - } - hasLeaves += scrubLeaves(children[i], hasLeaves); - } + self.rootCopy = JSON.parse(JSON.stringify(self.root)); + + if(query == undefined || query.trim() === "") { + query = null; + } + + function scrubLeaves(root, hasLeaves=0) { + if(root === undefined || root.children === undefined) { return hasLeaves; } - function scrubTree(root) { - while(scrubLeaves(root)) { + let children = root.children; - }; + for(let i = 0; i < children.length; i++) { + if(children[i].children.length === 0 && !children[i].name.includes(query)) { + hasLeaves++; + root.children.splice(i, 1); + } + hasLeaves += scrubLeaves(children[i], hasLeaves); } + return hasLeaves; + } - function makeQuery() { - scrubTree(formattedTrace); - } + function scrubTree(root) { + while(scrubLeaves(root)); + } + + function makeQuery() { + scrubTree(root); + } + + if(query !== null && queryType === "functions") { + makeQuery(); + } + + d3.select(divElement).html(""); + + let margin = {top: 20, right: 20, bottom: 30, left: 40}, + width = 400 - margin.left - margin.right, + height = 250 - margin.top - margin.bottom; + + let rectWidth = 100, + rectHeight = 40; + + let tree = d3.tree() + .nodeSize([160, 200]); + + let diagonal = self.directionManager[self.currentDirection].linkRenderer; + + let nodeRenderer = self.directionManager[self.currentDirection].nodeRenderer; + + d3.select(divElement).select("svg").remove(); + + let svg = d3.select(divElement).append("svg") + .attr("width", width) + .attr("height", height) + .attr("position","relative") + .call(d3.zoom() + .on("zoom", function () { + svg.attr("transform", function() { + let d3event = d3.event.transform; + return "translate(" + d3event.x + ", " + d3event.y + ") scale(" + d3event.k +")"; + }); + })) + .append("g"); + + svg.attr("transform","translate(150,0)"); + + let root = d3.hierarchy(branches), + nodes = root.descendants(), + links = root.descendants().slice(1); + + tree(root); + let link = svg.selectAll(".link") + .data(links) + .enter() + .append("g") + .attr("class", "link"); + + link.append("line") + .attr("x1", function(d) { return d.parent.x; }) + .attr("y1", function(d) { return !d.parent.data.name.includes(query) ? d.parent.y + rectHeight/2 : d.parent.y + rectHeight; }) + .attr("x2", function(d) { return d.x; }) + .attr("y2", function(d) { return !d.data.name.includes(query) ? d.y + rectHeight/2 : d.y; }) + .style("fill","none") + .style("stroke","#ccc") + .style("stroke-width","1.5px"); + + function showHoverText(d) { + d3.select(this).append("text") + .attr("class", "hover") + .attr("transform", function(d) { + return "translate(5, -5)"; + }) + .text(d.data.name); + highlight(d); + } + + function hideHoverText(d) { + d3.select(this).select("text.hover").remove(); + unhighlight(d); + } + + function highlight(d) { + aceUtils.updateAceMarkers(aceMarkerManager, d.data.ranges); + } + + function unhighlight(d) { + aceUtils.updateAceMarkers(aceMarkerManager, []); + } - if(query !== null && queryType === "functions") { - makeQuery(); + let node = svg.selectAll(".node") + .data(nodes) + .enter().append("g"); + + node.attr("class", "node") + .attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); }) + .attr("transform", nodeRenderer) + .style("font","10px sans-serif"); + + let matchedNodes = node.filter(function(d, i) { + if(queryType === "functions") { + return query === null || d.data.name.includes(query) || i === 0; + } + else { + return true; // TODO support other query types } + }); + + matchedNodes.append("rect") + .attr("width", rectWidth) + .attr("height", rectHeight) + .attr("transform", "translate(" + (-1 * rectWidth/2) + ",0)") + .style("fill","#fff") + .style("stroke","steelblue") + .style("stroke-width","1.5px"); + + let regNodes = node.filter(function(d, i) { + if(queryType === "functions") { + return query !== null && i !== 0 && !d.data.name.includes(query); + } + else { + return false; // TODO support other query types + } + }); + + regNodes.on("mouseover", showHoverText) + .on("mouseout", hideHoverText); + + matchedNodes.on("mouseover",highlight) + .on("mouseout", unhighlight); + + regNodes.append("circle") + .attr("r", 6) + .attr("transform", "translate(0," + rectHeight/2 + ")"); + + matchedNodes.append("text") + .attr("dy", 22.5) + .attr("text-anchor", "middle") + .text(function(d) { return d.data.name; }); + + function updatePins() { + matchedNodes.selectAll("circle").remove(); + matchedNodes.filter(function(d) { + return d.data.pinned === false; + }) + .append("circle") + .attr("r", 3) + .on("click", function(d) { + self.togglePinOnBranch(d.data); + updatePins(); + }) + .style("stroke-width", 2) + .style("color", "white"); + + matchedNodes.filter(function(d) { + return d.data.pinned === true; + }) + .append("circle") + .attr("r", 3) + .on("click", function(d) { + self.togglePinOnBranch(d.data); + updatePins(); + }) + .style("stroke-width", 2) + .style("color", "blue"); + } + + updatePins(); + + } + + d3.select(self.frameElement).style("height", 200 + "px"); - d3.select(divElement).html(""); - - let margin = {top: 20, right: 20, bottom: 30, left: 40}, - width = 400 - margin.left - margin.right, - height = 250 - margin.top - margin.bottom; - - let rectWidth = 100, - rectHeight = 40; - - let tree = d3.tree() - .nodeSize([160, 200]); - - let diagonal = self.directionManager[self.currentDirection].linkRenderer; - - let nodeRenderer = self.directionManager[self.currentDirection].nodeRenderer; - - d3.select(divElement).select("svg").remove(); - - let svg = d3.select(divElement).append("svg") - .attr("width", width) - .attr("height", height) - .attr("position","relative") - .call(d3.zoom() - .on("zoom", function () { - svg.attr("transform", function() { - let devent = d3.event.transform; - return "translate(" + devent.x + ", " + devent.y + ") scale(" + devent.k +")"; - }); - })) - .append("g"); - - svg.attr("transform","translate(100,0)"); - - let root = d3.hierarchy(formattedTrace), - nodes = root.descendants(), - links = root.descendants().slice(1); - - tree(root); - let link = svg.selectAll(".link") - .data(links) - .enter() - .append("g") - .attr("class", "link"); - - link.append("line") - .attr("x1", function(d) { return d.parent.x; }) - .attr("y1", function(d) { return !d.parent.data.name.includes(query) ? d.parent.y + rectHeight/2 : d.parent.y + rectHeight; }) - .attr("x2", function(d) { return d.x; }) - .attr("y2", function(d) { return !d.data.name.includes(query) ? d.y + rectHeight/2 : d.y; }) - .style("fill","none") - .style("stroke","#ccc") - .style("stroke-width","1.5px"); - - // link.append("text") - // .attr("class","num_text") - // .attr("x", function(d) { - // return (d.x + d.parent.x)/2; - // }) - // .attr("y", function(d) { - // return (d.y + d.parent.y + rectHeight)/2; - // }) - // .attr("text-anchor", "middle") - // .text(function (d) { - // return d.data.childCalls;//Math.floor((Math.random() * 10) + 1); - // }) - // .style("font","10px sans-serif"); - - function showHoverText(d) { - d3.select(this).append("text") - .attr("class", "hover") - .attr("transform", function(d) { - return "translate(5, -5)"; - }) - .text(d.data.name); - highlight(d); - } - - function hideHoverText(d) { - d3.select(this).select("text.hover").remove(); - unhighlight(d); - } - - function highlight(d) { - aceUtils.updateAceMarkers(aceMarkerManager, d.data.ranges); - } - - function unhighlight(d) { - aceUtils.updateAceMarkers(aceMarkerManager, []); - } - - // console.log('nodes') - // console.log(node) - let node = svg.selectAll(".node") - .data(nodes) - .enter().append("g"); - - let multiParents = node.filter(function (d, i) { - return d.data.parents.length > 1; - }); - - let parentPairs = []; - - multiParents.each(function(d) { - for(let i = 1; i < d.data.parents.length; i++) { - let p; - node.filter(function (d2, i2) { return d2.data.id === d.data.parents[i].id; }).each(function(pNode) { - p = pNode; - }) - parentPairs.push({ - parent: p, - child: d - }); - } - }); - - parentPairs.forEach(function(multiPair) { - link.append("line") - .attr("class", "additionalParentLink") - .attr("x1", multiPair.parent.x) - .attr("y1", !multiPair.parent.data.name.includes(query) ? multiPair.parent.y + rectHeight/2 : multiPair.parent.y + rectHeight ) - .attr("x2", multiPair.child.x) - .attr("y2", !multiPair.child.data.name.includes(query) ? multiPair.child.y + rectHeight/2 : multiPair.child.y) - .style("fill","none") - .style("stroke","#ccc") - .style("shape-rendering", "geometricPrecision") - .style("stroke-width","1.5px") - }) - - node.attr("class", "node") - .attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); }) - .attr("transform", nodeRenderer) - .style("font","10px sans-serif"); - - let filteredNodes = node.filter(function(d, i) { - if(queryType === "functions") { - return query === null || d.data.name.includes(query) || i === 0; - } - else { - return true; // TODO support other query types - } - }); - - filteredNodes.append("rect") - .attr("width", rectWidth) - .attr("height", rectHeight) - .attr("transform", "translate(" + (-1 * rectWidth/2) + ",0)") - .style("fill","#fff") - .style("stroke","steelblue") - .style("stroke-width","1.5px"); - - let regNodes = node.filter(function(d, i) { - if(queryType === "functions") { - return query !== null && i !== 0 && !d.data.name.includes(query); - } - else { - return false; // TODO support other query types - } - }); - - regNodes.on("mouseover", showHoverText) - .on("mouseout", hideHoverText); - - filteredNodes.on("mouseover",highlight) - .on("mouseout", unhighlight); - - regNodes.append("circle") - .attr("r", 6) - .attr("transform", "translate(0," + rectHeight/2 + ")"); - - filteredNodes.append("text") - .attr("dy", 22.5) - .attr("text-anchor", "middle") - .text(function(d) { return d.data.name; }); - } - - // update(); - d3.select(self.frameElement).style("height", 200 + "px"); } + createBranchHierarchy(branches) { + branches[branches.length-1].parentId = branches[branches.length-2]; + for(let i = 0; i < branches.length-1; i++) { + branches[i].children = [branches[i+1]]; + if(i !== 0) { + branches[i].parentId = branches[i-1].id; + } + } + return branches[0]; + } + + addToGraph(branches, currentIndex=0, currentBranch=this.rootNode) { + for(let i = 0; i < currentBranch.children.length; i++) { + if(!areBranchesEqual(branch, currentBranch)) { + currentBranch.children.push(branch); + } + this.addToGraph(currentBranch.children[i]); + } + } + + removeUnpinned(currentBranch=this.rootNode) { + for(let i = 0; i < currentBranch.children.length; i++) { + if(!currentBranch.children[i].pinned) { + currentBranch.children.splice(i, 1); + } + this.removeUnpinned(currentBranch.children[i]); + } + } + + togglePinOnBranch(branch) { + let path = generatePath(branch); + for(let i in path) { + path[i].pinned = !path[i].pinned; + } + } - findFuncs( trace ) - { - let self = this ; - let funcs = [] - let doesFuncExist = {} ; - let doesCallExist = {} ; - let lastBlockRange = null ; - let callfuncs = []; - // - for( let index = 1 ; index < trace.timeline.length - 1 ; index++ ) //precomputes all the funcs - { - let step = self.scrubStep( trace.timeline[ index ] ) ; - // - switch( step.type ) - { - case "BlockStatement" : - lastBlockRange = step.range ; - break ; - // - case "FunctionData" : - if( !doesFuncExist[ step.id ] ) - { - if( !lastBlockRange ) - funcs.push( new Vertex( step.type , step.id , [{range:step.range}] , step.value , step.text ) ) ; - else - { - funcs.push( new Vertex( step.type , step.id , [{range:lastBlockRange}] , step.value , step.text ) ) ; - lastBlockRange = null ; - } - doesFuncExist[ step.id ] = true ; - } - else - { - - } - break ; - // - case "CallExpression" : - let found = false; - for(let i = 0; i < callfuncs.length; i++) { - if(callfuncs[i] === step.id) { - found = true; - } - } - if( !found || step.id.includes(".")) { - let currentVertex = new Vertex( step.type , step.id , [{range:step.range}] , null , step.text ); - funcs.push( currentVertex ) ; - callfuncs.push(step.id) - } - else { - for(let i = 0; i < funcs.length; i++) { - if(funcs[i].name === step.id) { - funcs[i].ranges.push({range:step.range}) - } - } - } - - default: {} - - } - } - return funcs ; - } - - makeMatrixList( funcs , map ) - { - // console.log(map) - let self = this ; - for( let index1 = 0 ; index1 < funcs.length ; index1++ ) - { - for( let index2 = 0 ; index2 < funcs.length ; index2++ ) - { - - if( funcs[ index2 ].type === "FunctionData" ) - { - - if( index1 !== index2 && self.isRangeInRange( funcs[ index1 ].ranges[0].range , funcs[ index2 ].ranges[0].range ) ) - { - let mom = funcs[ index2 ].name ; - let child = funcs[ index1 ].name ; - - if( !map[ mom ].childCalls[ child ] ) - { - map[ mom ].children.push( map[ child ] ) ; - if( funcs[ index1 ].type !== "CallExpression" ) - map[ child ].parents.push( map[ mom ] ) ; - // - map[ mom ].childCalls[ child ] = [ "testing" ] ; - } - else - { - map[ mom ].childCalls[ child ].push( "testing" ) ; - } - - } - // - // if( index1 !== index2 && funcs[index1].type === "CallExpression" - // && funcs[index1].text.indexOf( funcs[index2].name.replace( /[()""]/g , "" ) ) ) - // { - // let mom = funcs[ index2 ].name ; - // let child = funcs[ index1 ].name ; - // - // console.log( "callback found" ) ; - // map[ mom ].children.push( map[ child ] ) ; - // map[ child ].parents.push( map[ mom ] ) ; - // map[ child ].isCallback = true ; - // } - } - } - } - return map ; - } - - // - //helpers - // - - scrubStep( step ) - { - if( step !== null ) - { - if( step.text !== null ) - { - step.text = step.text.replace( /"/g , "" ) ; //scrubs for " - } - if( step.id !== null ) - { - step.id = step.id.replace( /[()""]/g , "" ) + "()" ; - - } - if( step.type !== null ) - { - step.type = step.type.replace( /"/g , "" ) ; - } - } - return step ; - } - - isRangeInRange(isRange, inRange) //be careful here! - { - if( isRange.start.row > inRange.start.row && isRange.end.row < inRange.end.row ) - return true ; - - if( isRange.start.row === inRange.start.row || isRange.end.row === inRange.end.row ) - { - if( isRange.start.row === inRange.start.row ) - if( isRange.start.column < inRange.start.column ) - return false ; - if( isRange.end.row === inRange.end.row ) - if( isRange.end.column > inRange.end.column ) - return false ; - return true ; - } - - return false ; - } + generatePath(branch) { + let currentBranch = this.rootNode; + let path = [currentBranch]; + while(currentBranch.parentId !== null) { + let branchHolder = {branch:null}; + getBranchById(currentBranch.parentId, branchHolder); + currentBranch = branchHolder.branch; + path.push(currentBranch); + } + return path; + } + getBranchById(id, branchHolder, currentBranch=this.root) { + if(id === currentBranch.id) { + branchHolder.branch = currentBranch; + return; + } + for(let i = 0; i < currentBranch.children.length; i++) { + getBranchById(currentBranch.children[i]); + } + } + areBranchesEqual(branch1, branch2) { + return branch1.id === branch2.id; } +} diff --git a/src/visualization/vertex.js b/src/visualization/vertex.js deleted file mode 100644 index 005f41ef..00000000 --- a/src/visualization/vertex.js +++ /dev/null @@ -1,15 +0,0 @@ -export class Vertex -{ - constructor( type , name , ranges , values , text ) - { - this.type = type ; - this.name = name ; - this.ranges = ranges ; - this.values = values ; - this.parents = [] ; - this.children = [] ; - this.childCalls = []; - this.isCallback = false ; - this.text = text ; - } -} diff --git a/src/visualization/visualization.js b/src/visualization/visualization.js index 34cb29a1..cc5cc188 100644 --- a/src/visualization/visualization.js +++ b/src/visualization/visualization.js @@ -15,9 +15,12 @@ export class Visualization { this.hasError = false; this.requestSelectionRange = this.getSelectionRange; this.traceHelper = null; + this.query = null; this.queryType = null; + this.branches = []; + this.aceUtils = new AceUtils(); this.aceEditor = ace.edit('aceJsEditorDiv'); this.aceMarkerManager = this.aceUtils.makeAceMarkerManager(this.aceEditor); @@ -33,7 +36,7 @@ export class Visualization { console.log(`No trace found when rendering visualization #${this.id}`); } let formattedTrace = this.formatTrace(this.trace); - this.render(formattedTrace, `#${this.contentId}`, this.query, this.queryType, this.aceUtils, this.aceMarkerManager); + this.render(formattedTrace, `#${this.contentId}`, this.branches, this.query, this.queryType, this.aceUtils, this.aceMarkerManager); } subscribe() { @@ -58,6 +61,10 @@ export class Visualization { this.renderVisualization(); }); + ea.subscribe('navigationChanged', payload => { + this.branches = payload; + }); + ea.publish('searchBoxStateRequest'); } From 751fff052457fd7eb4c5268cf22b0d60e9dea61b Mon Sep 17 00:00:00 2001 From: hmir Date: Mon, 8 Aug 2016 17:18:49 -0400 Subject: [PATCH 2/6] call graph is generated in center of viewport, rubber banding on initial transform fixed --- src/visualization/call-graph.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index a91e3664..95233d8b 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -110,9 +110,9 @@ export class CallGraph { d3.select(divElement).html(""); - let margin = {top: 20, right: 20, bottom: 30, left: 40}, - width = 400 - margin.left - margin.right, - height = 250 - margin.top - margin.bottom; + let margin = {top: 20, right: 20, bottom: 30, left: 40}; + let width = $("#right-splitter").width() - margin.left - margin.right; + let height = $(".tab-content").height() - 300 - margin.top - margin.bottom; let rectWidth = 100, rectHeight = 40; @@ -139,11 +139,14 @@ export class CallGraph { })) .append("g"); - svg.attr("transform","translate(150,0)"); + $(window).resize(function() { + d3.select(divElement).select("svg").attr("width", $("#right-splitter").width() - margin.left - margin.right); + d3.select(divElement).select("svg").attr("height", $(".tab-content").height() - 300 - margin.top - margin.bottom); + }); - let root = d3.hierarchy(branches), - nodes = root.descendants(), - links = root.descendants().slice(1); + let root = d3.hierarchy(branches); + let nodes = root.descendants(); + let links = root.descendants().slice(1); tree(root); let link = svg.selectAll(".link") @@ -263,10 +266,11 @@ export class CallGraph { updatePins(); + svg.selectAll(".node").selectAll("*").attr("transform","translate(" + (width/2 - rectWidth/2) + ",5)"); + svg.selectAll(".node").selectAll("text").attr("transform","translate(" + width/2 + ",5)"); + svg.selectAll(".link").attr("transform","translate(" + width/2 + ",5)"); } - d3.select(self.frameElement).style("height", 200 + "px"); - } createBranchHierarchy(branches) { From affb3be4b6cfb46eaef2ce67f2d9bc251ee88ea0 Mon Sep 17 00:00:00 2001 From: hmir Date: Tue, 9 Aug 2016 10:50:08 -0400 Subject: [PATCH 3/6] added scale extent to call graph, call graph remains in center of viewport even upon window resize, removed vertex import --- src/visualization/call-graph.js | 45 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index 95233d8b..fc5ea469 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -1,5 +1,4 @@ /*global d3*/ -import {Vertex} from "./vertex.js" export class CallGraph { currentDirection = "down";// or "right" directionManager = { @@ -114,11 +113,11 @@ export class CallGraph { let width = $("#right-splitter").width() - margin.left - margin.right; let height = $(".tab-content").height() - 300 - margin.top - margin.bottom; - let rectWidth = 100, - rectHeight = 40; + let rectWidth = 100; + let rectHeight = 40; let tree = d3.tree() - .nodeSize([160, 200]); + .nodeSize([160, 200]); let diagonal = self.directionManager[self.currentDirection].linkRenderer; @@ -127,21 +126,25 @@ export class CallGraph { d3.select(divElement).select("svg").remove(); let svg = d3.select(divElement).append("svg") - .attr("width", width) - .attr("height", height) - .attr("position","relative") - .call(d3.zoom() - .on("zoom", function () { - svg.attr("transform", function() { - let d3event = d3.event.transform; - return "translate(" + d3event.x + ", " + d3event.y + ") scale(" + d3event.k +")"; - }); - })) - .append("g"); + .attr("width", width) + .attr("height", height) + .attr("position","relative") + .call(d3.zoom() + .scaleExtent([0.3, 2]) + .on("zoom", function () { + svg.attr("transform", function() { + let d3event = d3.event.transform; + return "translate(" + d3event.x + ", " + d3event.y + ") scale(" + d3event.k +")"; + }); + })) + .append("g"); $(window).resize(function() { - d3.select(divElement).select("svg").attr("width", $("#right-splitter").width() - margin.left - margin.right); - d3.select(divElement).select("svg").attr("height", $(".tab-content").height() - 300 - margin.top - margin.bottom); + width = $("#right-splitter").width() - margin.left - margin.right; + height = $(".tab-content").height() - 300 - margin.top - margin.bottom; + d3.select(divElement).select("svg").attr("width", width); + d3.select(divElement).select("svg").attr("height", height); + centerNodes(); }); let root = d3.hierarchy(branches); @@ -266,9 +269,11 @@ export class CallGraph { updatePins(); - svg.selectAll(".node").selectAll("*").attr("transform","translate(" + (width/2 - rectWidth/2) + ",5)"); - svg.selectAll(".node").selectAll("text").attr("transform","translate(" + width/2 + ",5)"); - svg.selectAll(".link").attr("transform","translate(" + width/2 + ",5)"); + function centerNodes() { + svg.selectAll(".node").selectAll("*").attr("transform","translate(" + (width/2 - rectWidth/2) + ",5)"); + svg.selectAll(".node").selectAll("text").attr("transform","translate(" + width/2 + ",5)"); + svg.selectAll(".link").attr("transform","translate(" + width/2 + ",5)"); + } } } From 4d4dd97e796ca6c1e031c1062bfda6361bbca735 Mon Sep 17 00:00:00 2001 From: hmir Date: Tue, 9 Aug 2016 11:15:49 -0400 Subject: [PATCH 4/6] call graph now takes up the entirety of #seePanelBody, nodeSize decreased, 'Call Graph' title removed --- src/visualization/call-graph.js | 13 ++++++------- src/visualization/visualization.js | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index fc5ea469..9e9ffc43 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -109,15 +109,14 @@ export class CallGraph { d3.select(divElement).html(""); - let margin = {top: 20, right: 20, bottom: 30, left: 40}; - let width = $("#right-splitter").width() - margin.left - margin.right; - let height = $(".tab-content").height() - 300 - margin.top - margin.bottom; + let width = $("#right-splitter").width(); + let height = $(".tab-content").height(); let rectWidth = 100; let rectHeight = 40; let tree = d3.tree() - .nodeSize([160, 200]); + .nodeSize([160, 80]); let diagonal = self.directionManager[self.currentDirection].linkRenderer; @@ -140,8 +139,8 @@ export class CallGraph { .append("g"); $(window).resize(function() { - width = $("#right-splitter").width() - margin.left - margin.right; - height = $(".tab-content").height() - 300 - margin.top - margin.bottom; + width = $("#right-splitter").width(); + height = $(".tab-content").height(); d3.select(divElement).select("svg").attr("width", width); d3.select(divElement).select("svg").attr("height", height); centerNodes(); @@ -236,7 +235,7 @@ export class CallGraph { .attr("transform", "translate(0," + rectHeight/2 + ")"); matchedNodes.append("text") - .attr("dy", 22.5) + .attr("dy", 14.5) .attr("text-anchor", "middle") .text(function(d) { return d.data.name; }); diff --git a/src/visualization/visualization.js b/src/visualization/visualization.js index cc5cc188..50f6d929 100644 --- a/src/visualization/visualization.js +++ b/src/visualization/visualization.js @@ -3,7 +3,7 @@ export class Visualization { constructor(index, eventAggregator, config) { this.id = "seecoderun-visualization-"+ index; this.buttonId = "seecoderun-visualization-"+ index+"-button"; - this.contentId = "seecoderun-visualization-"+ index+"-content"; + this.contentId = "seePanelBody"; // "seecoderun-visualization-"+ index+"-content"; this.eventAggregator = eventAggregator; this.styleClass = config.config.styleClass; this.title = config.config.title; From 04d59c5522803fd4a588460f24b51052c81cdc30 Mon Sep 17 00:00:00 2001 From: hmir Date: Thu, 11 Aug 2016 09:49:12 -0400 Subject: [PATCH 5/6] increased efficiency when making queries within call graph, documented file --- src/visualization/call-graph.js | 83 ++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index 9e9ffc43..accb0f8d 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -1,6 +1,7 @@ /*global d3*/ export class CallGraph { - currentDirection = "down";// or "right" + currentDirection = "down"; // or "right" + // defines direction that graph displays (top to bottom or left to right) directionManager = { right: { nodeRenderer: function translateRight(d) { @@ -40,26 +41,18 @@ export class CallGraph { this.rootNode = { name: "Program", id: -1, - parentId: null, + parent = null, ranges: [], children: [], isCallback: false, + visible: true, pinned: true } - - this.rootCopy = { - name: "Program", - id: -1, - parentId: null, - ranges: [], - children: [], - isCallback: false, - pinned: true - }; } prepareFx() { - let self = this ; + let self = this; + this.formatTraceFx = function() { return null; } this.renderFx = function renderFx(formattedTrace, divElement, branches, query, queryType, aceUtils, aceMarkerManager) { @@ -67,40 +60,30 @@ export class CallGraph { return; } - self.rootNode = JSON.parse(JSON.stringify(self.rootCopy)); - self.removeUnpinned(); self.addToGraph(self.createBranchHierarchy(branches)); - self.rootCopy = JSON.parse(JSON.stringify(self.root)); - if(query == undefined || query.trim() === "") { query = null; } - function scrubLeaves(root, hasLeaves=0) { - if(root === undefined || root.children === undefined) { - return hasLeaves; - } - + // sets visibility of nodes that match or have a direct path to a match to true, false otherwise + function makeQuery(root=self.rootNode) { let children = root.children; for(let i = 0; i < children.length; i++) { - if(children[i].children.length === 0 && !children[i].name.includes(query)) { - hasLeaves++; - root.children.splice(i, 1); + if(children[i].name.includes(query)) { + let currentNode = children[i]; + while(currentNode.parent) { + currentNode.visible = true; + currentNode = currentNode.parent; + } + } + else { + children[i].visible = false; } - hasLeaves += scrubLeaves(children[i], hasLeaves); + makeQuery(children[i]); } - return hasLeaves; - } - - function scrubTree(root) { - while(scrubLeaves(root)); - } - - function makeQuery() { - scrubTree(root); } if(query !== null && queryType === "functions") { @@ -138,6 +121,7 @@ export class CallGraph { })) .append("g"); + // resize svg upon window resize $(window).resize(function() { width = $("#right-splitter").width(); height = $(".tab-content").height(); @@ -166,6 +150,7 @@ export class CallGraph { .style("stroke","#ccc") .style("stroke-width","1.5px"); + // displays name of node when hovered function showHoverText(d) { d3.select(this).append("text") .attr("class", "hover") @@ -198,6 +183,19 @@ export class CallGraph { .attr("transform", nodeRenderer) .style("font","10px sans-serif"); + // remove nodes of visibility false + if(query !== null) { + node = node.filter(function(d, i) { + if(queryType === "functions") { + return d.data.visible === true; + } + else { + return true; // TODO support other query types + } + }); + } + + // selection of nodes that match query let matchedNodes = node.filter(function(d, i) { if(queryType === "functions") { return query === null || d.data.name.includes(query) || i === 0; @@ -215,6 +213,7 @@ export class CallGraph { .style("stroke","steelblue") .style("stroke-width","1.5px"); + // selection of nodes that do not match query, but have a direct path to a match let regNodes = node.filter(function(d, i) { if(queryType === "functions") { return query !== null && i !== 0 && !d.data.name.includes(query); @@ -239,6 +238,7 @@ export class CallGraph { .attr("text-anchor", "middle") .text(function(d) { return d.data.name; }); + // for all nodes, sets fill of pin to blue if node is pinned, white otherwise function updatePins() { matchedNodes.selectAll("circle").remove(); matchedNodes.filter(function(d) { @@ -268,6 +268,7 @@ export class CallGraph { updatePins(); + // centers all content within svg function centerNodes() { svg.selectAll(".node").selectAll("*").attr("transform","translate(" + (width/2 - rectWidth/2) + ",5)"); svg.selectAll(".node").selectAll("text").attr("transform","translate(" + width/2 + ",5)"); @@ -277,17 +278,19 @@ export class CallGraph { } + // creates hierarchy from array, for any given node, the following node is its child createBranchHierarchy(branches) { - branches[branches.length-1].parentId = branches[branches.length-2]; + branches[branches.length-1].parent = branches[branches.length-2]; for(let i = 0; i < branches.length-1; i++) { branches[i].children = [branches[i+1]]; if(i !== 0) { - branches[i].parentId = branches[i-1].id; + branches[i].parent = branches[i-1]; } } return branches[0]; } + // new branch is added to the graph addToGraph(branches, currentIndex=0, currentBranch=this.rootNode) { for(let i = 0; i < currentBranch.children.length; i++) { if(!areBranchesEqual(branch, currentBranch)) { @@ -297,6 +300,7 @@ export class CallGraph { } } + // all nodes that are not pinned to the graph are removed removeUnpinned(currentBranch=this.rootNode) { for(let i = 0; i < currentBranch.children.length; i++) { if(!currentBranch.children[i].pinned) { @@ -306,6 +310,7 @@ export class CallGraph { } } + // pins or unpins a node and all of its ancestors togglePinOnBranch(branch) { let path = generatePath(branch); for(let i in path) { @@ -313,7 +318,8 @@ export class CallGraph { } } - generatePath(branch) { + // generates path from rootNode to given node + generatePath(node) { let currentBranch = this.rootNode; let path = [currentBranch]; while(currentBranch.parentId !== null) { @@ -325,6 +331,7 @@ export class CallGraph { return path; } + // sets branchHolder.branch to the branch of the requested id, branchHolder must be an object getBranchById(id, branchHolder, currentBranch=this.root) { if(id === currentBranch.id) { branchHolder.branch = currentBranch; From b5d64684860133433f4880f3effe3356dad02bc1 Mon Sep 17 00:00:00 2001 From: hmir Date: Fri, 12 Aug 2016 08:32:57 -0400 Subject: [PATCH 6/6] call grpah changes on branch navigation change --- src/traceView/branch-navigator.js | 2 +- src/visualization/call-graph.js | 159 ++++++++++++++++++++--------- src/visualization/visualization.js | 16 +-- 3 files changed, 119 insertions(+), 58 deletions(-) diff --git a/src/traceView/branch-navigator.js b/src/traceView/branch-navigator.js index 331ad06c..0e8d0389 100644 --- a/src/traceView/branch-navigator.js +++ b/src/traceView/branch-navigator.js @@ -323,4 +323,4 @@ export class BranchNavigator{ this.eventAggregator.publish("branchNavigatorChange", {isVisible: true}); } } -} \ No newline at end of file +} diff --git a/src/visualization/call-graph.js b/src/visualization/call-graph.js index accb0f8d..534601cf 100644 --- a/src/visualization/call-graph.js +++ b/src/visualization/call-graph.js @@ -40,11 +40,11 @@ export class CallGraph { this.rootNode = { name: "Program", - id: -1, - parent = null, + branch: 1, + count: 1, + parent: null, ranges: [], children: [], - isCallback: false, visible: true, pinned: true } @@ -53,15 +53,50 @@ export class CallGraph { prepareFx() { let self = this; - this.formatTraceFx = function() { return null; } + this.formatTraceFx = function(trace, traceHelper) { + if(!traceHelper || !traceHelper.branches) { + return null; + } + + function createBranch(template, prev, branch) { + let newBranch = {}; + newBranch.name = String(template.branch) + "/" + String(template.count); + newBranch.branch = branch; + newBranch.count = template.count; + if(prev.length === 0) { + newBranch.parent = null; + } + + else { + newBranch.parent = prev[0]; + prev[0].children.push(newBranch); + } + + newBranch.ranges = [template.entry.range]; + newBranch.children = []; + newBranch.visible = true; + newBranch.pinned = false; + return newBranch; + } - this.renderFx = function renderFx(formattedTrace, divElement, branches, query, queryType, aceUtils, aceMarkerManager) { + let branches = []; + for(let i = 0; i < traceHelper.branches.length; i++) { + if(traceHelper.branches[i] != undefined) { + branches.push(createBranch(traceHelper.branches[i], branches.slice(-1), traceHelper.branches[i].branch)); + } + } + return branches; + } + + this.renderFx = function renderFx(branches, divElement, query, queryType, aceUtils, aceMarkerManager) { if (!branches || branches.length === 0) { return; } + console.log(branches) + self.removeUnpinned(); - self.addToGraph(self.createBranchHierarchy(branches)); + self.addToGraph(branches);//self.createBranchHierarchy(branches)); if(query == undefined || query.trim() === "") { query = null; @@ -130,7 +165,7 @@ export class CallGraph { centerNodes(); }); - let root = d3.hierarchy(branches); + let root = d3.hierarchy(self.rootNode); let nodes = root.descendants(); let links = root.descendants().slice(1); @@ -224,14 +259,14 @@ export class CallGraph { }); regNodes.on("mouseover", showHoverText) - .on("mouseout", hideHoverText); + .on("mouseout", hideHoverText); matchedNodes.on("mouseover",highlight) - .on("mouseout", unhighlight); + .on("mouseout", unhighlight); regNodes.append("circle") - .attr("r", 6) - .attr("transform", "translate(0," + rectHeight/2 + ")"); + .attr("r", 6) + .attr("transform", "translate(0," + rectHeight/2 + ")"); matchedNodes.append("text") .attr("dy", 14.5) @@ -245,25 +280,28 @@ export class CallGraph { return d.data.pinned === false; }) .append("circle") - .attr("r", 3) + .attr("r", 4) .on("click", function(d) { - self.togglePinOnBranch(d.data); + self.togglePinOn(d.data); updatePins(); }) + .style("stroke", "steelblue") .style("stroke-width", 2) - .style("color", "white"); + .style("fill", "white"); matchedNodes.filter(function(d) { return d.data.pinned === true; }) .append("circle") - .attr("r", 3) + .attr("r", 4) .on("click", function(d) { - self.togglePinOnBranch(d.data); + self.togglePinOff(d.data); updatePins(); }) + .style("stroke", "steelblue") .style("stroke-width", 2) - .style("color", "blue"); + .style("fill", "steelblue"); + centerNodes(); } updatePins(); @@ -275,7 +313,6 @@ export class CallGraph { svg.selectAll(".link").attr("transform","translate(" + width/2 + ",5)"); } } - } // creates hierarchy from array, for any given node, the following node is its child @@ -287,19 +324,44 @@ export class CallGraph { branches[i].parent = branches[i-1]; } } - return branches[0]; + return branches; } - // new branch is added to the graph - addToGraph(branches, currentIndex=0, currentBranch=this.rootNode) { - for(let i = 0; i < currentBranch.children.length; i++) { - if(!areBranchesEqual(branch, currentBranch)) { - currentBranch.children.push(branch); + // new branch is added to the graph TODO: slice off top if branches are equal + addToGraph(branches) { + console.log(this.rootNode.children) + let targetParent = this.rootNode; + let currentChildren; + for(let i = 0; i < branches.length; i++) { + currentChildren = branches.splice(i); + if(!this.branchExists(currentChildren[0])) { + let connectedParent = this.branchExists(targetParent); + currentChildren[0].parent = connectedParent; + connectedParent.children.push(currentChildren[0]); + return; } - this.addToGraph(currentBranch.children[i]); + targetParent = currentChildren[0]; } } + branchExists(branch) { + let found = null; + let self = this; + function search(currentBranch=self.rootNode) { + console.log(self.rootNode.children) + if(self.areBranchesEqual(branch, currentBranch)) { + found = currentBranch; + return; + } + for(let i = 0; i < currentBranch.children.length; i++) { + console.log("eyyy") + search(currentBranch.children[i]); + } + } + search(); + return found; + } + // all nodes that are not pinned to the graph are removed removeUnpinned(currentBranch=this.rootNode) { for(let i = 0; i < currentBranch.children.length; i++) { @@ -310,39 +372,34 @@ export class CallGraph { } } - // pins or unpins a node and all of its ancestors - togglePinOnBranch(branch) { - let path = generatePath(branch); - for(let i in path) { - path[i].pinned = !path[i].pinned; - } - } - - // generates path from rootNode to given node - generatePath(node) { - let currentBranch = this.rootNode; - let path = [currentBranch]; - while(currentBranch.parentId !== null) { - let branchHolder = {branch:null}; - getBranchById(currentBranch.parentId, branchHolder); - currentBranch = branchHolder.branch; - path.push(currentBranch); + // branchToArray(branch) { + // let array = []; + // let currentBranch = branch; + // while(currentBranch.children.length) { + // array.push(currentBranch.children[0]); + // currentBranch = currentBranch.children[0]; + // } + // return array; + // } + + // pins a node and all of its direct ancestors + togglePinOn(branch) { + let currentBranch = branch; + while(currentBranch.parent) { + currentBranch.pinned = true; + currentBranch = currentBranch.parent; } - return path; } - // sets branchHolder.branch to the branch of the requested id, branchHolder must be an object - getBranchById(id, branchHolder, currentBranch=this.root) { - if(id === currentBranch.id) { - branchHolder.branch = currentBranch; - return; - } + // unpins a node and all of its descendants + togglePinOff(currentBranch) { + currentBranch.pinned = false; for(let i = 0; i < currentBranch.children.length; i++) { - getBranchById(currentBranch.children[i]); + this.togglePinOff(currentBranch.children[i]); } } areBranchesEqual(branch1, branch2) { - return branch1.id === branch2.id; + return branch1.name === branch2.name; } } diff --git a/src/visualization/visualization.js b/src/visualization/visualization.js index 50f6d929..83a477c6 100644 --- a/src/visualization/visualization.js +++ b/src/visualization/visualization.js @@ -19,8 +19,6 @@ export class Visualization { this.query = null; this.queryType = null; - this.branches = []; - this.aceUtils = new AceUtils(); this.aceEditor = ace.edit('aceJsEditorDiv'); this.aceMarkerManager = this.aceUtils.makeAceMarkerManager(this.aceEditor); @@ -35,8 +33,8 @@ export class Visualization { if(!this.trace){ console.log(`No trace found when rendering visualization #${this.id}`); } - let formattedTrace = this.formatTrace(this.trace); - this.render(formattedTrace, `#${this.contentId}`, this.branches, this.query, this.queryType, this.aceUtils, this.aceMarkerManager); + let formattedTrace = this.formatTrace(this.trace, this.traceHelper); + this.render(formattedTrace, `#${this.contentId}`, this.query, this.queryType, this.aceUtils, this.aceMarkerManager); } subscribe() { @@ -61,8 +59,14 @@ export class Visualization { this.renderVisualization(); }); - ea.subscribe('navigationChanged', payload => { - this.branches = payload; + ea.subscribe('traceNavigationChange', payload => { + let self = this; + console.log(payload) + //without timeout, at least one value of attribute "branch" in the payload is undefined + setTimeout( function(){ + this.traceHelper = payload; + self.renderVisualization(); + }, 1000) }); ea.publish('searchBoxStateRequest');