Improvements to graph rendering
This commit is contained in:
parent
4026d7eaa8
commit
7388a15cff
|
@ -69,6 +69,7 @@ export class Box {
|
|||
}
|
||||
|
||||
move(vector) {
|
||||
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||
this.position = this.position.add(vector);
|
||||
this.el.style.left = `${Math.floor(this.position[0])}px`;
|
||||
this.el.style.top = `${Math.floor(this.position[1])}px`;
|
||||
|
|
|
@ -3,16 +3,19 @@ import {
|
|||
MINIMUM_VELOCITY,
|
||||
VISCOSITY_FACTOR,
|
||||
TIME_DILATION_FACTOR,
|
||||
MAX_STEPS_TO_EQUILIBRIUM,
|
||||
MAXIMUM_STEPS,
|
||||
TRANSLATION_VELOCITY_FACTOR,
|
||||
ARROWHEAD_LENGTH,
|
||||
ARROWHEAD_WIDTH,
|
||||
MINIMUM_STEPS,
|
||||
CENTRAL_RESTORING_FORCE,
|
||||
} from '../../util/constants.js';
|
||||
import { Edge } from '../supporting/edge.js';
|
||||
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||
import { Box } from './box.js';
|
||||
import { Vector, Rectangle } from '../supporting/geometry/index.js';
|
||||
import { overlapRepulsionForce, targetRadiusForce } from './pairwise-forces.js';
|
||||
import { Vertex } from '../supporting/vertex.js';
|
||||
|
||||
// Render children with absolute css positioning.
|
||||
|
||||
|
@ -60,7 +63,6 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
[this.canvas.width, this.canvas.height] = this.box.rect.dimensions;
|
||||
}
|
||||
|
||||
// addVertex(type, id, data, label, options) {
|
||||
addVertex(...args) {
|
||||
const vertex = super.addVertex(...args);
|
||||
const box = this.box.addBox(vertex.id);
|
||||
|
@ -89,6 +91,7 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
|
||||
// Allow moving vertices with the mouse
|
||||
box.el.addEventListener('mousedown', (e) => {
|
||||
console.log('mousedown, button:', e.button);
|
||||
if (!this.mouseMoving) {
|
||||
e.preventDefault();
|
||||
// Record current mouse position
|
||||
|
@ -119,10 +122,10 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
}
|
||||
|
||||
addEdge(type, from, to, ...rest) {
|
||||
const fromBox = this.nodes.find(({ name }) => name === from);
|
||||
const toBox = this.nodes.find(({ name }) => name === to);
|
||||
if (!fromBox) throw new Error(`from ${from}: Node not found`);
|
||||
if (!toBox) throw new Error(`to ${to}: Node not found`);
|
||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||
if (!from) throw new Error(`from ${from}: Node not found`);
|
||||
if (!to) throw new Error(`to ${to}: Node not found`);
|
||||
|
||||
const edge = super.addEdge(type, from, to, ...rest);
|
||||
const box = this.box.addBox(Edge.getKey({ from, to, type }));
|
||||
|
@ -136,7 +139,7 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
this.edges.push(box);
|
||||
|
||||
// Initially place near the midpoint between the `from` and `to` nodes.
|
||||
const midpoint = fromBox.rect.center.add(toBox.rect.center).scale(0.5);
|
||||
const midpoint = from.box.rect.center.add(to.box.rect.center).scale(0.5);
|
||||
const startPosition = midpoint.subtract(box.rect.dimensions.scale(0.5));
|
||||
box.move(startPosition);
|
||||
box.velocity = Vector.from([0, 0]);
|
||||
|
@ -163,6 +166,7 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
ctx.stroke();
|
||||
|
||||
const direction = edge.to.box.rect.center.subtract(edge.box.rect.center);
|
||||
const arrowTail = edge.box.rect.lineIntersect(edge.to.box.rect.center, direction.scale(-1));
|
||||
const arrowPoint = edge.to.box.rect.lineIntersect(edge.box.rect.center, direction);
|
||||
const arrowBaseCenter = arrowPoint.subtract(direction.normalize().scale(ARROWHEAD_LENGTH));
|
||||
const arrowBaseDirection = Vector.from([direction[1], -direction[0]]).normalize();
|
||||
|
@ -170,7 +174,7 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
const arrowBaseRight = arrowBaseCenter.subtract(arrowBaseDirection.scale(ARROWHEAD_WIDTH));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...edge.box.rect.center);
|
||||
ctx.moveTo(...arrowTail);
|
||||
ctx.lineTo(...arrowPoint);
|
||||
ctx.stroke();
|
||||
|
||||
|
@ -190,13 +194,16 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.intervalTask = setInterval(() => {
|
||||
if (this.intervalTaskExecuting) return;
|
||||
this.steps++;
|
||||
if (this.steps > MAX_STEPS_TO_EQUILIBRIUM) {
|
||||
if (this.steps > MAXIMUM_STEPS) {
|
||||
clearInterval(this.intervalTask);
|
||||
this.intervalTask = null;
|
||||
reject(new Error('Exceeded map steps to reach equilibrium'));
|
||||
reject(new Error('Exceeded max steps to reach equilibrium'));
|
||||
}
|
||||
this.intervalTaskExecuting = true;
|
||||
const { atEquilibrium } = this.computeEulerFrame(tDelta);
|
||||
this.intervalTaskExecuting = false;
|
||||
if (atEquilibrium) {
|
||||
console.log(`Reached equilibrium after ${this.steps} steps`);
|
||||
clearInterval(this.intervalTask);
|
||||
|
@ -213,44 +220,81 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
|
||||
// Initialize net force vectors
|
||||
for (const box of boxes) {
|
||||
box.netForce = Vector.zeros(2);
|
||||
box.forces = [];
|
||||
}
|
||||
|
||||
// Compute overlap repulsion forces among node boxes
|
||||
for (const boxA of this.nodes) {
|
||||
const addForce = (box, force, type) => box.forces.push({ force, type });
|
||||
|
||||
// All boxes repel each other if they overlap
|
||||
for (const boxA of boxes) {
|
||||
const idxA = boxes.indexOf(boxA);
|
||||
for (const boxB of this.nodes.slice(idxA + 1)) {
|
||||
for (const boxB of boxes.slice(idxA + 1)) {
|
||||
const force = overlapRepulsionForce(boxA, boxB);
|
||||
boxA.netForce = boxA.netForce.subtract(force);
|
||||
boxB.netForce = boxB.netForce.add(force);
|
||||
addForce(
|
||||
boxA,
|
||||
force.scale(-1),
|
||||
`${boxB.name} -- overlapRepulsion --> ${boxA.name}`,
|
||||
);
|
||||
addForce(
|
||||
boxB,
|
||||
force,
|
||||
`${boxA.name} -- overlapRepulsion --> ${boxB.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute pairwise forces among nodes
|
||||
for (const boxA of this.nodes) {
|
||||
const idxA = this.nodes.indexOf(boxA);
|
||||
for (const boxB of this.nodes.slice(idxA + 1)) {
|
||||
const force = targetRadiusForce(boxA, boxB);
|
||||
boxA.netForce = boxA.netForce.subtract(force);
|
||||
boxB.netForce = boxB.netForce.add(force);
|
||||
// Center of graph attracts all boxes that are outside the graph
|
||||
for (const box of boxes) {
|
||||
if (!this.box.rect.doesContain(box.rect.center)) {
|
||||
const r = this.box.rect.center.subtract(box.rect.center);
|
||||
addForce(
|
||||
box,
|
||||
r.normalize().scale(CENTRAL_RESTORING_FORCE),
|
||||
`center -- attraction --> ${box.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute forces on edge boxes:
|
||||
// Attraction to the `from` and `to` nodes
|
||||
// Compute edge-related forces
|
||||
for (const edgeBox of this.edges) {
|
||||
const { edge } = edgeBox;
|
||||
const fromBox = edge.from.box;
|
||||
const toBox = edge.to.box;
|
||||
edgeBox.netForce = edgeBox.netForce
|
||||
.subtract(targetRadiusForce(edgeBox, fromBox, 0))
|
||||
.subtract(targetRadiusForce(edgeBox, toBox, 0));
|
||||
|
||||
// Attraction to the `from` and `to` nodes
|
||||
addForce(
|
||||
edgeBox,
|
||||
targetRadiusForce(fromBox, edgeBox, 0),
|
||||
`${edgeBox.name} -- attract fromBox --> ${fromBox.name}`,
|
||||
);
|
||||
addForce(
|
||||
edgeBox,
|
||||
targetRadiusForce(toBox, edgeBox, 0),
|
||||
`${edgeBox.name} -- attract toBox --> ${toBox.name}`,
|
||||
);
|
||||
|
||||
// Pairwise force between nodes
|
||||
{
|
||||
const force = targetRadiusForce(fromBox, toBox);
|
||||
addForce(
|
||||
fromBox,
|
||||
force.scale(-1),
|
||||
`${toBox.name} -- pairwise targetRadius --> ${fromBox.name}`,
|
||||
);
|
||||
addForce(
|
||||
toBox,
|
||||
force,
|
||||
`${fromBox.name} -- pairwise targetRadius --> ${toBox.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not apply forces to a box if it is being moved by the mouse
|
||||
for (const box of boxes) {
|
||||
if (this.mouseMoving === box) {
|
||||
box.netForce = Vector.zeros(2);
|
||||
} else {
|
||||
box.netForce = box.forces.reduce((net, { force }) => net.add(force), Vector.from([0, 0]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,8 +305,9 @@ export class ForceDirectedGraph extends WeightedDirectedGraph {
|
|||
box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR);
|
||||
}
|
||||
|
||||
// When all velocities are below MINIMUM_VELOCITY, we have reached equilibrium.
|
||||
let atEquilibrium = true;
|
||||
// When all velocities are below MINIMUM_VELOCITY and we have executed more than
|
||||
// MINIMUM_STEPS, we have reached equilibrium.
|
||||
let atEquilibrium = this.steps > MINIMUM_STEPS;
|
||||
|
||||
// Apply velocities
|
||||
for (const box of boxes) {
|
||||
|
|
|
@ -1,30 +1,55 @@
|
|||
import {
|
||||
DEFAULT_OVERLAP_FORCE,
|
||||
DEFAULT_TARGET_RADIUS,
|
||||
DISTANCE_FACTOR,
|
||||
DEFAULT_DISTANCE_FACTOR,
|
||||
DEFAULT_OVERLAP_BUFFER,
|
||||
OVERLAP_THRESHOLD_RANDOMIZE,
|
||||
} from '../../util/constants.js';
|
||||
import { Rectangle, Vector } from '../supporting/geometry/index.js';
|
||||
|
||||
const getRectangles = (boxes) => boxes.map((box) => (box instanceof Rectangle ? box : box.rect));
|
||||
const getCenters = (boxes) => getRectangles(boxes).map((rect) => rect.center);
|
||||
|
||||
export const overlapRepulsionForce = (boxA, boxB, force = DEFAULT_OVERLAP_FORCE) => {
|
||||
export const overlapRepulsionForce = (
|
||||
boxA,
|
||||
boxB,
|
||||
force = DEFAULT_OVERLAP_FORCE,
|
||||
margin = DEFAULT_OVERLAP_BUFFER,
|
||||
) => {
|
||||
const [rectA, rectB] = getRectangles([boxA, boxB]);
|
||||
const [centerA, centerB] = getCenters([rectA, rectB]);
|
||||
const r = centerB.subtract(centerA);
|
||||
|
||||
// Apply a stronger force when overlap occurs
|
||||
if (!rectA.doesOverlap(rectB)) {
|
||||
return Vector.zeros(rectA.dim);
|
||||
const overlap = rectA.doesOverlap(rectB);
|
||||
if (overlap) {
|
||||
// If there is sufficient overlap, randomize the direction of force.
|
||||
// Note that we don't want to keep randomizing it once we've picked a direction
|
||||
if (overlap <= OVERLAP_THRESHOLD_RANDOMIZE) {
|
||||
if (!boxB.overlapForceDirection) {
|
||||
boxB.overlapForceDirection = Vector.randomUnitVector(rectB.dim);
|
||||
}
|
||||
return boxB.overlapForceDirection.scale(force);
|
||||
}
|
||||
return r.normalize().scale(force);
|
||||
}
|
||||
// If their centers actually coincide we can just randomize the direction.
|
||||
if (r.magnitudeSquared === 0) {
|
||||
return Vector.randomUnitVector(rectA.dim).scale(force);
|
||||
boxB.overlapForceDirection = null;
|
||||
|
||||
// Apply a weaker force until distance > margin
|
||||
const separation = rectA.separationFromRect(rectB);
|
||||
if (separation < margin) {
|
||||
return r.normalize().scale(force * ((margin - separation) / margin));
|
||||
}
|
||||
return r.normalize().scale(force);
|
||||
// Otherwise, zero force
|
||||
return Vector.zeros(rectA.dim);
|
||||
};
|
||||
|
||||
export const targetRadiusForce = (boxA, boxB, targetRadius = DEFAULT_TARGET_RADIUS) => {
|
||||
export const targetRadiusForce = (
|
||||
boxA,
|
||||
boxB,
|
||||
targetRadius = DEFAULT_TARGET_RADIUS,
|
||||
distanceFactor = DEFAULT_DISTANCE_FACTOR,
|
||||
) => {
|
||||
const [rectA, rectB] = getRectangles([boxA, boxB]);
|
||||
const [centerA, centerB] = getCenters([rectA, rectB]);
|
||||
const r = centerB.subtract(centerA);
|
||||
|
@ -35,6 +60,6 @@ export const targetRadiusForce = (boxA, boxB, targetRadius = DEFAULT_TARGET_RADI
|
|||
|
||||
// Repel if closer than targetRadius
|
||||
// Attract if farther than targetRadius
|
||||
const force = -DISTANCE_FACTOR * (distance - targetRadius);
|
||||
const force = -distanceFactor * (distance - targetRadius);
|
||||
return r.normalize().scale(force);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { DEFAULT_OVERLAP_BUFFER } from '../../../util/constants.js';
|
||||
import { Polygon } from './polygon.js';
|
||||
import { Vector } from './vector.js';
|
||||
|
||||
|
@ -29,14 +30,29 @@ export class Rectangle extends Polygon {
|
|||
}
|
||||
|
||||
doesOverlap(rect) {
|
||||
return this.dimensions.every((_, dim) => {
|
||||
const overlapFractions = this.dimensions.map((_, dim) => {
|
||||
const thisMin = this.position[dim];
|
||||
const thisMax = this.position[dim] + this.dimensions[dim];
|
||||
const thatMin = rect.position[dim];
|
||||
const thatMax = rect.position[dim] + rect.dimensions[dim];
|
||||
return (thisMin <= thatMin && thisMax >= thatMin)
|
||||
|| (thisMin >= thatMin && thisMin <= thatMax);
|
||||
if (thatMin <= thisMin && thatMax >= thisMin) {
|
||||
if (thatMax >= thisMax) {
|
||||
return 1;
|
||||
}
|
||||
return (thatMax - thisMin) / (thatMax - thatMin);
|
||||
}
|
||||
if (thatMin <= thisMax && thatMax >= thisMin) {
|
||||
if (thatMax <= thisMax) {
|
||||
return 1;
|
||||
}
|
||||
return (thisMax - thatMin) / (thatMax - thatMin);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
if (overlapFractions.every((x) => x > 0)) {
|
||||
return Math.max(...overlapFractions);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
doesContain(point) {
|
||||
|
@ -69,4 +85,20 @@ export class Rectangle extends Polygon {
|
|||
}
|
||||
return everInside ? point : null;
|
||||
}
|
||||
|
||||
addMargin(margin = DEFAULT_OVERLAP_BUFFER) {
|
||||
const position = this.position.subtract([margin, margin]);
|
||||
const dimensions = this.dimensions.add([2 * margin, 2 * margin]);
|
||||
return new Rectangle(position, dimensions);
|
||||
}
|
||||
|
||||
separationFromRect(rect) {
|
||||
if (this.doesOverlap(rect)) {
|
||||
return 0;
|
||||
}
|
||||
const r = rect.center.subtract(this.center);
|
||||
const outerA = this.lineIntersect(rect.center, r.scale(-1));
|
||||
const outerB = rect.lineIntersect(this.center, r);
|
||||
return outerA.subtract(outerB).magnitude;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ export class Vector extends Array {
|
|||
}
|
||||
|
||||
add(vector) {
|
||||
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||
if (vector.dim !== this.dim) {
|
||||
throw new Error('Can only add vectors of the same dimensions');
|
||||
}
|
||||
|
@ -11,6 +12,7 @@ export class Vector extends Array {
|
|||
}
|
||||
|
||||
subtract(vector) {
|
||||
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||
if (vector.dim !== this.dim) {
|
||||
throw new Error('Can only subtract vectors of the same dimensions');
|
||||
}
|
||||
|
@ -38,7 +40,7 @@ export class Vector extends Array {
|
|||
}
|
||||
|
||||
static randomUnitVector(totalDim) {
|
||||
return Vector.from(Array(totalDim), () => Math.random()).normalize();
|
||||
return Vector.from(Array(totalDim), () => Math.random() - 0.5).normalize();
|
||||
}
|
||||
|
||||
static zeros(totalDim) {
|
||||
|
|
|
@ -114,21 +114,20 @@ span.small {
|
|||
width: 1px;
|
||||
}
|
||||
.force-directed-graph {
|
||||
border: 1px white dotted;
|
||||
margin: 20px;
|
||||
}
|
||||
.force-directed-graph > canvas {
|
||||
position: absolute;
|
||||
border: 1px red dashed;
|
||||
}
|
||||
.force-directed-graph > .box {
|
||||
background-color: #09343f;
|
||||
border: 1px hsl(195.4545454545, 4%, 39.4117647059%) solid;
|
||||
color: #b6b6b6;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
.force-directed-graph > .vertex {
|
||||
border: 1px #46b4b4 solid;
|
||||
text-align: center;
|
||||
background-color: #216262;
|
||||
}
|
||||
.force-directed-graph > .edge {
|
||||
border: 1px #51b769 solid;
|
||||
text-align: center;
|
||||
background-color: #2a5b6c;
|
||||
}
|
|
@ -4,7 +4,7 @@ import { Rectangle, Vector } from '../../classes/supporting/geometry/index.js';
|
|||
import { overlapRepulsionForce, targetRadiusForce } from '../../classes/display/pairwise-forces.js';
|
||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { EPSILON } from '../../util/constants.js';
|
||||
import { DEFAULT_OVERLAP_BUFFER, EPSILON } from '../../util/constants.js';
|
||||
import { mochaRun } from '../../util/helpers.js';
|
||||
|
||||
const rootElement = document.getElementById('scene');
|
||||
|
@ -18,7 +18,7 @@ describe('Force-Directed Graph', function tests() {
|
|||
|
||||
before(() => {
|
||||
graph = (window.graph = new ForceDirectedGraph('test1', window.scene.middleSection.el, {
|
||||
width: 800, height: 600,
|
||||
width: 1200, height: 900,
|
||||
}));
|
||||
|
||||
graph.addVertex('v1', 'box1');
|
||||
|
@ -32,6 +32,34 @@ describe('Force-Directed Graph', function tests() {
|
|||
rect.vertices[3].should.eql([0, 1]);
|
||||
});
|
||||
|
||||
it('should measure extent of overlap', () => {
|
||||
const rects = [
|
||||
new Rectangle([0, 0], [1, 1]),
|
||||
new Rectangle([0.5, 0], [1, 1]),
|
||||
new Rectangle([0, 0.5], [1, 1]),
|
||||
new Rectangle([0.5, 0.5], [1, 1]),
|
||||
new Rectangle([0, 1], [1, 1]),
|
||||
new Rectangle([0, 0], [1, 2]),
|
||||
new Rectangle([0, 0], [0.5, 0.5]),
|
||||
new Rectangle([2, 2], [1, 1]),
|
||||
];
|
||||
rects[0].doesOverlap(rects[1]).should.eql(1);
|
||||
rects[0].doesOverlap(rects[2]).should.eql(1);
|
||||
rects[0].doesOverlap(rects[3]).should.eql(0.5);
|
||||
rects[0].doesOverlap(rects[4]).should.eql(0);
|
||||
rects[0].doesOverlap(rects[5]).should.eql(1);
|
||||
rects[0].doesOverlap(rects[6]).should.eql(1);
|
||||
rects[0].doesOverlap(rects[7]).should.eql(0);
|
||||
|
||||
rects[1].doesOverlap(rects[0]).should.eql(1);
|
||||
rects[2].doesOverlap(rects[0]).should.eql(1);
|
||||
rects[3].doesOverlap(rects[0]).should.eql(0.5);
|
||||
rects[4].doesOverlap(rects[0]).should.eql(0);
|
||||
rects[5].doesOverlap(rects[0]).should.eql(1);
|
||||
rects[6].doesOverlap(rects[0]).should.eql(1);
|
||||
rects[7].doesOverlap(rects[0]).should.eql(0);
|
||||
});
|
||||
|
||||
it('overlapping boxes should repel', () => {
|
||||
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||
const rect2 = new Rectangle([0, 0], [1, 2]);
|
||||
|
@ -41,6 +69,29 @@ describe('Force-Directed Graph', function tests() {
|
|||
force1.should.eql([0, 10]);
|
||||
});
|
||||
|
||||
it('adjacent boxes should repel', () => {
|
||||
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||
const rect2 = new Rectangle([0, 1], [1, 1]);
|
||||
const rect3 = new Rectangle([0, DEFAULT_OVERLAP_BUFFER / 2 + 1], [1, 1]);
|
||||
const rect4 = new Rectangle([0, DEFAULT_OVERLAP_BUFFER + 1], [1, 1]);
|
||||
const rect5 = new Rectangle([DEFAULT_OVERLAP_BUFFER + 1, DEFAULT_OVERLAP_BUFFER + 1], [1, 1]);
|
||||
rect1.doesOverlap(rect2).should.eql(0);
|
||||
rect1.doesOverlap(rect3).should.eql(0);
|
||||
rect1.doesOverlap(rect4).should.eql(0);
|
||||
const force1 = overlapRepulsionForce(rect1, rect2, 10);
|
||||
force1[0].should.eql(0);
|
||||
force1[1].should.be.within(9.99, 10.01);
|
||||
const force2 = overlapRepulsionForce(rect1, rect3, 10);
|
||||
force2[0].should.eql(0);
|
||||
force2[1].should.be.within(4.99, 5.01);
|
||||
const force3 = overlapRepulsionForce(rect1, rect4, 10);
|
||||
force3[0].should.eql(0);
|
||||
force3[1].should.be.within(-0.01, 0.01);
|
||||
const force4 = overlapRepulsionForce(rect1, rect5, 10);
|
||||
force4[0].should.be.within(-0.01, 0.01);
|
||||
force4[1].should.be.within(-0.01, 0.01);
|
||||
});
|
||||
|
||||
it('boxes at target radius should have no net force', () => {
|
||||
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||
const rect2 = new Rectangle([11, 0], [1, 1]);
|
||||
|
@ -104,6 +155,50 @@ describe('Force-Directed Graph', function tests() {
|
|||
it('runs until reaching equilibrium', async () => {
|
||||
await graph.runUntilEquilibrium();
|
||||
});
|
||||
|
||||
it('can add 10 random nodes', async () => {
|
||||
for (let i = 3; i <= 10; i++) {
|
||||
await delayOrWait(200);
|
||||
const v = graph.addVertex('v2', `box${i}`);
|
||||
v.setProperty('prop', 'value');
|
||||
}
|
||||
});
|
||||
|
||||
it('can add 10 random edges', async () => {
|
||||
await delayOrWait(500);
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
let from;
|
||||
let to;
|
||||
do {
|
||||
from = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||
to = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||
} while (from.name === to.name && !graph.getEdge('one', from.name, to.name));
|
||||
await delayOrWait(200);
|
||||
graph.addEdge('one', from.name, to.name, Math.floor(Math.random() * 100) / 100);
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('can add 10 more random nodes', async () => {
|
||||
for (let i = 11; i <= 20; i++) {
|
||||
await delayOrWait(200);
|
||||
const v = graph.addVertex('v3', `box${i}`);
|
||||
v.setProperty('prop', Math.random() * 10000);
|
||||
}
|
||||
});
|
||||
|
||||
it('can add 10 more random edges', async () => {
|
||||
await delayOrWait(500);
|
||||
for (let i = 11; i <= 20; i++) {
|
||||
let from;
|
||||
let to;
|
||||
do {
|
||||
from = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||
to = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||
} while (from.name === to.name && !graph.getEdge('two', from.name, to.name));
|
||||
await delayOrWait(200);
|
||||
graph.addEdge('two', from.name, to.name, Math.floor(Math.random() * 100) / 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mochaRun();
|
||||
|
|
|
@ -9,16 +9,20 @@ export const VertexTypes = {
|
|||
};
|
||||
export const ARROWHEAD_LENGTH = 12;
|
||||
export const ARROWHEAD_WIDTH = 6;
|
||||
export const DEFAULT_OVERLAP_FORCE = 200;
|
||||
export const CENTRAL_RESTORING_FORCE = 100;
|
||||
export const DEFAULT_OVERLAP_BUFFER = 100;
|
||||
export const DEFAULT_OVERLAP_FORCE = 400;
|
||||
export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
|
||||
export const DEFAULT_TARGET_RADIUS = 300;
|
||||
export const DEFAULT_TARGET_RADIUS = 200;
|
||||
export const DEFAULT_TIME_STEP = 0.1;
|
||||
export const DISTANCE_FACTOR = 0.5;
|
||||
export const DEFAULT_DISTANCE_FACTOR = 0.5;
|
||||
export const EPSILON = 2.23e-16;
|
||||
export const INCINERATOR_ADDRESS = '0';
|
||||
export const MAX_STEPS_TO_EQUILIBRIUM = 100;
|
||||
export const MAXIMUM_STEPS = 500;
|
||||
export const MINIMUM_FORCE = 1;
|
||||
export const MINIMUM_VELOCITY = 0.1;
|
||||
export const MINIMUM_VELOCITY = 1;
|
||||
export const MINIMUM_STEPS = 10;
|
||||
export const OVERLAP_THRESHOLD_RANDOMIZE = 0.5;
|
||||
export const TIME_DILATION_FACTOR = 500;
|
||||
export const TRANSLATION_VELOCITY_FACTOR = 0.2;
|
||||
export const VISCOSITY_FACTOR = 0.4;
|
||||
export const VISCOSITY_FACTOR = 0.7;
|
||||
|
|
Loading…
Reference in New Issue