99 lines
2.5 KiB
TypeScript
99 lines
2.5 KiB
TypeScript
import React, { useState } from "react";
|
|
import Viz from 'viz.js';
|
|
import { Module, render } from 'viz.js/full.render.js';
|
|
import { graphToDot } from "./Graphviz";
|
|
import * as d3 from 'd3';
|
|
import { NodeContextMenu } from "./NodeContextMenu";
|
|
|
|
const viz = new Viz({ Module, render });
|
|
|
|
export class GraphRenderer {
|
|
graph: Graph;
|
|
setGraph: React.Dispatch<React.SetStateAction<Graph>>;
|
|
containerRef: React.RefObject<null>;
|
|
contextMenu: NodeContextMenu;
|
|
|
|
constructor(containerRef: React.RefObject<null>) {
|
|
[this.graph, this.setGraph] = useState(defaultGraph());
|
|
this.containerRef = containerRef;
|
|
this.contextMenu = new NodeContextMenu(containerRef);
|
|
}
|
|
|
|
public async render() {
|
|
const dot = graphToDot(this.graph);
|
|
|
|
try {
|
|
const svgElement = await viz.renderSVGElement(dot, { engine: 'dot' });
|
|
const container = this.containerRef.current;
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
container.appendChild(svgElement);
|
|
this.attachInteractions(svgElement);
|
|
} catch (e) {
|
|
console.error('Viz render error', e);
|
|
}
|
|
}
|
|
|
|
attachInteractions(svgElement: SVGSVGElement) {
|
|
const svg = d3.select(svgElement);
|
|
const self = this;
|
|
svg.selectAll('g.node')
|
|
.style('cursor', 'pointer')
|
|
.on('click', function (event) {
|
|
event.stopPropagation();
|
|
const id = d3.select(this).attr('id');
|
|
self.createChildNode(id);
|
|
self.render();
|
|
})
|
|
.on('contextmenu', function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const id = d3.select(this).attr('id');
|
|
const { clientX: x, clientY: y } = event;
|
|
self.contextMenu.setContextMenu({ x, y, nodeId: id });
|
|
});
|
|
}
|
|
|
|
createChildNode(parentId: string) {
|
|
const id = crypto.randomUUID();
|
|
this.setGraph(prev => ({ ...prev, nodes: [...prev.nodes, { id, label: 'New node' }], edges: [...prev.edges, { from: parentId, to: id }] }));
|
|
}
|
|
}
|
|
|
|
function defaultGraph(): Graph {
|
|
return {
|
|
nodes: [
|
|
{ id: 'A', label: 'A' },
|
|
{ id: 'B', label: 'B' },
|
|
{ id: 'C', label: 'C' }
|
|
],
|
|
edges: [
|
|
{ from: 'A', to: 'B' },
|
|
{ from: 'B', to: 'C' }
|
|
]
|
|
};
|
|
}
|
|
|
|
export class Graph {
|
|
nodes: Node[] = [];
|
|
edges: Edge[] = [];
|
|
}
|
|
|
|
export class Edge {
|
|
from: string;
|
|
to: string;
|
|
|
|
constructor(from: string, to: string) {
|
|
this.from = from;
|
|
this.to = to;
|
|
}
|
|
}
|
|
|
|
export class Node {
|
|
public id: string;
|
|
public label?: string;
|
|
|
|
constructor(id: string) {
|
|
this.id = id;
|
|
}
|
|
} |