Browse Source

added cubic bezier curves.

flowchartTest
Alex de Mulder 8 years ago
parent
commit
700e735336
17 changed files with 896 additions and 453 deletions
  1. +1
    -0
      HISTORY.md
  2. +663
    -370
      dist/vis.js
  3. +9
    -1
      docs/network/edges.html
  4. +7
    -49
      examples/network/edgeStyles/smooth.html
  5. +5
    -1
      examples/network/layout/hierarchicalLayoutUserdefined.html
  6. +3
    -3
      lib/network/modules/Clustering.js
  7. +2
    -1
      lib/network/modules/EdgesHandler.js
  8. +4
    -2
      lib/network/modules/LayoutEngine.js
  9. +1
    -2
      lib/network/modules/NodesHandler.js
  10. +10
    -7
      lib/network/modules/PhysicsEngine.js
  11. +19
    -4
      lib/network/modules/components/Edge.js
  12. +17
    -0
      lib/network/modules/components/Node.js
  13. +91
    -0
      lib/network/modules/components/edges/CubicBezierEdge.js
  14. +8
    -11
      lib/network/modules/components/edges/util/BezierEdgeBase.js
  15. +48
    -0
      lib/network/modules/components/edges/util/CubicBezierEdgeBase.js
  16. +4
    -0
      lib/network/modules/components/nodes/util/NodeBase.js
  17. +4
    -2
      lib/network/options.js

+ 1
- 0
HISTORY.md View File

@ -14,6 +14,7 @@ http://visjs.org
### Network
- Added moveNode method.
- Added cubic Bezier curves.
## 2015-07-22, version 4.6.0

+ 663
- 370
dist/vis.js
File diff suppressed because it is too large
View File


+ 9
- 1
docs/network/edges.html View File

@ -623,13 +623,21 @@ var options: {
<td>String</td>
<td><code>'dynamic'</code></td>
<td>Possible options: <code>'dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal',
'vertical', 'curvedCW', 'curvedCCW'</code>. Take a look at our example 26 to see what these look like
'vertical', 'curvedCW', 'curvedCCW', 'cubicBezier'</code>. Take a look at our example 26 to see what these look like
and pick the one that you like best!
<br><br>
When using dynamic, the edges will have an invisible support node guiding the shape. This node is part of the
physics simulation.
</td>
</tr>
<tr parent="smooth" class="hidden">
<td class="indent">smooth.forceDirection</td>
<td>String or Boolean</td>
<td><code>false</code></td>
<td>Accepted options: <code>['horizontal', 'vertical', 'none']</code>. This options is only used with the cubicBezier curves. When true, horizontal is chosen, when false,
the direction that is larger (x distance between nodes vs y distance between nodes) is used. If the x distance is larger, horizontal. This is ment to be used with hierarchical layouts.
</td>
</tr>
<tr parent="smooth" class="hidden">
<td class="indent">smooth.roundness</td>
<td>Number</td>

+ 7
- 49
examples/network/edgeStyles/smooth.html View File

@ -33,37 +33,15 @@
<br /> <br />
</div>
<p>
Smooth curve type:
<select id="dropdownID" onchange="update();">
<option value="continuous" selected="selected">continuous</option>
<option value="discrete">discrete</option>
<option value="diagonalCross">diagonalCross</option>
<option value="straightCross">straightCross</option>
<option value="horizontal">horizontal</option>
<option value="vertical">vertical</option>
<option value="curvedCW">curvedCW</option>
<option value="curvedCCW">curvedCCW</option>
<option value="dynamic">dynamic</option>
<option value="none">none</option>
</select>
</p>
<p>
Roundness (0..1): <input type="range" min="0" max="1" value="0.5" step="0.05" style="width:200px" id="roundnessSlider" onchange="update();"> <input id="roundnessScreen" style="width:30px;" value="0.5"> <br>(0.5 is max roundness for continuous, 1.0 for the others)
</p>
<div id="mynetwork"></div>
<script type="text/javascript">
var dropdown = document.getElementById("dropdownID");
var roundnessSlider = document.getElementById("roundnessSlider");
var roundnessScreen = document.getElementById("roundnessScreen");
// create an array with nodes
var nodes = [
{id: 1, label: 'Fixed node', x:0, y:0, fixed:true},
{id: 2, label: 'Drag me', x:150, y:130, physics:false},
{id: 3, label: 'Obstacle', x:80, y:-80, fixed:true, mass:5}
{id: 3, label: 'Obstacle', x:80, y:-80, fixed:true, mass:10}
];
// create an array with edges
@ -79,6 +57,12 @@
};
var options = {
physics:true,
configure:function (option, path) {
if (path.indexOf('smooth') !== -1 || option === 'smooth') {
return true;
}
return false;
},
edges: {
smooth: {
type: 'continuous'
@ -87,33 +71,7 @@
};
var network = new vis.Network(container, data, options);
function update() {
var type = dropdown.value;
var options;
var roundness = parseFloat(roundnessSlider.value);
roundnessScreen.value = roundness;
if (type === 'none') {
options = {
edges: {
smooth: false
}
};
}
else {
options = {
edges: {
smooth: {
type: type,
roundness: roundness
}
}
};
}
network.setOptions(options);
}
update();
</script>
</body>

+ 5
- 1
examples/network/layout/hierarchicalLayoutUserdefined.html View File

@ -79,7 +79,11 @@
var options = {
edges: {
smooth: true
smooth: {
type:'cubicBezier',
forceDirection: (directionInput.value == "UD" || directionInput.value == "DU") ? 'vertical' : 'horizontal',
roundness: 0.4
}
},
layout: {
hierarchical:{

+ 3
- 3
lib/network/modules/Clustering.js View File

@ -383,7 +383,7 @@ class ClusterEngine {
// if this is a cluster edge that is fully encompassed in the cluster, we want to delete it
// this check verifies that both of the connected nodes are in this cluster
if (edgeId.substr(0,12) === "clusterEdge:" && childNodesObj[edge.fromId] !== undefined && childNodesObj[edge.toId] !== undefined) {
edge.edgeType.cleanup();
edge.cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
edge.disconnect();
delete childEdgesObj[edgeId];
@ -538,7 +538,7 @@ class ClusterEngine {
let edge = containedEdges[edgeId];
// if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data
if (this.body.nodes[edge.fromId] === undefined || this.body.nodes[edge.toId] === undefined || edge.toId == clusterNodeId || edge.fromId == clusterNodeId) {
edge.edgeType.cleanup();
edge.cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
edge.disconnect();
delete this.body.edges[edgeId];
@ -591,7 +591,7 @@ class ClusterEngine {
// actually removing the edges
for (let i = 0; i < removeIds.length; i++) {
let edgeId = removeIds[i];
this.body.edges[edgeId].edgeType.cleanup();
this.body.edges[edgeId].cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
this.body.edges[edgeId].disconnect();
delete this.body.edges[edgeId];

+ 2
- 1
lib/network/modules/EdgesHandler.js View File

@ -81,6 +81,7 @@ class EdgesHandler {
smooth: {
enabled: true,
type: "dynamic",
forceDirection:'none',
roundness: 0.5
},
title:undefined,
@ -309,7 +310,7 @@ class EdgesHandler {
var id = ids[i];
var edge = edges[id];
if (edge !== undefined) {
edge.edgeType.cleanup();
edge.cleanup();
edge.disconnect();
delete edges[id];
}

+ 4
- 2
lib/network/modules/LayoutEngine.js View File

@ -126,12 +126,14 @@ class LayoutEngine {
this.optionsBackup.edges = {
smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
type:allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type,
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
};
allOptions.edges.smooth = {
enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
type:type,
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
}
}
}

+ 1
- 2
lib/network/modules/NodesHandler.js View File

@ -433,8 +433,7 @@ class NodesHandler {
*/
moveNode(nodeId, x, y) {
if (this.body.nodes[nodeId] !== undefined) {
this.body.nodes[nodeId].x = Number(x);
this.body.nodes[nodeId].y = Number(y);
this.body.nodes[nodeId].move(Number(x), Number(y));
setTimeout(() => {this.body.emitter.emit("startSimulation")},0);
}
else {

+ 10
- 7
lib/network/modules/PhysicsEngine.js View File

@ -399,10 +399,12 @@ class PhysicsEngine {
* @private
*/
_performStep(nodeId,maxVelocity) {
var node = this.body.nodes[nodeId];
var timestep = this.options.timestep;
var forces = this.physicsBody.forces;
var velocities = this.physicsBody.velocities;
let node = this.body.nodes[nodeId];
let timestep = this.options.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
let x = node.x;
let y = node.y;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
@ -412,7 +414,7 @@ class PhysicsEngine {
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
x += velocities[nodeId].x * timestep; // position
}
else {
forces[nodeId].x = 0;
@ -424,14 +426,15 @@ class PhysicsEngine {
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
y += velocities[nodeId].y * timestep; // position
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
node.move(x,y);
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}

+ 19
- 4
lib/network/modules/components/Edge.js View File

@ -1,6 +1,7 @@
var util = require('../../../util');
import Label from './shared/Label'
import CubicBezierEdge from './edges/CubicBezierEdge'
import BezierEdgeDynamic from './edges/BezierEdgeDynamic'
import BezierEdgeStatic from './edges/BezierEdgeStatic'
import StraightEdge from './edges/StraightEdge'
@ -208,13 +209,15 @@ class Edge {
updateEdgeType() {
let dataChanged = false;
let changeInType = true;
let smooth = this.options.smooth;
if (this.edgeType !== undefined) {
if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') {changeInType = false;}
if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') {changeInType = false;}
if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled === false) {changeInType = false;}
if (this.edgeType instanceof BezierEdgeDynamic && smooth.enabled === true && smooth.type === 'dynamic') {changeInType = false;}
if (this.edgeType instanceof CubicBezierEdge && smooth.enabled === true && smooth.type === 'cubicBezier') {changeInType = false;}
if (this.edgeType instanceof BezierEdgeStatic && smooth.enabled === true && smooth.type !== 'dynamic' && smooth.type !== 'cubicBezier') {changeInType = false;}
if (this.edgeType instanceof StraightEdge && smooth.enabled === false) {changeInType = false;}
if (changeInType === true) {
dataChanged = this.edgeType.cleanup();
dataChanged = this.cleanup();
}
}
@ -224,6 +227,9 @@ class Edge {
dataChanged = true;
this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
}
else if (this.options.smooth.type === 'cubicBezier') {
this.edgeType = new CubicBezierEdge(this.options, this.body, this.labelModule);
}
else {
this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
}
@ -495,6 +501,15 @@ class Edge {
unselect() {
this.selected = false;
}
/**
* cleans all required things on delete
* @returns {*}
*/
cleanup() {
return this.edgeType.cleanup();
}
}
export default Edge;

+ 17
- 0
lib/network/modules/components/Node.js View File

@ -440,7 +440,24 @@ class Node {
);
}
/**
* move the node to a new position.
* @param x
* @param y
*/
move(x,y) {
this.x = x;
this.y = y;
this.shape.move(x,y)
}
/**
* clean all required things on delete.
* @returns {*}
*/
cleanup() {
return this.shape.cleanup();
}
}
export default Node;

+ 91
- 0
lib/network/modules/components/edges/CubicBezierEdge.js View File

@ -0,0 +1,91 @@
import CubicBezierEdgeBase from './util/CubicBezierEdgeBase'
class CubicBezierEdge extends CubicBezierEdgeBase {
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
// get the coordinates of the support points.
let [via1,via2] = this._getViaCoordinates();
let returnValue = [via1,via2];
// start drawing the line.
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
// fallback to normal straight edges
if (via1.x === undefined) {
ctx.lineTo(this.to.x, this.to.y);
returnValue = undefined;
}
else {
ctx.bezierCurveTo(via1.x, via1.y, via2.x, via2.y, this.to.x, this.to.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
return returnValue;
}
_getViaCoordinates() {
let dx = this.from.x - this.to.x;
let dy = this.from.y - this.to.y;
let x1, y1, x2, y2;
let roundness = this.options.smooth.roundness;;
// horizontal if x > y or if direction is forced or if direction is horizontal
if ((Math.abs(dx) > Math.abs(dy) || this.options.smooth.forceDirection === true || this.options.smooth.forceDirection === 'horizontal') && this.options.smooth.forceDirection !== 'vertical') {
y1 = this.from.y;
y2 = this.to.y;
x1 = this.from.x - roundness * dx;
x2 = this.to.x + roundness * dx;
}
else {
y1 = this.from.y - roundness * dy;
y2 = this.to.y + roundness * dy;
x1 = this.from.x;
x2 = this.to.x;
}
return [{x: x1, y: y1},{x: x2, y: y2}];
}
_findBorderPosition(nearNode, ctx) {
return this._findBorderPositionBezier(nearNode, ctx);
}
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, [via1, via2] = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via1, via2);
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param percentage
* @param via
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, [via1, via2] = this._getViaCoordinates()) {
let t = percentage;
let vec = [];
vec[0] = Math.pow(1 - t, 3);
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
let x = vec[0] * this.from.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.to.x;
let y = vec[0] * this.from.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.to.y;
return {x: x, y: y};
}
}
export default CubicBezierEdge;

+ 8
- 11
lib/network/modules/components/edges/util/BezierEdgeBase.js View File

@ -73,18 +73,15 @@ class BezierEdgeBase extends EdgeBase {
* Calculate the distance between a point (x3,y3) and a line segment from
* (x1,y1) to (x2,y2).
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @param {number} x1 from x
* @param {number} y1 from y
* @param {number} x2 to x
* @param {number} y2 to y
* @param {number} x3 point to check x
* @param {number} y3 point to check y
* @private
*/
_getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { // x3,y3 is the point
let xVia, yVia;
xVia = via.x;
yVia = via.y;
let minDistance = 1e9;
let distance;
let i, t, x, y;
@ -92,8 +89,8 @@ class BezierEdgeBase extends EdgeBase {
let lastY = y1;
for (i = 1; i < 10; i++) {
t = 0.1 * i;
x = Math.pow(1 - t, 2) * x1 + (2 * t * (1 - t)) * xVia + Math.pow(t, 2) * x2;
y = Math.pow(1 - t, 2) * y1 + (2 * t * (1 - t)) * yVia + Math.pow(t, 2) * y2;
x = Math.pow(1 - t, 2) * x1 + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * x2;
y = Math.pow(1 - t, 2) * y1 + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * y2;
if (i > 0) {
distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
minDistance = distance < minDistance ? distance : minDistance;

+ 48
- 0
lib/network/modules/components/edges/util/CubicBezierEdgeBase.js View File

@ -0,0 +1,48 @@
import BezierEdgeBase from './BezierEdgeBase'
class CubicBezierEdgeBase extends BezierEdgeBase {
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Calculate the distance between a point (x3,y3) and a line segment from
* (x1,y1) to (x2,y2).
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
* https://en.wikipedia.org/wiki/B%C3%A9zier_curve
* @param {number} x1 from x
* @param {number} y1 from y
* @param {number} x2 to x
* @param {number} y2 to y
* @param {number} x3 point to check x
* @param {number} y3 point to check y
* @private
*/
_getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via1, via2) { // x3,y3 is the point
let minDistance = 1e9;
let distance;
let i, t, x, y;
let lastX = x1;
let lastY = y1;
let vec = [0,0,0,0]
for (i = 1; i < 10; i++) {
t = 0.1 * i;
vec[0] = Math.pow(1 - t, 3);
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
x = vec[0] * x1 + vec[1] * via1.x + vec[2] * via2.x + vec[3] * x2;
y = vec[0] * y1 + vec[1] * via1.y + vec[2] * via2.y + vec[3] * y2;
if (i > 0) {
distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
minDistance = distance < minDistance ? distance : minDistance;
}
lastX = x;
lastY = y;
}
return minDistance;
}
}
export default CubicBezierEdgeBase;

+ 4
- 0
lib/network/modules/components/nodes/util/NodeBase.js View File

@ -67,6 +67,10 @@ class NodeBase {
}
}
}
// possible to overload in the shapes.
move(x,y) {}
cleanup() {}
}
export default NodeBase;

+ 4
- 2
lib/network/options.js View File

@ -79,8 +79,9 @@ let allOptions = {
},
smooth: {
enabled: { boolean },
type: { string: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'] },
type: { string: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW', 'cubicBezier'] },
roundness: { number },
forceDirection: { string: ['horizontal', 'vertical', 'none'], boolean },
__type__: { object, boolean }
},
title: { string, 'undefined': 'undefined' },
@ -401,7 +402,8 @@ let configureOptions = {
},
smooth: {
enabled: true,
type: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'],
type: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW', 'cubicBezier'],
forceDirection: ['horizontal', 'vertical', 'none'],
roundness: [0.5, 0, 1, 0.05]
},
width: [1, 0, 30, 1]

Loading…
Cancel
Save