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>; containerRef: React.RefObject; contextMenu: NodeContextMenu; constructor(containerRef: React.RefObject) { [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; } }