Published
Edited
Jan 13, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
diffChart=chart(urlParams.has("chart") ? urlParams.get("chart") : "diff")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function chart(whatwhat) {
//0 - diff, 1 - union, 2 - intersection
const what = (whatwhat=="union" ? 1 : (whatwhat == "intersection" ? 2 : 0))
const root = tree(d3.hierarchy(dataDiff));
//data structures for the D3 graph links
var allLeaves = root.leaves().map(function addInOutBounds(d){
d.data.parent = d;
d.data.inbound = [...d.data.ingressClasses.values()]
d.data.outbound = [...d.data.egressClasses.values()]
return d })
const linkData = allLeaves.flatMap(leaf => leaf.data.outbound.map(x => path(leaf, x.parent)))
var addedLeaves = root.leaves().map(function addInOutBounds(d){
d.data.parent = d;
d.data.inbound = [...d.data.ingressAdded.values()]
d.data.outbound = [...d.data.egressAdded.values()]
return d })
const linkDataAdded = addedLeaves.flatMap(leaf => leaf.data.outbound.map(x => path(leaf, x.parent)))
var removedLeaves = root.leaves().map(function addInOutBounds(d){
d.data.parent = d;
d.data.inbound = [...d.data.ingressRemoved.values()]
d.data.outbound = [...d.data.egressRemoved.values()]
return d })
const linkDataRemoved = removedLeaves.flatMap(leaf => leaf.data.outbound.map(x => path(leaf, x.parent)))
var leavesInBoth = root.leaves().map(function addInOutBounds(d){
d.data.parent = d;
d.data.inbound = [...d.data.ingressInBoth.values()]
d.data.outbound = [...d.data.egressInBoth.values()]
return d })
const linkDataInBoth = leavesInBoth.flatMap(leaf => leaf.data.outbound.map(x => path(leaf, x.parent)))
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -width / 2, width, width]);

// nodes == classes organized in packages
const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 4)
.selectAll("g")
.data(allLeaves)
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? 6 : -6)
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.attr("fill", d=> what==0 ? (d.data.inMapOne? (d.data.inMapTwo? "#000" : "#500") : "#050"):
(what==2 ? ((d.data.inMapOne && d.data.inMapTwo) ? "#000" : "#ccc"): "#000"))
.text(d => (what==0? (d.data.inMapOne ? (d.data.inMapTwo ? "| " : "- ") : "+ "): "") + d.data.name)
.on("mouseover", textOver)
.on("mouseout", textOut)
.call(text => text.append("title").text(d => getText(d.data))
);
//links == class relationships
var link = null, linkOne = null, linkTwo = null;
if(what != 0) {
link = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(d3.transpose((what == 2 ? linkDataInBoth : linkData).map(path => Array.from(path.split(k)))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 1.0)
.attr("stroke", (d, i) => d3.interpolateRdBu((d3.easeQuad(i / ((1 << k) - 1)))))
.attr("d", d => d.join(""))
} else {
link = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(d3.transpose(linkDataInBoth.map(path => Array.from(path.split(k)))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 0.01)
.attr("stroke", (d, i) => d3.interpolateGreys(1-(d3.easeQuad(i / ((1 << k) - 1)))))
.attr("d", d => d.join(""))

linkOne = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(d3.transpose(linkDataRemoved.map(path => Array.from(path.split(k)))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 1.0)
.attr("stroke-width", 1.7)
.attr("stroke", (d, i) => d3.interpolateOrRd(1-(d3.easeQuad(i / ((1 << k) - 1)))))
.attr("d", d => d.join(""))

linkTwo = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(d3.transpose(linkDataAdded.map(path => Array.from(path.split(k)))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 1.0)
.attr("stroke-width", 1.7)
.attr("stroke", (d, i) => d3.interpolateGnBu((d3.easeQuad(i / ((1 << k) - 1)))))
.attr("d", d => d.join(""))
}
const overLink = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data((what == 2 ? linkDataInBoth : linkData).map(path => Array.from(path.split(1))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 0.0)
.attr("stroke-width", 1.5)
.attr("d", d => d.join(""))

function textOver(event, d) {
[link, linkOne, linkTwo].forEach( l=> { if (l!=null) l.style("opacity",0.05)});
overLink.style("opacity",0.0)
d3.select(this).attr("font-weight", "bold");
overLink.style("opacity", function (ol) {
if(ol[0].getSource()==d || ol[0].getTarget()==d) return .8; else
return 0
})
overLink.style("stroke", function (ol) {
if(what != 0) {//if not diff, blue called, red if calling
//if calling self, set color to purple
if(ol[0].getSource().data.id==d.data.id && ol[0].getTarget().data.id==d.data.id) return "#F3F";
if(ol[0].getSource()==d) return d3.interpolateRdBu(0.1);
if(ol[0].getTarget()==d) return d3.interpolateRdBu(.9);
return "#FFF"
} else {
return "#000"
}
})
}
function getText(node) {
return `${node.className} in package ${node.packageName}

- called from ${node.ingressClassesOne.size} classes in AppMap 1, total calls: ${node.countOne}
- called from ${node.ingressClassesTwo.size} classes in AppMap 2, total calls: ${node.countTwo}
coupling ingress diff: +${node.ingressAdded.size} classes, -${node.ingressRemoved.size} classes, | ${node.ingressInBoth.size} classes

- calls methods in ${node.egressClassesOne.size} classes in AppMap 1, total calls: ${node.callsOne}
- calls methods in ${node.egressClassesTwo.size} classes in AppMap 2, total calls: ${node.callsTwo}
coupling egress diff: +${node.egressAdded.size} classes, -${node.egressRemoved.size} classes, | ${node.egressInBoth.size} classes

`}

function textOut(event, d) {
[linkOne, linkTwo].forEach( l=> { if (l!=null) l.style("opacity",1.0)});
if(link!=null) link.style("opacity", (what == 0) ? 0.5 : 1.0)
overLink.style("opacity", 0.0)
d3.select(this).attr("font-weight", null);
}
return svg.node();
}
Insert cell
Insert cell
urlParams = new URLSearchParams(location.search)
Insert cell

appmapData1 = d3.json(scenarioApiUrl1)
Insert cell
appmapData2 = d3.json(scenarioApiUrl2)
Insert cell
data1 = appMap2Hierarchy(appmapData1)
Insert cell
data2 = appMap2Hierarchy(appmapData2)
Insert cell
dataDiff = appMapDiff(data1, data2)
Insert cell
function appMapDiff(map1, map2) {
const diffMap = Object.assign(map1)
//add all nodes from first map to the result
let iter = diffMap.index_nodes.values();
let node = iter.next()
while(!node.done) {
node.value.inMapOne = true;
node.value.inMapTwo = false;
node.value.countOne = node.value.count
node.value.callsOne = node.value.calls
node.value.countTwo = 0
node.value.callsTwo = 0
node.value.ingressClassesOne = new Map(node.value.ingressClasses);
node.value.egressClassesOne = new Map(node.value.egressClasses);
node.value.ingressClassesTwo = new Map();
node.value.egressClassesTwo = new Map();
node = iter.next()
}//end while
//add all nodes from the second map to the result
iter = map2.index_nodes.values();
node = iter.next()
while(!node.done) {
if(diffMap.index_nodes.has(node.value.id)) {
let oldNode = diffMap.index_nodes.get(node.value.id)
oldNode.inMapTwo = true
oldNode.countTwo = node.value.count
oldNode.callsTwo = node.value.calls
oldNode.count += oldNode.countTwo
oldNode.calls += oldNode.callsTwo
oldNode.ingressClassesTwo = node.value.ingressClasses;
oldNode.egressClassesTwo = node.value.egressClasses;
} else {
var newNode = Object.assign(node.value)
diffMap.index_nodes.set(node.value.id, newNode )
newNode.inMapOne = false
newNode.inMapTwo = true
newNode.countOne = 0
newNode.callsOne = 0
newNode.countTwo = node.value.count
newNode.callsTwo = node.value.calls
newNode.ingressClassesOne = new Map();
newNode.egressClassesOne = new Map();
newNode.ingressClassesTwo = node.value.ingressClasses;
newNode.egressClassesTwo = node.value.egressClasses;
addClass(diffMap, newNode)
}
node = iter.next()
}//end while
iter = diffMap.index_nodes.values()
node = iter.next()
while(!node.done) {
//re-point ingress and egress classes to nodes in the diffMap
let maps = [node.value.ingressClassesOne, node.value.ingressClassesTwo,
node.value.egressClassesOne, node.value.egressClassesTwo]
debugger;
maps.forEach(nodes => {
[ ...nodes.keys()].forEach(key => {
nodes.set(key, diffMap.index_nodes.get(key))})
})
//get ingress and egress maps as unions of maps one and two
node.value.ingressClasses=new Map(node.value.ingressClassesOne);
[ ...(node.value.ingressClassesTwo.keys())].forEach(key => {
if(!node.value.ingressClasses.has(key)) node.value.ingressClasses.set(key, node.value.ingressClassesTwo.get(key))
})
node.value.egressClasses=new Map(node.value.egressClassesOne);
[ ...(node.value.egressClassesTwo.keys())].forEach(key => {
if(!node.value.egressClasses.has(key)) node.value.egressClasses.set(key, node.value.egressClassesTwo.get(key))
})
//now let's get the diff of relationships for each node
node.value.ingressRemoved = new Map(node.value.ingressClassesOne);
node.value.ingressAdded = new Map();
node.value.ingressInBoth = new Map();
node.value.ingressClassesTwo.forEach( function(value, key) {
if(node.value.ingressRemoved.has(key)) {
node.value.ingressRemoved.delete(key)
node.value.ingressInBoth.set(key, value)
} else {
node.value.ingressAdded.set(key, value)
}
})
node.value.egressRemoved = new Map(node.value.egressClassesOne);
node.value.egressAdded = new Map();
node.value.egressInBoth = new Map();
node.value.egressClassesTwo.forEach( function(value, key) {
if(node.value.egressRemoved.has(key)) {
node.value.egressRemoved.delete(key)
node.value.egressInBoth.set(key, value)
} else {
node.value.egressAdded.set(key, value)
}
})

node = iter.next()
} //end while
diffMap.children = [ ...diffMap.index_packages.values()].sort((a, b) => a.name < b.name? -1 : a.name > b.name ? 1 : 0)
diffMap.children.forEach( n => n.children = [ ...n.children_index.values()].sort((a, b) => a.name < b.name? -1 : a.name > b.name ? 1 : 0))
return diffMap
}
Insert cell
function addClass(hierarchy, node) {
var pkg
//add the package to the hieararchy
if(!hierarchy.index_packages.has(node.packageName)) {
pkg = { name: node.packageName, children_index: new Map() }
hierarchy.index_packages.set(node.packageName, pkg)
} else {
pkg = hierarchy.index_packages.get(node.packageName)
}
if(!pkg.children_index.has(node.className)) {
//this class has not been seen in this package before
pkg.children_index.set(node.className, node)
}
}
Insert cell
//converts a recorded AppMap to a hiearchy object
function appMap2Hierarchy(appmap) {
const noPackage = "<no package>"
var newHierarchy = {name: "root", children: []}
newHierarchy.index_nodes = new Map()
newHierarchy.index_packages = new Map()
newHierarchy.metadata = appmap.metadata
function getClassPackage(event, language) {
//returns {name: "classname", package: "packagename"}
var info = {};
if(language=="java") {
let split = /^(.*)\.(.*$)?/
let splitResult = event.defined_class.match(split)
if(splitResult == null) {
info.className=event.defined_class
info.packageName=noPackage
} else {
info.className=splitResult[2]
info.packageName=splitResult[1]
}
} else if(language=="ruby") {
info.className=event.defined_class
let split = /^([^\/].*)\/(.*)/
let splitResult = event.path.match(split)
info.packageName=(splitResult != null) ? splitResult[1] : noPackage
} else throw new Error("Unsupported language: " + language);
return info;
}
function appMap2HierarchyInner(appmap, hierarchy, currentIndex, currentCaller) {
var newNode = {}
var newCaller = {}
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 hierarchy
//first getting unique node, have we seen this class before?
if(!hierarchy.index_nodes.has(newCaller.defined_class)) {
newNode = Object.assign(Object.create(newCaller))
newNode.id = newNode.defined_class
newNode.count = 1
newNode.calls = 0
newNode.ingressClasses = new Map()
newNode.egressClasses = new Map()
hierarchy.index_nodes.set(newCaller.defined_class, newNode)
//get package and class name
let info = getClassPackage(newNode, appmap.metadata.language.name)
newNode.className=info.className
newNode.name=info.className
newNode.packageName=info.packageName
} else {
newNode = hierarchy.index_nodes.get(newCaller.defined_class)
newNode.count++
}
addClass(newHierarchy, newNode)
if(currentCaller != null) {
// add the relationships to the node
newNode.ingressClasses.set(currentCaller.id, currentCaller)
currentCaller.egressClasses.set(newNode.id, newNode)
currentCaller.calls++
}
} else if (newCaller.event=="command") {
//sql tbd
}
currentIndex = appMap2HierarchyInner(appmap, hierarchy, currentIndex + 1, newNode)
} while (currentIndex > -1)
return currentIndex
}
appMap2HierarchyInner(appmap, newHierarchy, 0, null)
newHierarchy.children = [ ...newHierarchy.index_packages.values()].sort((a, b) => a.name < b.name? -1 : a.name > b.name ? 1 : 0)
newHierarchy.children.forEach( n => n.children = [ ...n.children_index.values()].sort((a, b) => a.name < b.name? -1 : a.name > b.name ? 1 : 0))
return newHierarchy
}
Insert cell
Insert cell
function path(source, target) {
if(target == null) {
debugger
}
const target2 = (source == target) ? Object.assign(Object.create(source)) : target
const p = new Path(null, source, target2);
line.context(p)(source.path(target2));
return p;
}
Insert cell
class Path {
constructor(_, src, tgt) {
this._ = _;
this._m = undefined;
this._source = src;
this._target = tgt;
}

getSource() {
return this._source;
}
getTarget() {
return this._target;
}
moveTo(x, y) {
this._ = [];
this._m = [x, y];
}
lineTo(x, y) {
this._.push(new Line(this._m, this._m = [x, y]));
}
bezierCurveTo(ax, ay, bx, by, x, y) {
this._.push(new BezierCurve(this._m, [ax, ay], [bx, by], this._m = [x, y]));
}
*split(k = 0) {
const n = this._.length;
const i = Math.floor(n / 2);
const j = Math.ceil(n / 2);
const a = new Path(this._.slice(0, i), this._source, this._target);
const b = new Path(this._.slice(j), this._source, this._target);
if (i !== j) {
const [ab, ba] = this._[i].split();
a._.push(ab);
b._.unshift(ba);
}
if (k > 1) {
yield* a.split(k - 1);
yield* b.split(k - 1);
} else {
yield a;
yield b;
}
}
toString() {
return this._.join("");
}
}
Insert cell
class Line {
constructor(a, b) {
this.a = a;
this.b = b;
}
split() {
const {a, b} = this;
const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
return [new Line(a, m), new Line(m, b)];
}
toString() {
return `M${this.a}L${this.b}`;
}
}
Insert cell
BezierCurve = {
const l1 = [4 / 8, 4 / 8, 0 / 8, 0 / 8];
const l2 = [2 / 8, 4 / 8, 2 / 8, 0 / 8];
const l3 = [1 / 8, 3 / 8, 3 / 8, 1 / 8];
const r1 = [0 / 8, 2 / 8, 4 / 8, 2 / 8];
const r2 = [0 / 8, 0 / 8, 4 / 8, 4 / 8];

function dot([ka, kb, kc, kd], {a, b, c, d}) {
return [
ka * a[0] + kb * b[0] + kc * c[0] + kd * d[0],
ka * a[1] + kb * b[1] + kc * c[1] + kd * d[1]
];
}

return class BezierCurve {
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
split() {
const m = dot(l3, this);
return [
new BezierCurve(this.a, dot(l1, this), dot(l2, this), m),
new BezierCurve(m, dot(r1, this), dot(r2, this), this.d)
];
}
toString() {
return `M${this.a}C${this.b},${this.c},${this.d}`;
}
};
}
Insert cell
line = d3.lineRadial()
.curve(d3.curveBundle.beta(.86))
.radius(d => d.y)
.angle(d => d.x)
Insert cell
tree = d3.cluster()
.size([2 * Math.PI, radius - 100])
Insert cell
color = t => d3.interpolateGreys(t)
Insert cell
k = 6 // 2^k colors segments per curve
Insert cell
width = 1000
Insert cell
radius = width / 2.5
Insert cell
d3 = require("d3@6")
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