Refinements to basic graph editing

This commit is contained in:
Ladd Hoffman 2023-07-03 19:30:04 -05:00
parent 77d6698899
commit dd582c4d20
6 changed files with 92 additions and 13 deletions

View File

@ -3,7 +3,11 @@ import { MermaidDiagram } from './mermaid.js';
export class Flowchart extends MermaidDiagram {
constructor(box, logBox, direction = 'BT') {
super(box, logBox);
this.direction = direction;
this.init();
}
this.log(`graph ${direction}`, false);
init() {
this.log(`graph ${this.direction}`, false);
}
}

View File

@ -69,4 +69,8 @@ export class MermaidDiagram {
}
}, 100);
}
reset() {
this.logBoxPre.textContent = '';
}
}

View File

@ -61,8 +61,7 @@ export class Scene {
const container = this.middleSection.addBox(name).flex();
const box = container.addBox('Flowchart').addClass('padded');
const logBox = container.addBox('Flowchart text').addClass('dim');
const flowchart = new MermaidDiagram(box, logBox);
flowchart.log(`graph ${direction}`, false);
const flowchart = new Flowchart(box, logBox, direction);
this.flowcharts.set(id, flowchart);
return this;
}

View File

@ -7,6 +7,11 @@ export class Edge {
this.weight = weight;
this.data = data;
this.options = options;
this.installedClickCallback = false;
}
reset() {
this.installedClickCallback = false;
}
static getKey({
@ -49,8 +54,9 @@ export class Edge {
}
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
if (this.graph.editable) {
if (this.graph.editable && !this.installedClickCallback) {
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} "Edit Edge"`);
this.installedClickCallback = true;
}
}
@ -100,19 +106,17 @@ export class Edge {
name: 'Save',
cb: ({ form: { value } }, { initializing }) => {
if (initializing) return;
console.log('graph before save', { ...graph });
// Handle additions and updates
for (const { type, weight } of value.edges) {
graph.setEdgeWeight(type, from, to, weight);
}
// Handle removals
for (const edgeType of graph.edgeTypes.values()) {
if (!value.edges.find((edge) => edge.type === edgeType)) {
graph.deleteEdge(edgeType, from, to);
for (const edge of graph.getEdges(null, from, to)) {
if (!value.edges.find(({ type }) => type === edge.type)) {
graph.deleteEdge(edge.type, from, to);
}
}
console.log('graph after save', { ...graph });
// TODO: Redraw graph
graph.redraw();
},
});
}

View File

@ -12,6 +12,10 @@ export class Vertex {
this.installedClickCallback = false;
}
reset() {
this.installedClickCallback = false;
}
set id(newId) {
this._id = newId;
}

View File

@ -3,7 +3,8 @@ import { Edge } from './edge.js';
const graphs = [];
const makeWDGHandler = (graph) => (vertexId) => {
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;
@ -28,7 +29,62 @@ export class WeightedDirectedGraph {
// 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);
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() {
@ -88,6 +144,9 @@ export class WeightedDirectedGraph {
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) {
@ -112,13 +171,18 @@ export class WeightedDirectedGraph {
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;
}