Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapsible Treeview: Initial Visualisation #1215

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading