Basic graph editing

Also renamed WDAG -> WDG
This commit is contained in:
Ladd Hoffman 2023-07-03 17:03:16 -05:00
parent 498b5c106f
commit 77d6698899
14 changed files with 382 additions and 107 deletions

View File

@ -1,4 +1,4 @@
import { WDAG } from '../supporting/wdag.js'; import { WeightedDirectedGraph } from '../supporting/wdg.js';
import { Action } from '../display/action.js'; import { Action } from '../display/action.js';
import { Actor } from '../display/actor.js'; import { Actor } from '../display/actor.js';
import { ReputationHolder } from '../reputation/reputation-holder.js'; import { ReputationHolder } from '../reputation/reputation-holder.js';
@ -51,7 +51,7 @@ export class Forum extends ReputationHolder {
super(name, scene); super(name, scene);
this.dao = dao; this.dao = dao;
this.id = this.reputationPublicKey; this.id = this.reputationPublicKey;
this.graph = new WDAG(scene); this.graph = new WeightedDirectedGraph(scene);
this.actions = { this.actions = {
propagate: new Action('propagate', scene), propagate: new Action('propagate', scene),
confirm: new Action('confirm', scene), confirm: new Action('confirm', scene),

View File

@ -2,8 +2,8 @@ import { Box } from './box.js';
import { Form } from './form.js'; import { Form } from './form.js';
export class Remark extends Box { export class Remark extends Box {
constructor(doc, text, opts) { constructor(doc, text, opts = {}) {
super('Remark', doc.el, opts); super('Remark', opts.parentEl ?? doc.el, opts);
this.setInnerHTML(text); this.setInnerHTML(text);
} }
} }
@ -16,8 +16,10 @@ export class Remark extends Box {
* ``` * ```
*/ */
export class Document extends Box { export class Document extends Box {
form() { elements = [];
return this.addElement(new Form(this));
form(opts) {
return this.addElement(new Form(this, opts));
} }
remark(text, opts) { remark(text, opts) {
@ -25,13 +27,17 @@ export class Document extends Box {
} }
addElement(element) { addElement(element) {
this.elements = this.elements ?? [];
this.elements.push(element); this.elements.push(element);
return this; return this;
} }
clear() {
this.el.innerHTML = '';
this.elements = [];
}
get lastElement() { get lastElement() {
if (!this.elements?.length) return null; if (!this.elements.length) return null;
return this.elements[this.elements.length - 1]; return this.elements[this.elements.length - 1];
} }
} }

View File

@ -4,13 +4,16 @@ import { Box } from './box.js';
const updateValuesOnEventTypes = ['keyup', 'mouseup']; const updateValuesOnEventTypes = ['keyup', 'mouseup'];
export class FormElement extends Box { export class FormElement extends Box {
constructor(name, parentEl, opts) { constructor(name, form, opts) {
const parentEl = opts.parentEl ?? form.el;
super(name, parentEl, opts); super(name, parentEl, opts);
this.form = form;
this.id = opts.id ?? name; this.id = opts.id ?? name;
this.includeInOutput = opts.includeInOutput ?? true;
const { cb } = opts; const { cb } = opts;
if (cb) { if (cb) {
updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => { updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => {
cb(this); cb(this, { initializing: false });
})); }));
cb(this, { initializing: true }); cb(this, { initializing: true });
} }
@ -18,21 +21,26 @@ export class FormElement extends Box {
} }
export class Button extends FormElement { export class Button extends FormElement {
constructor(name, parentEl, opts) { constructor(name, form, opts) {
super(name, parentEl, opts); super(name, form, opts);
this.button = document.createElement('button'); this.button = document.createElement('button');
this.button.setAttribute('type', 'button'); this.button.setAttribute('type', 'button');
this.button.innerHTML = name; this.button.innerHTML = name;
this.button.disabled = !!opts.disabled;
this.el.appendChild(this.button); this.el.appendChild(this.button);
} }
} }
export class TextField extends FormElement { export class TextField extends FormElement {
constructor(name, parentEl, opts) { constructor(name, form, opts) {
super(name, parentEl, opts); super(name, form, opts);
this.label = document.createElement('label'); this.label = document.createElement('label');
this.label.innerHTML = name; this.labelDiv = document.createElement('div');
this.label.appendChild(this.labelDiv);
this.labelDiv.innerHTML = name;
this.input = document.createElement('input'); this.input = document.createElement('input');
this.input.disabled = !!opts.disabled;
this.input.defaultValue = opts.defaultValue || '';
this.label.appendChild(this.input); this.label.appendChild(this.input);
this.el.appendChild(this.label); this.el.appendChild(this.label);
} }
@ -44,25 +52,83 @@ export class TextField extends FormElement {
export class TextArea extends FormElement { } export class TextArea extends FormElement { }
export class Form { export class SubFormArray extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
this.subForms = [];
}
get value() {
return this.subForms.map((subForm) => subForm.value);
}
remove(subForm) {
const idx = this.subForms.findIndex((s) => s === subForm);
this.subForms.splice(idx, 1);
subForm.el.remove();
}
}
export class SubForm extends FormElement {
constructor(name, form, opts) {
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
const subForm = form.document.form({ name, parentEl }).lastElement;
super(name, form, { ...opts, parentEl });
this.subForm = subForm;
if (opts.subFormArray) {
opts.subFormArray.subForms.push(this);
this.includeInOutput = false;
}
}
get value() {
return this.subForm.value;
}
}
export class Form extends Box {
constructor(document, opts = {}) { constructor(document, opts = {}) {
super(opts.name, opts.parentEl || document.el, opts);
this.document = document; this.document = document;
this.items = []; this.items = [];
this.id = opts.id ?? `form_${randomID()}`; this.id = opts.id ?? `form_${randomID()}`;
} }
button(opts) { button(opts) {
this.items.push(new Button(opts.name, this.document.el, opts)); this.items.push(new Button(opts.name, this, opts));
return this; return this;
} }
textField(opts) { textField(opts) {
this.items.push(new TextField(opts.name, this.document.el, opts)); this.items.push(new TextField(opts.name, this, opts));
return this; return this;
} }
textArea(opts) { textArea(opts) {
this.items.push(new TextArea(opts.name, this.document.el, opts)); this.items.push(new TextArea(opts.name, this, opts));
return this; return this;
} }
subForm(opts) {
this.items.push(new SubForm(opts.name, this, opts));
return this;
}
subFormArray(opts) {
this.items.push(new SubFormArray(opts.name, this, opts));
return this;
}
get lastItem() {
return this.items[this.items.length - 1];
}
get value() {
return this.items.reduce((result, { id, value, includeInOutput }) => {
if (includeInOutput && value !== undefined) {
result[id] = value;
}
return result;
}, {});
}
} }

View File

@ -29,6 +29,8 @@ export class MermaidDiagram {
activationBkgColor: '#1d3f49', activationBkgColor: '#1d3f49',
activationBorderColor: '#569595', activationBorderColor: '#569595',
}, },
securityLevel: 'loose', // 'loose' so that we can use click events
// logLevel: 'debug',
}); });
} }
@ -51,11 +53,16 @@ export class MermaidDiagram {
return debounce(async () => { return debounce(async () => {
const text = this.getText(); const text = this.getText();
try { try {
const graph = await mermaid.mermaidAPI.render( await mermaid.mermaidAPI.render(
this.element.getId(), this.element.getId(),
text, text,
(svgCode, bindFunctions) => {
this.renderBox.setInnerHTML(svgCode);
if (bindFunctions) {
bindFunctions(this.renderBox.el);
}
},
); );
this.renderBox.setInnerHTML(graph);
} catch (e) { } catch (e) {
console.error(`render text:\n${text}`); console.error(`render text:\n${text}`);
throw e; throw e;

View File

@ -21,6 +21,7 @@ export class Scene {
this.actors = new Set(); this.actors = new Set();
this.dateStart = new Date(); this.dateStart = new Date();
this.flowcharts = new Map(); this.flowcharts = new Map();
this.documents = [];
MermaidDiagram.initializeAPI(); MermaidDiagram.initializeAPI();
@ -105,6 +106,10 @@ export class Scene {
return this.documents[this.documents.length - 1]; return this.documents[this.documents.length - 1];
} }
getDocument(name) {
return this.documents.find((doc) => doc.name === name);
}
registerActor(actor) { registerActor(actor) {
this.actors.add(actor); this.actors.add(actor);
if (actor.options.announce) { if (actor.options.announce) {

View File

@ -0,0 +1,119 @@
export class Edge {
constructor(graph, type, from, to, weight, data, options = {}) {
this.graph = graph;
this.from = from;
this.to = to;
this.type = type;
this.weight = weight;
this.data = data;
this.options = options;
}
static getKey({
from, to, type,
}) {
return ['edge', from.id, to.id, type].join(':');
}
static getCombinedKey({ from, to }) {
return ['edge', from.id, to.id].join(':');
}
getComorphicEdges() {
return this.graph.getEdges(null, this.from, this.to);
}
getHtml() {
let html = '<table>';
for (const { type, weight } of this.getComorphicEdges()) {
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
}
html += '</table>';
return html;
}
getFlowchartNode() {
return `${Edge.getCombinedKey(this)}(${this.getHtml()})`;
}
displayEdgeNode() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(this.getFlowchartNode());
}
displayEdge() {
if (this.options.hide) {
return;
}
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) {
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} "Edit Edge"`);
}
}
static prepareEditorDocument(graph, doc, from, to) {
doc.clear();
const form = doc.form({ name: 'editorForm' }).lastElement;
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
form
.textField({
id: 'from', name: 'from', defaultValue: from, disabled: true,
})
.textField({
id: 'to', name: 'to', defaultValue: to, disabled: true,
});
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
const addEdgeForm = (edge) => {
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
doc.remark('<br/>', { parentEl: subForm.el });
subForm.textField({ id: 'type', name: 'type', defaultValue: edge.type });
subForm.textField({ id: 'weight', name: 'weight', defaultValue: edge.weight });
subForm.button({
id: 'remove',
name: 'Remove Edge Type',
cb: (_, { initializing }) => {
if (initializing) return;
subFormArray.remove(subForm);
},
});
};
for (const edge of graph.getEdges(null, from, to)) {
addEdgeForm(edge);
}
form.button({
id: 'add',
name: 'Add Edge Type',
cb: (_, { initializing }) => {
if (initializing) return;
addEdgeForm(new Edge(graph, null, graph.getVertex(from), graph.getVertex(to)));
},
});
form.button({
id: 'save',
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);
}
}
console.log('graph after save', { ...graph });
// TODO: Redraw graph
},
});
}
}

View File

@ -0,0 +1,82 @@
export class Vertex {
constructor(graph, type, id, data, options = {}) {
this.graph = graph;
this.type = type;
this._id = id;
this.data = data;
this.options = options;
this.edges = {
from: [],
to: [],
};
this.installedClickCallback = false;
}
set id(newId) {
this._id = newId;
}
get id() {
return this._id;
}
getEdges(type, away) {
return this.edges[away ? 'from' : 'to'].filter(
(edge) => edge.type === type,
);
}
setDisplayLabel(label) {
this.label = label;
this.displayVertex();
}
displayVertex() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(`${this.id}[${this.label}]`);
if (this.graph.editable && !this.installedClickCallback) {
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex"`);
this.installedClickCallback = true;
}
}
static prepareEditorDocument(graph, doc, vertexId) {
doc.clear();
const vertex = graph.getVertex(vertexId);
if (!vertex) {
throw new Error(`Could not find WDG Vertex ${vertexId}`);
}
doc.remark('<h3>Edit Vertex</h3>');
const form = doc.form().lastElement;
form
.textField({
id: 'id', name: 'id', defaultValue: vertex.id, disabled: true,
})
.textField({ id: 'type', name: 'type', defaultValue: vertex.type })
.textField({ id: 'label', name: 'label', defaultValue: vertex.label })
.button({
id: 'save',
name: 'Save',
cb: ({ form: { value } }, { initializing }) => {
if (initializing) return;
if (value.id && value.id !== vertex.id) {
// TODO: When an ID changes we really need to wipe out and redraw!
// But we don't yet have a systematic approach for doing that.
// Everything is getting rendered as needed. Lacking abstraction.
// HMM we're not actually that far! Just wipe everything out and draw each vertex and edge :)
// for (const vertex of )
// for (const edge of [...vertex.edges.to, ...vertex.edges.from]) {
// edge.displayEdge();
// }
}
Object.assign(vertex, value);
vertex.displayVertex();
},
});
return doc;
}
}

View File

@ -1,79 +1,34 @@
const getEdgeKey = ({ from, to }) => btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, ''); import { Vertex } from './vertex.js';
import { Edge } from './edge.js';
export class Vertex { const graphs = [];
constructor(graph, type, id, data, options = {}) {
this.graph = graph;
this.type = type;
this.id = id;
this.data = data;
this.options = options;
this.edges = {
from: [],
to: [],
};
}
getEdges(type, away) { const makeWDGHandler = (graph) => (vertexId) => {
return this.edges[away ? 'from' : 'to'].filter( // We want a document for editing this node, which may be a vertex or an edge
(edge) => edge.type === type, 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);
} }
};
setDisplayLabel(label) { export class WeightedDirectedGraph {
if (this.options.hide) { constructor(scene, options = {}) {
return;
}
this.graph.flowchart?.log(`${this.id}[${label}]`);
}
}
export class Edge {
constructor(graph, type, from, to, weight, data, options = {}) {
this.graph = graph;
this.from = from;
this.to = to;
this.type = type;
this.weight = weight;
this.data = data;
this.options = options;
}
getHtml() {
let html = '<table>';
for (const { type, weight } of this.graph.getEdges(null, this.from, this.to)) {
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
}
html += '</table>';
return html;
}
getFlowchartNode() {
return `${getEdgeKey(this)}(${this.getHtml()})`;
}
displayEdgeNode() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(this.getFlowchartNode());
}
displayEdge() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
this.graph.flowchart?.log(`class ${getEdgeKey(this)} edge`);
}
}
export class WDAG {
constructor(scene) {
this.scene = scene; this.scene = scene;
this.vertices = new Map(); this.vertices = new Map();
this.edgeTypes = new Map(); this.edgeTypes = new Map();
this.nextVertexId = 0; this.nextVertexId = 0;
this.flowchart = scene?.flowchart; 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);
} }
withFlowchart() { withFlowchart() {
@ -83,11 +38,12 @@ export class WDAG {
} }
addVertex(type, id, data, label, options) { addVertex(type, id, data, label, options) {
// Support simple case of auto-incremented numeric ids // Supports simple case of auto-incremented numeric ids
if (typeof id === 'object') { if (typeof id === 'object') {
data = id; data = id;
id = this.nextVertexId++; id = this.nextVertexId++;
} }
id = (typeof id === 'number') ? id.toString(10) : id;
if (this.vertices.has(id)) { if (this.vertices.has(id)) {
throw new Error(`Vertex already exists with id: ${id}`); throw new Error(`Vertex already exists with id: ${id}`);
} }
@ -98,6 +54,7 @@ export class WDAG {
} }
getVertex(id) { getVertex(id) {
id = (typeof id === 'number') ? id.toString(10) : id;
return this.vertices.get(id); return this.vertices.get(id);
} }
@ -116,10 +73,23 @@ export class WDAG {
return undefined; return undefined;
} }
const edges = this.edgeTypes.get(type); const edges = this.edgeTypes.get(type);
const edgeKey = getEdgeKey({ from, to }); const edgeKey = Edge.getKey({ from, to, type });
return edges?.get(edgeKey); 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);
}
getEdgeWeight(type, from, to) { getEdgeWeight(type, from, to) {
return this.getEdge(type, from, to)?.weight; return this.getEdge(type, from, to)?.weight;
} }
@ -133,7 +103,7 @@ export class WDAG {
edges = new Map(); edges = new Map();
this.edgeTypes.set(type, edges); this.edgeTypes.set(type, edges);
} }
const edgeKey = getEdgeKey(edge); const edgeKey = Edge.getKey(edge);
edges.set(edgeKey, edge); edges.set(edgeKey, edge);
edge.displayEdgeNode(); edge.displayEdgeNode();
return edge; return edge;
@ -172,6 +142,4 @@ export class WDAG {
} }
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length; return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
} }
// TODO: Add support for inputs to add/edit vertices and edges
} }

View File

@ -79,3 +79,7 @@ label {
font-size: smaller; font-size: smaller;
color: #999999; color: #999999;
} }
label > div {
display: inline-block;
min-width: 50px;
}

View File

@ -55,7 +55,7 @@
<ul> <ul>
<li><a href="./tests/basic.test.html">Basic Sequencing</a></li> <li><a href="./tests/basic.test.html">Basic Sequencing</a></li>
<li><a href="./tests/basic2.test.html">Basic Sequencing 2</a></li> <li><a href="./tests/basic2.test.html">Basic Sequencing 2</a></li>
<li><a href="./tests/wdag.test.html">WDAG</a></li> <li><a href="./tests/wdg.test.html">Weighted Directed Graph</a></li>
<li><a href="./tests/debounce.test.html">Debounce</a></li> <li><a href="./tests/debounce.test.html">Debounce</a></li>
<li><a href="./tests/flowchart.test.html">Flowchart</a></li> <li><a href="./tests/flowchart.test.html">Flowchart</a></li>
<li><a href="./tests/mocha.test.html">Mocha</a></li> <li><a href="./tests/mocha.test.html">Mocha</a></li>

View File

@ -25,7 +25,7 @@
<script type="module" src="./scripts/mocha.test.js"></script> <script type="module" src="./scripts/mocha.test.js"></script>
<script type="module" src="./scripts/validation-pool.test.js"></script> <script type="module" src="./scripts/validation-pool.test.js"></script>
<script type="module" src="./scripts/vm.test.js"></script> <script type="module" src="./scripts/vm.test.js"></script>
<script type="module" src="./scripts/wdag.test.js"></script> <script type="module" src="./scripts/wdg.test.js"></script>
<script type="module" src="./scripts/forum/forum1.test.js"></script> <script type="module" src="./scripts/forum/forum1.test.js"></script>
<script type="module" src="./scripts/forum/forum2.test.js"></script> <script type="module" src="./scripts/forum/forum2.test.js"></script>
<script type="module" src="./scripts/forum/forum3.test.js"></script> <script type="module" src="./scripts/forum/forum3.test.js"></script>

View File

@ -45,7 +45,7 @@ describe('Document > Form > Button', () => {
const form = doc.lastElement; const form = doc.lastElement;
const dvMap = new Map(); const dvMap = new Map();
let clicks = 0; let clicks = 0;
const handleClick = ({ id, name }, { initializing = false } = {}) => { const handleClick = ({ id, name }, { initializing }) => {
const dv = dvMap.get(id) ?? scene.addDisplayValue(name); const dv = dvMap.get(id) ?? scene.addDisplayValue(name);
dvMap.set(id, dv); dvMap.set(id, dv);
if (!initializing) { if (!initializing) {

View File

@ -1,19 +1,19 @@
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { WDAG } from '../../classes/supporting/wdag.js'; import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
import { mochaRun } from '../../util/helpers.js'; import { mochaRun } from '../../util/helpers.js';
const rootElement = document.getElementById('scene'); const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene('WDAG test', rootBox); window.scene = new Scene('WDG test', rootBox);
describe('Query the graph', function tests() { describe('Weighted Directed Acyclic Graph', function tests() {
this.timeout(0); this.timeout(0);
let graph; let graph;
before(() => { before(() => {
graph = (window.graph = new WDAG()).withFlowchart(); graph = (window.graph = new WeightedDirectedGraph(window.scene)).withFlowchart();
graph.addVertex('v1', {}); graph.addVertex('v1', {});
graph.addVertex('v1', {}); graph.addVertex('v1', {});
@ -34,17 +34,35 @@ describe('Query the graph', function tests() {
it('can query for all e1 edges from a particular vertex', () => { it('can query for all e1 edges from a particular vertex', () => {
const edges = graph.getEdges('e1', 2); const edges = graph.getEdges('e1', 2);
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([[2, 1, 0.5]]); edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([[
'2', '1', 0.5,
]]);
}); });
it('can query for all e1 edges to a particular vertex', () => { it('can query for all e1 edges to a particular vertex', () => {
const edges = graph.getEdges('e1', null, 1); const edges = graph.getEdges('e1', null, 1);
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([ edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([
[0, 1, 1], ['0', '1', 1],
[2, 1, 0.5], ['2', '1', 0.5],
[3, 1, 0.25], ['3', '1', 0.25],
]); ]);
}); });
}); });
describe('editable', () => {
let graph;
it('should be editable', () => {
graph = (window.graph2 = new WeightedDirectedGraph(window.scene, { editable: true })).withFlowchart();
graph.addVertex('v1', {});
graph.addVertex('v2', {});
graph.addVertex('v3', {});
graph.addEdge('e1', 2, 1, 1);
graph.addEdge('e2', 1, 0, 0.5);
graph.addEdge('e3', 2, 0, 0.25);
});
});
mochaRun(); mochaRun();

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<title>WDAG test</title> <title>Weighted Directed Graph test</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" /> <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
<link type="text/css" rel="stylesheet" href="../index.css" /> <link type="text/css" rel="stylesheet" href="../index.css" />
@ -24,4 +24,4 @@
}); });
chai.should(); chai.should();
</script> </script>
<script type="module" src="./scripts/wdag.test.js"></script> <script type="module" src="./scripts/wdg.test.js"></script>