Refinements to basic graph editing
This commit is contained in:
parent
77d6698899
commit
dd582c4d20
|
@ -3,7 +3,11 @@ import { MermaidDiagram } from './mermaid.js';
|
||||||
export class Flowchart extends MermaidDiagram {
|
export class Flowchart extends MermaidDiagram {
|
||||||
constructor(box, logBox, direction = 'BT') {
|
constructor(box, logBox, direction = 'BT') {
|
||||||
super(box, logBox);
|
super(box, logBox);
|
||||||
|
this.direction = direction;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
this.log(`graph ${direction}`, false);
|
init() {
|
||||||
|
this.log(`graph ${this.direction}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,4 +69,8 @@ export class MermaidDiagram {
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.logBoxPre.textContent = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,8 +61,7 @@ export class Scene {
|
||||||
const container = this.middleSection.addBox(name).flex();
|
const container = this.middleSection.addBox(name).flex();
|
||||||
const box = container.addBox('Flowchart').addClass('padded');
|
const box = container.addBox('Flowchart').addClass('padded');
|
||||||
const logBox = container.addBox('Flowchart text').addClass('dim');
|
const logBox = container.addBox('Flowchart text').addClass('dim');
|
||||||
const flowchart = new MermaidDiagram(box, logBox);
|
const flowchart = new Flowchart(box, logBox, direction);
|
||||||
flowchart.log(`graph ${direction}`, false);
|
|
||||||
this.flowcharts.set(id, flowchart);
|
this.flowcharts.set(id, flowchart);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,11 @@ export class Edge {
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getKey({
|
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(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
||||||
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
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.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',
|
name: 'Save',
|
||||||
cb: ({ form: { value } }, { initializing }) => {
|
cb: ({ form: { value } }, { initializing }) => {
|
||||||
if (initializing) return;
|
if (initializing) return;
|
||||||
console.log('graph before save', { ...graph });
|
|
||||||
// Handle additions and updates
|
// Handle additions and updates
|
||||||
for (const { type, weight } of value.edges) {
|
for (const { type, weight } of value.edges) {
|
||||||
graph.setEdgeWeight(type, from, to, weight);
|
graph.setEdgeWeight(type, from, to, weight);
|
||||||
}
|
}
|
||||||
// Handle removals
|
// Handle removals
|
||||||
for (const edgeType of graph.edgeTypes.values()) {
|
for (const edge of graph.getEdges(null, from, to)) {
|
||||||
if (!value.edges.find((edge) => edge.type === edgeType)) {
|
if (!value.edges.find(({ type }) => type === edge.type)) {
|
||||||
graph.deleteEdge(edgeType, from, to);
|
graph.deleteEdge(edge.type, from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('graph after save', { ...graph });
|
graph.redraw();
|
||||||
// TODO: Redraw graph
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,10 @@ export class Vertex {
|
||||||
this.installedClickCallback = false;
|
this.installedClickCallback = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
set id(newId) {
|
set id(newId) {
|
||||||
this._id = newId;
|
this._id = newId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Edge } from './edge.js';
|
||||||
|
|
||||||
const graphs = [];
|
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
|
// We want a document for editing this node, which may be a vertex or an edge
|
||||||
const editorDoc = graph.scene.getDocument('editorDocument')
|
const editorDoc = graph.scene.getDocument('editorDocument')
|
||||||
?? graph.scene.withDocument('editorDocument').lastDocument;
|
?? 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.
|
// 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
|
// In order to provide the appropriate graph context for each callback, we create a separate callback
|
||||||
// function for each graph.
|
// 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() {
|
withFlowchart() {
|
||||||
|
@ -88,6 +144,9 @@ export class WeightedDirectedGraph {
|
||||||
to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1));
|
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));
|
from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1));
|
||||||
edges.delete(edgeKey);
|
edges.delete(edgeKey);
|
||||||
|
if (edges.size === 0) {
|
||||||
|
this.edgeTypes.delete(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgeWeight(type, from, to) {
|
getEdgeWeight(type, from, to) {
|
||||||
|
@ -112,13 +171,18 @@ export class WeightedDirectedGraph {
|
||||||
addEdge(type, from, to, weight, data, options) {
|
addEdge(type, from, to, weight, data, options) {
|
||||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const existingEdges = this.getEdges(type, from, to);
|
||||||
if (this.getEdge(type, from, to)) {
|
if (this.getEdge(type, from, to)) {
|
||||||
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
||||||
}
|
}
|
||||||
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
||||||
from.edges.from.push(edge);
|
from.edges.from.push(edge);
|
||||||
to.edges.to.push(edge);
|
to.edges.to.push(edge);
|
||||||
|
if (existingEdges.length) {
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
} else {
|
||||||
edge.displayEdge();
|
edge.displayEdge();
|
||||||
|
}
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue