Published
Edited
Jan 8, 2021
1 fork
2 stars
Insert cell
Insert cell
Insert cell
chart = {
const links = graphData.links;
const nodes = graphData.nodes;

const svg = d3.select(DOM.svg(width, height));

const layout = cola.d3adaptor(d3)
.size([width, height])
.nodes(nodes)
.links(links)
.jaccardLinkLengths(60, 0.7)
.start(30);
//default opacity and width of links
function linkO(d) {return .3 + Math.log10(d.value)*.3}
function linkW(d) {return .7 + Math.log10(d.value)*.6}
// Per-link markers
const marker = svg.append("defs").selectAll("marker")
.data(links)
.join("marker")
.attr("id", d => `arrow-${d.id}`)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -0.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.attr("stroke-width", .01)
.attr("opacity", .8)
.append("path")
.attr("fill", d => color(d.source.group))
.attr("stroke", d => color(d.source.group))
.attr("stroke-width", .01)
.attr("opacity", 0.8)
.attr("d", "M0,-5L10,0L0,5")

// Links
const link = svg.append("g")
.attr("stroke", "#999")
.attr("fill", "none")
.attr("stroke-width", 0.5)
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", d => color(d.source.group))
.attr("stroke-width", d=> linkW(d))
.attr("stroke-opacity", d=> linkO(d))
.attr("marker-end", d => `url(${new URL(`#arrow-${d.id}`, location)})`);

//hover-over spots in mid-links
const linkDot = svg.append("g")
.attr("stroke", "#889")
.attr("stroke-width", 0)
.selectAll("circle")
.data(links)
.enter().append("circle")
.attr("r", d => 10)
.attr("fill", "#889")
.attr("opacity", .04)
linkDot.append("title")
.text(d => d.source.className + " -> "+d.target.className +"\n" + d.value + "x");
//nodes
const node = svg.append("g")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", d => 5 + Math.log2(d.count))
.attr("fill", d => color(d.group))
.attr("stroke", "#333")
.attr("stroke-width", .5)
.attr("opacity", .8)
.call(layout.drag);

node.append("title")
.text(d => d.className
+ " (called "+d.count+"x)\n"
+ d.packageName
//+ " - group " + d.group
+ "\n---\nIngress method calls: \n"
+ (function methods(iter){ let item = iter.next(); return item.done ? "" : " - "
+ item.value[0] + "(): " + item.value[1].count +"x\n" + methods(iter); })(d.inboundMethods.entries()))

// mouse-over event handlers
node.on('mouseover', function (d) {
// Highlight the nodes: every node is green except of him
node.style('stroke', "#333")
.style("stroke-width", 0.5)
.style('opacity',.4)
d3.select(this)
.style("stroke-width", 2.0)
.style('stroke', "#f3f")
.style("opacity", 1.0)
// Highlight the connections
link
.style('stroke-width', function (link_d) {
return link_d.source.id === d.id || link_d.target.id === d.id ? 1.5 + Math.log10(link_d.value) :
.4;})
.style('stroke-opacity', function (link_d) {
return link_d.source.id === d.id || link_d.target.id === d.id ? 1.0 :
.3;})
});
node.on('mouseout', function (d) {
node.style('stroke', "#333")
.style("stroke-width", 0.5)
.style("opacity", .8)
link.style("stroke-width", link_d => linkW(link_d))
.style('stroke-opacity', link_d => linkO(link_d))
.style("stroke", linkd_d => color(d.source.group))
marker.style("stroke", color(d.source.group))
.style("fill", color(d.source.group))
});
// highlight a link when moving over its mid-section
linkDot.on('mouseover', function (d) {
node
.style('opacity', function (dd) {
return d.source.id === dd.id || d.target.id === dd.id ? 1.0 : .4;})
.style('stroke-width', function (dd) {
return d.source.id === dd.id || d.target.id === dd.id ? 2.0 : .5;})
.style('stroke', function (dd) {
return d.source.id === dd.id || d.target.id === dd.id ? "#f3f" : "#333";})

link
.style('stroke', function (link_d) {
return link_d.id === d.id ? color(link_d.source.group) : color(link_d.source.group);})
.style('stroke-width', function (link_d) {
return link_d.id === d.id ? 1.5 + Math.log10(link_d.value) : .4 ;})
.style('stroke-opacity', function (link_d) {
return link_d.id === d.id ? 1.0 : .3 })
marker.style("stroke", link_d => color(link_d.source.group))
.style("fill", link_d => color(link_d.source.group))
});
linkDot.on('mouseout', function (d) {
node.style('stroke', "#333")
.style("stroke-width", 0.5)
.style("opacity", .8)

link.style("stroke", link_d => color(link_d.source.group))
.style("stroke-width", link_d => linkW(link_d))
.style('stroke-opacity', link_d => linkO(link_d))
marker.style("stroke", link_d => color(link_d.source.group))
.style("fill", link_d => color(link_d.source.group))
})
layout.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);

node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
linkDot
.attr("cx", d => (d.source.x + d.target.x)/2)
.attr("cy", d => (d.source.y + d.target.y)/2);
});
//legend
const scale = d3.scaleOrdinal()
.domain(Array.from(graphData.index_packages.keys()))
.range(colors)
const legend = d3Legend
.legendColor()
.shape("path", d3.symbol().type(d3.symbolCircle).size(50)()) //.shape("circle")
.shapePadding(20)
.labelOffset(5)
.scale(scale)
.labels(Array.from(graphData.index_packages.keys()));
svg.append("g")
.attr("class", "legend_auto")
.attr('fill',"#444")
.attr("font-family", "'Work Sans', sans-serif")
.attr('font-size', 12)
.attr("transform", `translate(${width - 200 + 5}, 20)`)
.call(legend)
invalidation.then(() => layout.stop());

return svg.node();
}
Insert cell
Insert cell
graphMap=d3.json(scenarioApiUrl)
Insert cell
graphData=appMap2Graph(graphMap)
Insert cell
//converts a recorded AppMap to a graph object with nodes and edges
function appMap2Graph(appmap) {
const noPackage = "<no package>"
var newGraph = {}
newGraph.nodes = []
newGraph.links = []
newGraph.index_nodes = new Map()
newGraph.index_links = new Map()
newGraph.index_packages = new Map().set(noPackage, {group: 0, count: 0})
newGraph.metadata = appmap.metadata
let group = 1
function packageFourDeep(packageName) {
let regexp = /^[^\/\.]*[\.\/][^\/\.]*[\.\/][^\/\.]*[\.\/][^\/\.]*/g
if (packageName == null) return packageName
let results = packageName.match(regexp)
return results == null ? packageName : results[0]
}

function appMap2GraphInner(appmap, graph, currentIndex, currentCaller) {
var newNode = {}
var newCaller = {}
var newLink = {}
do {
if(appmap.events.length <= currentIndex) return -1; //at the end of the event array
newCaller = appmap.events[currentIndex]
if(newCaller.event=="return") {
return currentIndex + 1; //this call is ovah
}
if (newCaller.event=="call" && newCaller.defined_class != null) {
//now lets add this call to the graph
//first getting unique node, have we seen this class before?
if(!graph.index_nodes.has(newCaller.defined_class)) {
newNode = Object.assign(Object.create(newCaller))
newNode.id = newNode.defined_class
newNode.count = 1
newNode.inboundMethods = new Map()
graph.index_nodes.set(newCaller.defined_class, newNode)
graph.nodes.push(newNode)
//get package and class name
if(appmap.metadata.language.name=="java") {
let split = /^(.*)\.(.*$)?/
let splitResult = newNode.defined_class.match(split)
if(splitResult == null) {
newNode.className=newNode.defined_class
newNode.packageName=noPackage
} else {
newNode.className=splitResult[2]
newNode.packageName=splitResult[1]
}
} else if(appmap.metadata.language.name=="ruby") {
newNode.className=newNode.defined_class
let split = /^([^\/].*)\/(.*)/
let splitResult = newNode.path.match(split)
newNode.packageName=(splitResult != null) ? splitResult[1] : noPackage
}
// to avoid too many groups, only consider up to four levels of package hierarchy
if (graph.index_packages.has(packageFourDeep(newNode.packageName))) {
newNode.group = graph.index_packages.get(packageFourDeep(newNode.packageName)).group
graph.index_packages.get(packageFourDeep(newNode.packageName)).count++
} else {
newNode.group = group
graph.index_packages.set(packageFourDeep(newNode.packageName), {group: group++, count: 1})
}
} else {
newNode = graph.index_nodes.get(newCaller.defined_class)
newNode.count++
graph.index_packages.get(packageFourDeep(newNode.packageName)).count++
}
//second - create or update links
if(currentCaller != null) {
if((!graph.index_links.has(currentCaller.id+"%%"+newNode.id))) {
newLink = {}
newLink.value = 1
newLink.source = currentCaller
newLink.target = newNode
newLink.id = currentCaller.id+"%%"+newNode.id
graph.links.push(newLink)
graph.index_links.set(currentCaller.id+"%%"+newNode.id, newLink)
} else {
newLink = graph.index_links.get(currentCaller.id+"%%"+newNode.id)
newLink.value++
}
}
// let's add some ibound methods in the mix
if(newNode.inboundMethods.has(newCaller.method_id)) {
newNode.inboundMethods.get(newCaller.method_id).count++
} else {
newNode.inboundMethods.set(newCaller.method_id, {count: 1})
}
//end processing method calls
} else if (newCaller.event=="command") {
//sql tbd
}
currentIndex = appMap2GraphInner(appmap, graph, currentIndex + 1, newNode)
} while (currentIndex > -1)
return currentIndex
}
appMap2GraphInner(appmap, newGraph, 0, null)
return newGraph
}
Insert cell
Insert cell
colors = ["#4e79a7","#f28e2c","#e15759","#76b7b2","#59a14f","#edc949","#af7aa1","#ff9da7","#9c755f","#bab0ab"]
Insert cell
function color(d){ return colors[d % colors.length]}
Insert cell
cola = require("webcola@3/WebCola/cola.min.js")
Insert cell
height = 800
Insert cell
d3Legend = require('d3-svg-legend')
Insert cell
d3 = require("d3@5")

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more