import { Vertex } from './vertex.js'; import { Edge } from './edge.js'; const graphs = []; const makeWDGHandler = (graphIndex) => (vertexId) => { const graph = graphs[graphIndex]; // We want a document for editing this node, which may be a vertex or an edge const editorDoc = graph.scene.getDocument('editorDocument') ?? graph.scene.withDocument('editorDocument').lastDocument; if (vertexId.startsWith('edge:')) { const [, from, to] = vertexId.split(':'); Edge.prepareEditorDocument(graph, editorDoc, from, to); } else { Vertex.prepareEditorDocument(graph, editorDoc, vertexId); } }; export class WeightedDirectedGraph { constructor(scene, options = {}) { this.scene = scene; this.vertices = new Map(); this.edgeTypes = new Map(); this.nextVertexId = 0; this.flowchart = scene?.flowchart; this.editable = options.editable; this.index = graphs.length; graphs.push(this); // Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID. // In order to provide the appropriate graph context for each callback, we create a separate callback // function for each graph. window[`WDGHandler${this.index}`] = makeWDGHandler(this.index); this.history = {}; } getHistory() { // record operations that modify the graph return this.history; } toJSON() { return { vertices: Array.from(this.vertices.values()), edgeTypes: Array.from(this.edgeTypes.keys()), edges: Array.from(this.edgeTypes.values()).flatMap((edges) => Array.from(edges.values())), history: this.getHistory(), }; } redraw() { // Call .reset() on all vertices and edges for (const vertex of this.vertices.values()) { vertex.reset(); } for (const edges of this.edgeTypes.values()) { for (const edge of edges.values()) { edge.reset(); } } // Clear the target div this.flowchart?.reset(); this.flowchart?.init(); // Draw all vertices and edges for (const vertex of this.vertices.values()) { vertex.displayVertex(); } // Let's flatmap and dedupe by [from, to] since each edge // renders all comorphic edges as well. const edgesFrom = new Map(); // edgeMap[from][to] = edge for (const edges of this.edgeTypes.values()) { for (const edge of edges.values()) { const edgesTo = edgesFrom.get(edge.from) || new Map(); edgesTo.set(edge.to, edge); edgesFrom.set(edge.from, edgesTo); } } for (const edgesTo of edgesFrom.values()) { for (const edge of edgesTo.values()) { edge.displayEdge(); } } // Rerender this.flowchart?.render(); } withFlowchart() { this.scene?.withAdditionalFlowchart(); this.flowchart = this.scene?.lastFlowchart(); return this; } addVertex(type, id, data, label, options) { // Supports simple case of auto-incremented numeric ids if (typeof id === 'object') { data = id; id = this.nextVertexId++; } id = (typeof id === 'number') ? id.toString(10) : id; if (this.vertices.has(id)) { throw new Error(`Vertex already exists with id: ${id}`); } const vertex = new Vertex(this, type, id, data, options); this.vertices.set(id, vertex); vertex.setDisplayLabel(label ?? id); return vertex; } getVertex(id) { id = (typeof id === 'number') ? id.toString(10) : id; return this.vertices.get(id); } getVertexData(id) { return this.getVertex(id)?.data; } getVerticesData() { return Array.from(this.vertices.values()).map(({ data }) => data); } getEdge(type, from, to) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); if (!from || !to) { return undefined; } const edges = this.edgeTypes.get(type); const edgeKey = Edge.getKey({ from, to, type }); return edges?.get(edgeKey); } deleteEdge(type, from, to) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); const edges = this.edgeTypes.get(type); const edgeKey = Edge.getKey({ type, from, to }); if (!edges) return; const edge = edges.get(edgeKey); if (!edge) return; to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1)); from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1)); edges.delete(edgeKey); if (edges.size === 0) { this.edgeTypes.delete(type); } } getEdgeWeight(type, from, to) { return this.getEdge(type, from, to)?.weight; } setEdgeWeight(type, from, to, weight, data, options) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); const edge = new Edge(this, type, from, to, weight, data, options); let edges = this.edgeTypes.get(type); if (!edges) { edges = new Map(); this.edgeTypes.set(type, edges); } const edgeKey = Edge.getKey(edge); edges.set(edgeKey, edge); edge.displayEdgeNode(); return edge; } addEdge(type, from, to, weight, data, options) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); const existingEdges = this.getEdges(type, from, to); if (this.getEdge(type, from, to)) { throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`); } const edge = this.setEdgeWeight(type, from, to, weight, data, options); from.edges.from.push(edge); to.edges.to.push(edge); if (existingEdges.length) { edge.displayEdgeNode(); } else { edge.displayEdge(); } return edge; } getEdges(type, from, to) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys()); return edgeTypes.flatMap((edgeType) => { const edges = this.edgeTypes.get(edgeType); return Array.from(edges?.values() || []).filter((edge) => { const matchFrom = from === null || from === undefined || from === edge.from; const matchTo = to === null || to === undefined || to === edge.to; return matchFrom && matchTo; }); }); } countVertices(type) { if (!type) { return this.vertices.size; } return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length; } }