Skip to content

Commit

Permalink
Merge pull request #1215 from carolinetew/collapsible-treeview-visual…
Browse files Browse the repository at this point in the history
…isation

Collapsible Treeview: Initial Visualisation
  • Loading branch information
stephengoldbaum authored Dec 24, 2024
2 parents 106edf6 + bc9b89c commit 652cd54
Show file tree
Hide file tree
Showing 20 changed files with 2,648 additions and 44 deletions.
1,172 changes: 1,172 additions & 0 deletions cli/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@types/d3": "^7.4.3",
"d3": "^7.9.0",
"morphir-elm": "2.86.0"
}
}
304 changes: 284 additions & 20 deletions cli/treeview/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toDistribution, Morphir } from "morphir-elm";
import { TreeNode } from "./treeNode";
import * as d3 from "d3";

const treeviewTitle: string = "Treeview Display";
document.body.innerHTML = `<h2>${treeviewTitle}</h2>`;

window.onload = Home;

Expand All @@ -15,9 +15,9 @@ interface RawDistribution {
fornatVersion: Number;
}

async function getIR() {
export async function getIR(): Promise<TreeNode | string> {
try {
const response = await fetch("/server/morphir-ir.json", {
let response = await fetch("/server/morphir-ir.json", {
method: "GET",
});

Expand All @@ -33,10 +33,11 @@ async function getIR() {
JSON.stringify(result)
);
console.log("DIST: ", distribution);
const treeview: TreeNode = createTree(distribution);
let treeview: TreeNode = createTree(distribution);
treeview.children = nestedTree(treeview);
console.log("CREATED TREE: ", treeview);
document.body.innerHTML = `<h2>${treeview.name}</h2>`;

d3.select("div").append(() => createChart(treeview));
return treeview;
} catch (error) {
return error instanceof Error
Expand All @@ -45,6 +46,241 @@ async function getIR() {
}
}

function createChart(data: TreeNode): SVGSVGElement | null {
// Chart from: https://observablehq.com/@d3/collapsible-tree with modifications to support our implementation.
// Specify the charts’ dimensions. The height is variable, depending on the layout.
const marginTop = 10;
const marginRight = 200;
const marginBottom = 10;
const marginLeft = 200;

// Rows are separated by dx pixels, columns by dy pixels. These names can be counter-intuitive
// (dx is a height, and dy a width). This because the tree must be viewed with the root at the
// “bottom”, in the data domain. The width of a column is based on the tree’s height.
const root = d3.hierarchy<TreeNode>(data as TreeNode);
const dx = 30; //10
// const dy = ((width - marginRight - marginLeft) / (1 + root.height))+30;// NEed mor space
const dy = 150;

// Define the tree layout and the shape for links.
const tree = d3.tree<TreeNode>().nodeSize([dx, dy]);
const diagonal = d3
.linkHorizontal()
.x((d: any) => d.y)
.y((d: any) => d.x);
// const diagonal = d3.linkHorizontal().x(d => d.x).y(d => d.y);

// Create the SVG container, a layer for the links and a layer for the nodes.
const svg = d3
.create("svg")
.attr("width", dy)
.attr("height", dx)
.attr("viewBox", [-marginLeft, -marginTop, dy, dx])
.attr(
"style",
"width: auto; height: auto; font: 10px sans-serif; user-select: none;"
);

const gLink = svg
.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);

const gNode = svg
.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");

function update(event: any, source: any) {
const duration = event?.altKey ? 2500 : 250; // hold the alt key to slow down the transition
const nodes = (
root.descendants() as d3.HierarchyPointNode<TreeNode>[]
).reverse();
const links = root.links();

// Compute the new tree layout.
tree(root);

let left = root;
let right = root;
let up = root;
let down = root;
root.eachBefore((node) => {
if (
node.x === undefined ||
node.y == undefined ||
left.x === undefined ||
right.x === undefined ||
up.y === undefined ||
down.y === undefined
)
return;
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
if (node.y < up.y) up = node;
if (node.y > down.y) down = node;
});
if (
left.x === undefined ||
right.x === undefined ||
down.y == undefined ||
up.y == undefined
)
return;

const height = right.x - left.x + marginTop + marginBottom;
const width = down.y - up.y + marginRight + marginLeft;

const transition = svg
.transition()
.duration(duration)
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, left.x - marginTop, width, height].join());

if (!window.ResizeObserver) {
transition.tween("resize", function () {
return function () {
svg.dispatch("toggle");
};
});
}

// Update the nodes…
const node = gNode
.selectAll<SVGGElement, d3.HierarchyNode<TreeNode>>("g")
.data(nodes, (d) => d.id as string);

// Enter any new nodes at the parent's previous position.
const nodeEnter = node
.enter()
.append("g")
.attr("transform", (d) => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d: any) => {
d.children = d.children ? null : d._children;
update(event, d);
});

const types = ["Enum", "CustomType", "Record", "Alias"];
nodeEnter
.append("circle")
.attr("r", 2.5)
.attr("fill", (d: any) => {
//blue node if it is a type, less bright when terminating node
if (types.includes(d.data.type))
return d._children ? "#0000ff" : "#5a86ad";
return d._children ? "#555" : "#999";
})
.attr("stroke-width", 10);

nodeEnter
.append("text")
.attr("dy", "0.31em")
.attr("x", (d: any) => (d._children ? -6 : 6))
.attr("text-anchor", (d: any) => (d._children ? "end" : "start"))
.text((d: any) => d.data.name)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white")
.attr("paint-order", "stroke");

// Transition nodes to their new position.
const nodeUpdate = node
.merge(nodeEnter)
.transition(transition as any)
.attr("transform", (d) => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);

// Transition exiting nodes to the parent's new position.
const nodeExit = node
.exit()
.transition(transition as any)
.remove()
.attr("transform", (d) => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);

// Update the links…
const link = gLink
.selectAll<SVGGElement, d3.HierarchyPointLink<TreeNode>>("path")
.data(links, (d) => d.target.id as string);

// Enter any new links at the parent's previous position.
const linkEnter = link
.enter()
.append("path")
.attr("d", (d) => {
const o: any = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});

// Transition links to their new position.
link
.merge(linkEnter as any)
.transition(transition as any)
.attr("d", diagonal as any);

// Transition exiting nodes to the parent's new position.
link
.exit()
.transition(transition as any)
.remove()
.attr("d", (d) => {
const o: any = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});

// Stash the old positions for transition.
root.eachBefore((d: any) => {
d.x0 = d.x;
d.y0 = d.y;
});
}

// Do the first update to the initial configuration of the tree — where a number of nodes
// are open (arbitrarily selected as the root, plus nodes with 7 letters).
(root as any).x0 = dy / 2;
(root as any).y0 = 0;
root.descendants().forEach((d: any, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});

update(null, root);

return svg.node();
}

function nestedTree(flatTree: TreeNode): TreeNode[] {
const root = new TreeNode("root", "placeholder");
flatTree.children.forEach((node) => {
const modules = node.name.split(".");
let currentNode = root;

modules.forEach((module, idx) => {
let child = currentNode.children.find((child) => child.name === module);
if (!child) {
currentNode.children = currentNode.children.filter(
(child) => child.type == "module"
);
child = new TreeNode(module, "module");
currentNode.children.push(child);
}

currentNode = child;
});

currentNode.children = node.children;
});
return root.children;
}

function createTree(ir: Morphir.IR.Distribution.Distribution) {
let packageName = ir.arg1.map((p) => p.map(capitalize).join(".")).join(".");
let tree: TreeNode = new TreeNode(packageName, "package");
Expand Down Expand Up @@ -146,13 +382,21 @@ function recursiveTypeFunction(

switch (distNode.kind) {
case "Reference":
treeNodes.push(
new TreeNode(toCamelCase(distNode.arg2[2]), distNode.kind)
);
if (!isMorphirSDK(distNode)) {
treeNodes.push(
new TreeNode(toCamelCase(distNode.arg2[2]), distNode.kind)
);
}
distNode.arg3.forEach((node) =>
treeNodes.push(...recursiveTypeFunction(node))
);
break;
case "Tuple":
treeNodes.push(
...recursiveTypeFunction(distNode.arg2[0]),
...recursiveTypeFunction(distNode.arg2[1])
);
break;
case "Record":
distNode.arg2.forEach((node) => {
let parentNode = new TreeNode(toCamelCase(node.name), node.tpe.kind);
Expand All @@ -166,7 +410,8 @@ function recursiveTypeFunction(
treeNodes.push(...parentNode);
break;
default:
console.log("Not yet covered: ", distNode.kind);
console.log("Unsupported type found: ", distNode.kind);
treeNodes.push(new TreeNode("Unknown Type", distNode.kind));
break;
}

Expand Down Expand Up @@ -218,22 +463,13 @@ function recursiveValueFunction(
treeNodes.push(...arg2IfDrilldown);
break;
case "PatternMatch":
if (distNode.arg2.kind == "Tuple") {
treeNodes.push(...recursiveValueFunction(distNode.arg2.arg2[0]));
treeNodes.push(...recursiveValueFunction(distNode.arg2.arg2[1]));
}
treeNodes.push(...recursiveValueFunction(distNode.arg2));
break;
case "Variable":
treeNodes.push(new TreeNode(toCamelCase(distNode.arg2), "Variable"));
break;
case "Reference":
if (
!(
toCamelCase(distNode.arg2[1][0]) == "basics" &&
toCamelCase(distNode.arg2[0][1]) == "sDK" &&
toCamelCase(distNode.arg2[0][0]) == "morphir"
)
) {
if (!isMorphirSDK(distNode)) {
//Stop normal operations from appearing in tree
treeNodes.push(
new TreeNode(toCamelCase(distNode.arg2[2]), "Reference")
Expand All @@ -245,7 +481,24 @@ function recursiveValueFunction(
break;
case "Literal":
break;
case "Constructor":
treeNodes.push(new TreeNode(toCamelCase(distNode.arg2[2]), "Reference"));
break;
case "List":
distNode.arg2.forEach((node) =>
treeNodes.push(...recursiveValueFunction(node))
);
break;
case "Tuple":
treeNodes.push(...recursiveValueFunction(distNode.arg2[0]));
treeNodes.push(...recursiveValueFunction(distNode.arg2[1]));
break;
case "Lambda":
break;
case "FieldFunction":
break;
default:
console.log("Unsupported type found:", distNode);
treeNodes.push(new TreeNode("Unknown Value", distNode.kind));
break;
}
Expand All @@ -272,3 +525,14 @@ function toCamelCase(array: string[]) {
function capitalize(str: string): string {
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}

function isMorphirSDK(
distNode: Morphir.IR.Type.Reference<{}> | Morphir.IR.Value.Reference<{}>
): boolean {
return (
distNode.arg2[0][1] &&
distNode.arg2[0][0] &&
toCamelCase(distNode.arg2[0][1]) == "sDK" &&
toCamelCase(distNode.arg2[0][0]) == "morphir"
);
}
Loading

0 comments on commit 652cd54

Please sign in to comment.