initial commit
This commit is contained in:
99
src/Graph.tsx
Normal file
99
src/Graph.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user