Published
Edited
Dec 26, 2020
Insert cell
Insert cell
Insert cell
chart = {
const root = tree(d3.hierarchy(data));
var leaves = 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 svg = d3.create("svg")
.attr("viewBox", [-width / 2, -width / 2, width, width]);

const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(leaves)
.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)
.text(d => d.data.name)
.on("mouseover", textOver)
.on("mouseout", textOut)
.call(text => text.append("title").text(d => `${d.data.className} in ${d.data.packageName}
- called from ${d.data.inbound.length} classes in this mapset, total calls: ${d.data.count}
- calls methods in ${d.data.outbound.length} classes in this mapset, total calls: ${d.data.calls}`)
);

const linkData = leaves.flatMap(leaf => leaf.data.outbound.map(x => path(leaf, x.parent)))
const link = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(d3.transpose(linkData.map(path => Array.from(path.split(k)))))
.join("path")
.style("mix-blend-mode", "darken")
.attr("opacity", 1.0)
.attr("stroke", (d, i) => color(d3.easeQuad(i / ((1 << k) - 1))))
.attr("d", d => d.join(""))

const overLink = svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(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.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 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 color(0.1);
if(ol[0].getTarget()==d) return color(.9);
return "#FFF"
})
}
function textOut(event, d) {
link.style("opacity",1.0)
overLink.style("opacity", 0.0)
d3.select(this).attr("font-weight", null);
}
return svg.node();
}
Insert cell
Insert cell
appmapData = d3.json(scenarioApiUrl)
Insert cell
data = appMap2Hierarchy(appmapData)
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 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)
}
}
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.interpolateRdBu(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