Browse Source

Tweaked physics system, added documentation for all new features (physics, manipulation, smooth) made all GUI elements CSS and HTML.

css_transitions
Alex de Mulder 10 years ago
parent
commit
2c6928d256
21 changed files with 1121 additions and 814 deletions
  1. +6
    -2
      Jakefile.js
  2. +298
    -29
      docs/graph.html
  3. +0
    -2
      examples/graph/02_random_nodes.html
  4. +2
    -1
      examples/graph/20_navigation.html
  5. +194
    -288
      examples/graph/21_data_manipulation.html
  6. +4
    -4
      src/graph/Edge.js
  7. +196
    -135
      src/graph/Graph.js
  8. +16
    -32
      src/graph/Node.js
  9. +128
    -0
      src/graph/css/graph-manipulation.css
  10. +62
    -0
      src/graph/css/graph-navigation.css
  11. +5
    -4
      src/graph/graphMixins/ClusterMixin.js
  12. +60
    -23
      src/graph/graphMixins/ManipulationMixin.js
  13. +37
    -24
      src/graph/graphMixins/MixinLoader.js
  14. +32
    -104
      src/graph/graphMixins/NavigationMixin.js
  15. +1
    -41
      src/graph/graphMixins/SectorsMixin.js
  16. +2
    -61
      src/graph/graphMixins/SelectionMixin.js
  17. +33
    -17
      src/graph/graphMixins/physics/PhysicsMixin.js
  18. +38
    -38
      src/graph/graphMixins/physics/barnesHut.js
  19. +7
    -9
      src/graph/graphMixins/physics/repulsion.js
  20. BIN
      src/graph/img/cross.png
  21. BIN
      src/graph/img/cross2.png

+ 6
- 2
Jakefile.js View File

@ -83,8 +83,8 @@ task('build', {async: true}, function () {
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/graphMixins/physics/PhysicsMixin.js',
'./src/graph/graphMixins/physics/barnesHut.js',
'./src/graph/graphMixins/physics/repulsion.js',
'./src/graph/graphMixins/physics/BarnesHut.js',
'./src/graph/graphMixins/physics/Repulsion.js',
'./src/graph/graphMixins/ManipulationMixin.js',
'./src/graph/graphMixins/SectorsMixin.js',
'./src/graph/graphMixins/ClusterMixin.js',
@ -103,6 +103,10 @@ task('build', {async: true}, function () {
wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', {
forceDelete: true
});
// copy css
wrench.copyDirSyncRecursive('./src/graph/css', DIST+ '/css', {
forceDelete: true
});
var timeStart = Date.now();
// bundle the concatenated script and dependencies into one file

+ 298
- 29
docs/graph.html View File

@ -53,7 +53,9 @@
<li><a href="#Nodes_configuration">Nodes</a></li>
<li><a href="#Edges_configuration">Edges</a></li>
<li><a href="#Groups_configuration">Groups</a></li>
<li><a href="#Clustering">Clustering</a></li>
<li><a href="#Physics">Physics</a></li>
<li><a href="#Data_manipulation">Data_manipulation</a></li>
<li><a href="#Clustering">Clustering</a></li>
<li><a href="#Navigation_controls">Navigation controls</a></li>
<li><a href="#Keyboard_navigation">Keyboard navigation</a></li>
</ul>
@ -529,13 +531,6 @@ var edges = [
type.</td>
</tr>
<tr>
<td>length</td>
<td>number</td>
<td>no</td>
<td>The length of the edge in pixels.</td>
</tr>
<tr>
<td>style</td>
<td>string</td>
@ -647,6 +642,25 @@ var options = {
<th>Description</th>
</tr>
<tr>
<td><a href="#Physics">physics</a></td>
<td>Object</td>
<td>none</td>
<td>
Configuration of the physics system governing the simulation of the nodes and edges.
Barnes-Hut nBody simulation is used by default. See section <a href="#Physics">Physics</a> for an overview of the available options.
</td>
</tr>
<tr>
<td><a href="#Data_manipulation">dataManipulation</a></td>
<td>Object</td>
<td>none</td>
<td>
Settings for manipulating the Dataset. See section <a href="#Data_manipulation">Data manipulation</a> for an overview of the available options.
</td>
</tr>
<tr>
<td><a href="#Clustering">clustering</a></td>
<td>Object</td>
@ -710,6 +724,13 @@ var options = {
</td>
</tr>
<tr>
<td>smoothCurves</td>
<td>Boolean</td>
<td>true</td>
<td>If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve with control points on both nodes and an invisible node in the center of the edge. This support node is also handed by the physics simulation.</td>
</tr>
<tr>
<td>selectable</td>
<td>Boolean</td>
@ -964,12 +985,6 @@ var options = {
Only applicable when the line style is <code>dash-line</code>.</td>
</tr>
<tr>
<td>length</td>
<td>Number</td>
<td>100</td>
<td>The default length of a edge.</td>
</tr>
<tr>
<td>style</td>
<td>String</td>
@ -1122,6 +1137,235 @@ var nodes = [
</table>
<h3 id="Physics">Physics</h3>
<p>
The physics system has been overhauled to increase performance. The original simulation method was based on particel physics with a repulsion field (potential) around each node,
and the edges were modelled as springs. The new system employed the <a href="http://en.wikipedia.org/wiki/Barnes%E2%80%93Hut_simulation">Barnes-Hut</a> gravitational simulation model. The edges are still modelled as springs.
To unify the physics system, the damping, repulsion distance and edge length have been combined in an physics option. To retain good behaviour, both the old repulsion model and the Barnes-Hut model have their own parameters.
If no options for the physics system are supplied, the Barnes-Hut method will be used with the default parameters.
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named physics.
// If a variable is not supplied, the default value is used.
var options = {
physics: {
barnesHut: {
enabled: true,
gravitationalConstant: -2000,
centralGravity: 0.1,
springLength: 100,
springConstant: 0.05,
damping: 0.09
},
repulsion: {
centralGravity: 0.1,
springLength: 50,
springConstant: 0.05,
nodeDistance: 100,
damping: 0.09
},
}
</pre>
<h5>barnesHut:</h5>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>true</td>
<td>This switches the Barnes-Hut simulation on or off. If it is turned off, the old repulsion model is used. Barnes-Hut is generally faster and yields better results.</td>
</tr>
<tr>
<td>gravitationalConstant</td>
<td>Number</td>
<td>-2000</td>
<td>This is the gravitational constand used to calculate the gravity forces. More information is available <a href="http://en.wikipedia.org/wiki/Newton's_law_of_universal_gravitation" target="_blank">here</a>.</td>
</tr>
<tr>
<td>centralGravity</td>
<td>Number</td>
<td>0.1</td>
<td>The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.</td>
</tr>
<tr>
<td>springLength</td>
<td>Number</td>
<td>100</td>
<td>In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields.
To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.</td>
</tr>
<tr>
<td>springConstant</td>
<td>Number</td>
<td>0.05</td>
<td>This is the spring constant used to calculate the spring forces based on Hooke&prime;s Law. More information is available <a href="http://en.wikipedia.org/wiki/Hooke's_law" target="_blank">here</a>.</td>
</tr>
<tr>
<td>damping</td>
<td>Number</td>
<td>0.09</td>
<td>This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available <a href="http://en.wikipedia.org/wiki/Damping" target="_blank">here</a>.</td>
</tr>
</table>
<h5>repulsion:</h5>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>centralGravity</td>
<td>Number</td>
<td>0.1</td>
<td>The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.</td>
</tr>
<tr>
<td>springLength</td>
<td>Number</td>
<td>50</td>
<td>In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields.
To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.</td>
</tr>
<tr>
<td>nodeDistance</td>
<td>Number</td>
<td>100</td>
<td>This parameter is used to define the distance of influence of the repulsion field of the nodes. Below half this distance, the repulsion is maximal and beyond twice this distance the repulsion is zero.</td>
</tr>
<tr>
<td>springConstant</td>
<td>Number</td>
<td>0.05</td>
<td>This is the spring constant used to calculate the spring forces based on Hooke&prime;s Law. More information is available <a href="http://en.wikipedia.org/wiki/Hooke's_law" target="_blank">here</a>.</td>
</tr>
<tr>
<td>damping</td>
<td>Number</td>
<td>0.09</td>
<td>This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available <a href="http://en.wikipedia.org/wiki/Damping" target="_blank">here</a>.</td>
</tr>
</table>
<h3 id="Data_manipulation">Data manipulation</h3>
<p>
By using the data manipulation feature of the graph you can dynamically create nodes, connect nodes with edges, edit nodes or delete nodes and edges.
The toolbar is fully HTML and CSS so the user can style this to their preference. To control the behaviour of the data manipulation, users can insert custom functions
into the data manipulation process. For example, an injected function can show an detailed pop-up when a user wants to add a node. In <a href="../examples/graph/21_data_manipulation.html">example 21</a>,
two functions have been injected into the add and edit functionality. This is described in more detail in the next subsection.
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named dataManipulation.
// If a variable is not supplied, the default value is used.
var options = {
dataManipulation: {
enabled: false,
initiallyVisible: false
}
}
// OR to just load the module with default values:
var options: {
dataManipulation: true
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>Enabling or disabling of the data manipulation toolbar. If it is initially hidden, an edit button appears in the top left corner.</td>
</tr>
<tr>
<td>initiallyVisible</td>
<td>Boolean</td>
<td>false</td>
<td>Initially hide or show the data manipulation toolbar.</td>
</tr>
</table>
<h4 id="Data_manipulation_custom">Data manipulation: custom functionality</h4>
<p>
Users can insert custom functions into the add node, edit node, connect nodes, and delete selected operations. This is done by supplying them in the options.
If the callback is NOT called, nothing happens. <a href="../examples/graph/21_data_manipulation.html">Example 21</a> has two working examples
for the add and edit functions. The data the user is supplied with in these functions has been described in the code below.
For the add data, you can add any and all options that are accepted for node creation as described above. The same goes for edit, however only the fields described
in the code below contain information on the selected node. The callback for connect accepts any options that are used for edge creation. Only the callback for delete selected
requires the same data structure that is supplied to the user.
</p>
<pre class="prettyprint">
// If a variable is not supplied, the default value is used.
var options: {
dataManipulation: true,
onAdd: function(data,callback) {
// fixed must be false because we define a set x and y position.
// If fixed is not false, the node cannot move.
/** data = {id: random unique id,
* label: new,
* x: x position of click (canvas space),
* y: y position of click (canvas space),
* fixed: false
* };
*/
var newData = {..}; // alter the data as you want.
// all fields normally accepted by a node can be used.
callback(newData); // call the callback to add a node.
},
onEdit: function(data,callback) {
/** data = {id:...,
* label: ...,
* group: ...,
* shape: ...,
* color: {
* background:...,
* border:...,
* highlight: {
* background:...,
* border:...
* }
* }
* };
*/
var newData = {..}; // alter the data as you want.
// all fields normally accepted by a node can be used.
callback(newData); // call the callback with the new data to edit the node.
}
onConnect: function(data,callback) {
// data = {from: nodeId1, to: nodeId2};
var newData = {..}; // check or alter data as you see fit.
callback(newData); // call the callback to connect the nodes.
},
onDelete: function(data,callback) {
// data = {nodes: [selectedNodeIds], edges: [selectedEdgeIds]};
var newData = {..}; // alter the data as you want.
// the same data structure is required.
callback(newData); // call the callback to delete the objects.
}
};
</pre>
<p>
Because the interface elements are CSS and HTML, the user will have to correct for size changes of the canvas. To facilitate this, a new event has been added called frameResize.
A function can be bound to this event. This function is supplied with the new widht and height of the canvas. The CSS can then be updated accordingly.
An code snippet from example 21 is shown below.
</p>
<pre class="prettyprint">
graph.on("frameResize", function(params) {console.log(params.width,params.height)});
</pre>
<h3 id="Clustering">Clustering</h3>
<p>
The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without
@ -1150,16 +1394,19 @@ var options = {
reduceToNodes:300,
chainThreshold: 0.4,
clusterEdgeThreshold: 20,
sectorThreshold: 50,
sectorThreshold: 100,
screenSizeThreshold: 0.2,
fontSizeMultiplier: 4.0,
forceAmplification: 0.6,
distanceAmplification: 0.2,
edgeGrowth: 11,
nodeScaling: {width: 10,
height: 10,
radius: 10},
activeAreaBoxSize: 100
maxFontSize: 1000,
forceAmplification: 0.1,
distanceAmplification: 0.1,
edgeGrowth: 20,
nodeScaling: {width: 1,
height: 1,
radius: 1},
maxNodeSizeIncrements: 600,
activeAreaBoxSize: 100,
clusterLevelDifference: 2
}
}
// OR to just load the module with default values:
@ -1233,6 +1480,12 @@ var options: {
<td>4.0</td>
<td>This parameter denotes the increase in fontSize of the cluster when a single node is added to it.</td>
</tr>
<tr>
<td>maxFontSize</td>
<td>Number</td>
<td>1000</td>
<td>This parameter denotes the largest allowed font size. If the font becomes too large, some browsers experience problems displaying this.</td>
</tr>
<tr>
<td>forceAmplification</td>
<td>Number</td>
@ -1251,7 +1504,7 @@ var options: {
<tr>
<td>edgeGrowth</td>
<td>Number</td>
<td>11</td>
<td>20</td>
<td>This factor determines the elongation of edges connected to a cluster.</td>
</tr>
<tr>
@ -1272,13 +1525,29 @@ var options: {
<td>10</td>
<td>This factor determines how much the radius of a cluster increases in pixels per added node.</td>
</tr>
<tr>
<td>activeAreaBoxSize</td>
<tr>
<td>maxNodeSizeIncrements</td>
<td>Number</td>
<td>600</td>
<td>This limits the size clusters can grow to. The default value, 600, implies that if a cluster contains more than 600 nodes, it will no longer grow.</td>
</tr>
<tr>
<td>activeAreaBoxSize</td>
<td>Number</td>
<td>100</td>
<td>Imagine a square with an edge length of <code>activeAreaBoxSize</code> pixels around your cursor.
If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector.
This is regardless of the zoom level.</td>
</tr>
<tr>
<td>clusterLevelDifference</td>
<td>Number</td>
<td>100</td>
<td>Imagine a square with an edge length of <code>activeAreaBoxSize</code> pixels around your cursor.
If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector.
This is regardless of the zoom level.</td>
<td>2</td>
<td>At every clustering session, Graph will check if the difference between cluster levels is
acceptable. When a cluster is formed when zooming out, that is one cluster level.
If you zoom out further and it encompasses more nodes, that is another level. For example:
If the highest level of your graph at any given time is 3, nodes that have not clustered or
have clustered only once will join their neighbour with the lowest cluster level.</td>
</tr>
</table>

+ 0
- 2
examples/graph/02_random_nodes.html View File

@ -102,8 +102,6 @@
</head>
<body onload="draw();">
calculation time:<span id="calctimereporter" style="display:inline;"></span> ms
render time:<span id="rendertimereporter"style="display:inline;"></span> ms
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="25" style="width: 50px;">

+ 2
- 1
examples/graph/20_navigation.html View File

@ -34,6 +34,7 @@
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<link type="text/css" rel="stylesheet" charset="UTF-8" href="../../dist/css/graph-navigation.css">
<script type="text/javascript">
var nodes = null;
@ -125,7 +126,7 @@
<body onload="draw();">
<h2>Navigation controls and keyboad navigation</h2>
<div style="width: 700px; font-size:14px;">
This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below. <br /><br />
This example is the same as example 2, except for the navigation controls that have been activated. The navigation controls are described below. <br /><br />
<table class="legend_table">
<tr>
<td>Icons: </td>

+ 194
- 288
examples/graph/21_data_manipulation.html View File

@ -1,308 +1,214 @@
<!doctype html>
<html>
<head>
<title>Graph | Navigation</title>
<title>Graph | Navigation</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
table.legend_table {
font-size: 11px;
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
}
table.legend_table,td {
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
padding: 2px;
}
div.table_content {
width:80px;
text-align:center;
}
div.table_description {
width:100px;
}
div.graph-manipulationDiv {
border-width:0px;
border-bottom: 1px;
border-style:solid;
border-color: #d6d9d8;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
width: 600px;
height:30px;
z-index:10;
position:absolute;
}
span.manipulationUI {
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
border-radius: 15px;
display:inline-block;
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: -14px 0px 0px 10px;
vertical-align:middle;
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
}
span.manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
}
span.manipulationUI.back {
background-image: url("../../dist/img/backIcon.png");
}
span.manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default;
}
span.manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
}
span.manipulationUI.none {
padding: 0px 0px 0px 0px;
}
span.manipulationUI.notification{
margin: 2px;
font-weight: bold;
}
span.manipulationUI.add {
background-image: url("../../dist/img/addNodeIcon.png");
}
span.manipulationUI.edit {
background-image: url("../../dist/img/editIcon.png");
}
span.manipulationUI.connect {
background-image: url("../../dist/img/connectIcon.png");
}
span.manipulationUI.delete {
background-image: url("../../dist/img/deleteIcon.png");
}
span.manipulationUI.acceptDelete {
background-image: url("../../dist/img/acceptDeleteIcon.png");
}
/* top right bottom left */
span.manipulationLabel {
margin: 0px 0px 0px 23px;
line-height: 25px;
}
div.seperatorLine {
display:inline-block;
width:1px;
height:20px;
background-color: #bdbdbd;
margin: 5px 7px 0px 15px;
}
input.manipulatorInput[type="text"] {
width:80px;
height:15px;
font-size:11px;
margin: 2px 0px 0px 0px;
}
input.manipulatorInput[type="button"] {
width:80px;
height:22px;
font-size:12px;
margin: 2px 0px 0px 10px;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
var nodeCount = document.getElementById('nodeCount').value;
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
connectionCount[i] = 0;
// create edges in a scale-free-graph way
if (i == 1) {
var from = i;
var to = 0;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
<style type="text/css">
body {
font: 10pt sans;
}
else if (i > 1) {
var conn = edges.length * 2;
var rand = Math.floor(Math.random() * conn);
var cum = 0;
var j = 0;
while (j < connectionCount.length && cum < rand) {
cum += connectionCount[j];
j++;
}
var from = i;
var to = j;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
#mygraph {
position:relative;
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
}
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
/*
var options = {
nodes: {
shape: 'circle'
},
edges: {
length: 50
},
stabilize: false
};
*/
var options = {
edges: {
length: 50
},
stabilize: false,
clustering:false,
navigation: true,
keyboard: true,
dataManipulationToolbar: true,
triggerFunctions: {add: function(data,callback) {
data.label = "hello";
callback(data);
},
edit: function(data,callback) {
data.label='edited'
callback(data);
},
edit: function(data,callback) {
data.label='edited'
callback(data);
}
table.legend_table {
font-size: 11px;
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
}
table.legend_table,td {
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
padding: 2px;
}
div.table_content {
width:80px;
text-align:center;
}
div.table_description {
width:100px;
}
};
graph = new vis.Graph(container, data, options);
// add event listeners
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</script>
#operation {
font-size:28px;
}
#graph-popUp {
display:none;
position:absolute;
top:350px;
left:170px;
z-index:299;
width:250px;
height:120px;
background-color: #f9f9f9;
border-style:solid;
border-width:3px;
border-color: #5394ed;
padding:10px;
text-align: center;
}
</style>
<link type="text/css" rel="stylesheet" charset="UTF-8" href="../../dist/css/graph-manipulation.css">
<link type="text/css" rel="stylesheet" charset="UTF-8" href="../../dist/css/graph-navigation.css">
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
var nodeCount = 25;
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
connectionCount[i] = 0;
// create edges in a scale-free-graph way
if (i == 1) {
var from = i;
var to = 0;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
else if (i > 1) {
var conn = edges.length * 2;
var rand = Math.floor(Math.random() * conn);
var cum = 0;
var j = 0;
while (j < connectionCount.length && cum < rand) {
cum += connectionCount[j];
j++;
}
var from = i;
var to = j;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
}
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
length: 50
},
stabilize: false,
dataManipulation: true,
onAdd: function(data,callback) {
var span = document.getElementById('operation');
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
var div = document.getElementById('graph-popUp');
span.innerHTML = "Add Node";
idInput.value = data.id;
labelInput.value = data.label;
saveButton.onclick = saveData.bind(this,data,callback);
cancelButton.onclick = clearPopUp.bind();
div.style.display = 'block';
},
onEdit: function(data,callback) {
var span = document.getElementById('operation');
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
var div = document.getElementById('graph-popUp');
span.innerHTML = "Edit Node";
idInput.value = data.id;
labelInput.value = data.label;
saveButton.onclick = saveData.bind(this,data,callback);
cancelButton.onclick = clearPopUp.bind();
div.style.display = 'block';
}
};
graph = new vis.Graph(container, data, options);
// add event listeners
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
graph.on("frameResize", function(params) {console.log(params.width,params.height)});
function clearPopUp() {
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
saveButton.onclick = null;
cancelButton.onclick = null;
var div = document.getElementById('graph-popUp');
div.style.display = 'none';
}
function saveData(data,callback) {
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var div = document.getElementById('graph-popUp');
data.id = idInput.value;
data.label = labelInput.value;
clearPopUp();
callback(data);
}
}
</script>
</head>
<body onload="draw();">
<h2>Navigation controls and keyboad navigation</h2>
<h2>Editing the dataset</h2>
<div style="width: 700px; font-size:14px;">
This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below. <br /><br />
<table class="legend_table">
<tr>
<td>Icons: </td>
<td><div class="table_content"><img src="../../dist/img/uparrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/downarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/leftarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/rightarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/plus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/minus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/zoomExtends.png" /> </div></td>
</tr>
<tr>
<td><div class="table_description">Keyboard shortcuts:</div></td>
<td><div class="table_content">Up arrow</div></td>
<td><div class="table_content">Down arrow</div></td>
<td><div class="table_content">Left arrow</div></td>
<td><div class="table_content">Right arrow</div></td>
<td><div class="table_content">=<br />[<br />Page up</div></td>
<td><div class="table_content">-<br />]<br />Page down</div></td>
<td><div class="table_content">None</div></td>
</tr>
<tr>
<td><div class="table_description">Description:</div></td>
<td><div class="table_content">Move up</div></td>
<td><div class="table_content">Move down</div></td>
<td><div class="table_content">Move left</div></td>
<td><div class="table_content">Move right</div></td>
<td><div class="table_content">Zoom in</div></td>
<td><div class="table_content">Zoom out</div></td>
<td><div class="table_content">Zoom extends</div></td>
</tr>
</table>
<br />
Apart from clicking the icons, you can also navigate using the keyboard. The buttons are in table above.
Zoom Extends changes the zoom and position of the camera to encompass all visible nodes.
In this example we have enabled the data manipulation setting. If the dataManipulation option is set to true, the edit button will appear.
If you prefer to have the toolbar visible initially, you can set the initiallyVisible option to true. The exact method is described in the docs.
<br /><br />
The data manipulation allows the user to add nodes, connect them, edit them and delete any selected items. In this example we have created trigger functions
for the add and edit operations. By settings these trigger functions the user can direct the way the data is manipulated. In this example we have created a simple
pop-up that allows us to edit some of the properties.
</div>
<br />
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="25" style="width: 50px;">
<input type="submit" value="Go">
</form>
<br>
<div id="graph-popUp">
<span id="operation">node</span> <br>
<table style="margin:auto;"><tr>
<td>id</td><td><input id="node-id" value="new value"></td>
</tr>
<tr>
<td>label</td><td><input id="node-label" value="new value"> </td>
</tr></table>
<input type="button" value="save" id="saveButton"></button>
<input type="button" value="cancel" id="cancelButton"></button>
</div>
<br />
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 4
- 4
src/graph/Edge.js View File

@ -231,7 +231,7 @@ Edge.prototype._drawLine = function(ctx) {
ctx.lineWidth = this._getLineWidth();
var point;
if (this.from != this.to) {
if (this.from != this.to+9) {
// draw line
this._line(ctx);
@ -706,15 +706,15 @@ Edge.prototype.setScale = function(scale) {
Edge.prototype.select = function() {
this.selected = true;
}
};
Edge.prototype.unselect = function() {
this.selected = false;
}
};
Edge.prototype.positionBezierNode = function() {
if (this.via !== null) {
this.via.x = 0.5 * (this.from.x + this.to.x);
this.via.y = 0.5 * (this.from.y + this.to.y);
}
}
};

+ 196
- 135
src/graph/Graph.js View File

@ -17,12 +17,17 @@ function Graph (container, data, options) {
this.containerElement = container;
this.width = '100%';
this.height = '100%';
// to give everything a nice fluidity, we seperate the rendering and calculating of the forces
this.renderRefreshRate = 60; // hz (fps)
// render and calculation settings
this.renderRefreshRate = 60; // hz (fps)
this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
this.stabilize = true; // stabilize before displaying the graph
this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
this.maxRenderSteps = 4; // max amount of physics ticks per render step.
this.stabilize = true; // stabilize before displaying the graph
this.selectable = true;
// these functions can be triggered when the dataset is edited
// these functions are triggered when the dataset is edited
this.triggerFunctions = {add:null,edit:null,connect:null,delete:null};
// set constant values
@ -61,8 +66,6 @@ function Graph (container, data, options) {
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
//distance: 100, //px
length: 100, // px
dash: {
length: 10,
gap: 5,
@ -72,18 +75,21 @@ function Graph (container, data, options) {
physics: {
barnesHut: {
enabled: true,
theta: 1 / 0.5, // inverted to save time during calculation
gravitationalConstant: -3000,
centralGravity: 0.9,
springLength: 40,
springConstant: 0.04
theta: 1 / 0.6, // inverted to save time during calculation
gravitationalConstant: -2000,
centralGravity: 0.1,
springLength: 100,
springConstant: 0.05,
damping: 0.09
},
repulsion: {
centralGravity: 0.01,
springLength: 80,
centralGravity: 0.1,
springLength: 50,
springConstant: 0.05,
nodeDistance: 100
nodeDistance: 100,
damping: 0.09
},
damping: null,
centralGravity: null,
springLength: null,
springConstant: null
@ -98,8 +104,8 @@ function Graph (container, data, options) {
sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
maxFontSize: 1000,
forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
@ -117,104 +123,101 @@ function Graph (container, data, options) {
enabled: false,
speed: {x: 10, y: 10, zoom: 0.02}
},
dataManipulationToolbar: {
dataManipulation: {
enabled: false,
initiallyVisible: false
},
smoothCurves: true,
maxVelocity: 25,
minVelocity: 0.1, // px/s
maxVelocity: 10,
minVelocity: 0.1, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize
};
this.editMode = this.constants.dataManipulationToolbar.initiallyVisible;
this.editMode = this.constants.dataManipulation.initiallyVisible;
// Node variables
var graph = this;
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
graph._redraw();
});
// navigation variables
// keyboard navigation variables
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
// loading all the mixins:
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
// create a frame and canvas
this._create();
// load the sector system. (mandatory, fully integrated with Graph)
this._loadSectorSystem();
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
this._loadClusterSystem();
// load the selection system. (mandatory, required by Graph)
this._loadSelectionSystem();
// apply options
this.setOptions(options);
// other vars
var graph = this;
this.freezeSimulation = false;// freeze the simulation
this.cachedFunctions = {};
// containers for nodes and edges
this.calculationNodes = {};
this.calculationNodeIndices = [];
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
// position and scale variables and objects
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
this.scale = 1; // defining the global scale variable in the constructor
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
// datasets or dataviews
this.nodesData = null; // A DataSet or DataView
this.edgesData = null; // A DataSet or DataView
// create event listeners used to subscribe on the DataSets of the nodes and edges
var me = this;
this.nodesListeners = {
'add': function (event, params) {
me._addNodes(params.items);
me.start();
graph._addNodes(params.items);
graph.start();
},
'update': function (event, params) {
me._updateNodes(params.items);
me.start();
graph._updateNodes(params.items);
graph.start();
},
'remove': function (event, params) {
me._removeNodes(params.items);
me.start();
graph._removeNodes(params.items);
graph.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
me._addEdges(params.items);
me.start();
graph._addEdges(params.items);
graph.start();
},
'update': function (event, params) {
me._updateEdges(params.items);
me.start();
graph._updateEdges(params.items);
graph.start();
},
'remove': function (event, params) {
me._removeEdges(params.items);
me.start();
graph._removeEdges(params.items);
graph.start();
}
};
// properties of the data
// properties for the animation
this.moving = false; // True if any of the nodes have an undefined position
this.timer = undefined;
this.timer = undefined; // Scheduling function. Is definded in this.start();
// load data (the disable start variable will be the same as the enabled clustering)
this.setData(data,this.constants.clustering.enabled);
@ -314,10 +317,10 @@ Graph.prototype.zoomToFit = function(initialZoom, doNotStart) {
if (initialZoom == true) {
if (this.constants.clustering.enabled == true &&
numberOfNodes >= this.constants.clustering.initialMaxNodes) {
zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
else {
zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
}
else {
@ -418,11 +421,22 @@ Graph.prototype.setOptions = function (options) {
if (options.height !== undefined) {this.height = options.height;}
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;}
if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
if (options.triggerFunctions) {
for (prop in options.triggerFunctions) {
this.triggerFunctions[prop] = options.triggerFunctions[prop];
if (options.onAdd) {
this.triggerFunctions.add = options.onAdd;
}
if (options.onEdit) {
this.triggerFunctions.edit = options.onEdit;
}
if (options.onConnect) {
this.triggerFunctions.connect = options.onConnect;
}
if (options.onDelete) {
this.triggerFunctions.delete = options.onDelete;
}
if (options.physics) {
@ -481,16 +495,16 @@ Graph.prototype.setOptions = function (options) {
this.constants.keyboard.enabled = false;
}
if (options.dataManipulationToolbar) {
this.constants.dataManipulationToolbar.enabled = true;
for (prop in options.dataManipulationToolbar) {
if (options.dataManipulationToolbar.hasOwnProperty(prop)) {
this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop];
if (options.dataManipulation) {
this.constants.dataManipulation.enabled = true;
for (prop in options.dataManipulation) {
if (options.dataManipulation.hasOwnProperty(prop)) {
this.constants.dataManipulation[prop] = options.dataManipulation[prop];
}
}
}
else if (options.dataManipulationToolbar !== undefined) {
this.constants.dataManipulationToolbar.enabled = false;
else if (options.dataManipulation !== undefined) {
this.constants.dataManipulation.enabled = false;
}
// TODO: work out these options and document them
@ -547,14 +561,17 @@ Graph.prototype.setOptions = function (options) {
}
}
// (Re)loading the mixins that can be enabled or disabled in the options.
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
// load the navigation system.
this._loadNavigationControls();
// load the data manipulation system
this._loadManipulationSystem();
// configure the smooth curves
this._configureSmoothCurves();
// bind keys. If disabled, this will not do anything;
this._createKeyBinds();
@ -562,7 +579,7 @@ Graph.prototype.setOptions = function (options) {
this.setSize(this.width, this.height);
this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
this._setScale(1);
this.zoomToFit()
this.zoomToFit();
this._redraw();
};
@ -575,7 +592,7 @@ Graph.prototype.setOptions = function (options) {
* event specific properties.
*/
Graph.prototype.on = function on (event, callback) {
var available = ['select'];
var available = ['select','frameResize'];
if (available.indexOf(event) == -1) {
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
@ -693,14 +710,13 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
}
this.mousetrap.bind("b",this._toggleBarnesHut.bind(me));
// this.mousetrap.bind("b",this._toggleBarnesHut.bind(me));
if (this.constants.dataManipulationToolbar.enabled == true) {
if (this.constants.dataManipulation.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
this.mousetrap.bind("del",this._deleteSelected.bind(me));
this.mousetrap.bind("e",this._toggleEditMode.bind(me));
}
}
};
/**
* Get the pointer location from a touch location
@ -737,6 +753,12 @@ Graph.prototype._onDragStart = function () {
};
/**
* This function is called by _onDragStart.
* It is separated out because we can then overload it for the datamanipulation system.
*
* @private
*/
Graph.prototype._handleDragStart = function() {
var drag = this.drag;
var node = this._getNodeAt(drag.pointer);
@ -778,7 +800,7 @@ Graph.prototype._handleDragStart = function() {
}
}
}
}
};
/**
@ -789,6 +811,13 @@ Graph.prototype._onDrag = function (event) {
this._handleOnDrag(event)
};
/**
* This function is called by _onDrag.
* It is separated out because we can then overload it for the datamanipulation system.
*
* @private
*/
Graph.prototype._handleOnDrag = function(event) {
if (this.drag.pinched) {
return;
@ -817,7 +846,7 @@ Graph.prototype._handleOnDrag = function(event) {
}
});
// start animation if not yet running
// start _animationStep if not yet running
if (!this.moving) {
this.moving = true;
this.start();
@ -834,7 +863,7 @@ Graph.prototype._handleOnDrag = function(event) {
this._redraw();
this.moved = true;
}
}
};
/**
* handle drag start event
@ -938,8 +967,6 @@ Graph.prototype._zoom = function(scale, pointer) {
this.areaCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
// this.areaCenter = {"x" : pointer.x,"y" : pointer.y };
// console.log(translation.x,translation.y,pointer.x,pointer.y,scale);
this.pinch.mousewheelScale = scale;
this._setScale(scale);
this._setTranslation(tx, ty);
@ -949,6 +976,7 @@ Graph.prototype._zoom = function(scale, pointer) {
return scale;
};
/**
* Event handler for mouse wheel event, used to zoom the timeline
* See http://adomas.org/javascript-mouse-wheel/
@ -1098,6 +1126,7 @@ Graph.prototype._checkShowPopup = function (pointer) {
}
};
/**
* Check if the popup must be hided, which is the case when the mouse is no
* longer hovering on the object
@ -1135,9 +1164,7 @@ Graph.prototype.setSize = function(width, height) {
this.manipulationDiv.style.width = this.frame.canvas.clientWidth;
}
if (this.constants.navigation.enabled == true) {
this._relocateNavigation();
}
this._trigger('frameResize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
};
/**
@ -1200,7 +1227,7 @@ Graph.prototype._addNodes = function(ids) {
this.nodes[id] = node; // note: this may replace an existing node
if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
var radius = this.constants.physics.springLength * 0.2*ids.length;
var radius = 10 * 0.1*ids.length;
var angle = 2 * Math.PI * Math.random();
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
@ -1211,7 +1238,7 @@ Graph.prototype._addNodes = function(ids) {
}
}
this._updateNodeIndexList();
this._setCalculationNodes()
this._updateCalculationNodes();
this._reconnectEdges();
this._updateValueRange(this.nodes);
this.updateLabels();
@ -1337,7 +1364,7 @@ Graph.prototype._addEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
this._createBezierNodes();
this._setCalculationNodes();
this._updateCalculationNodes();
};
/**
@ -1392,7 +1419,7 @@ Graph.prototype._removeEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
this._setCalculationNodes();
this._updateCalculationNodes();
};
/**
@ -1497,10 +1524,6 @@ Graph.prototype._redraw = function() {
// restore original scaling and translation
ctx.restore();
if (this.constants.navigation.enabled == true) {
this._doInNavigationSector("_drawNodes",ctx,true);
}
};
/**
@ -1698,7 +1721,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private
*/
Graph.prototype._discreteStepNodes = function() {
var interval = 1.0;
var interval = 0.75;
var nodes = this.nodes;
var nodeId;
@ -1726,13 +1749,7 @@ Graph.prototype._discreteStepNodes = function() {
};
/**
* Start animating nodes and edges
*
* @poram {Boolean} runCalculationStep
*/
Graph.prototype.start = function() {
Graph.prototype._physicsTick = function() {
if (!this.freezeSimulation) {
if (this.moving) {
this._doInAllActiveSectors("_initializeForceCalculation");
@ -1743,33 +1760,53 @@ Graph.prototype.start = function() {
this._findCenter(this._getRange())
}
}
};
if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
// start animation. only start calculationTimer if it is not already running
if (!this.timer) {
var graph = this;
this.timer = window.setTimeout(function () {
graph.timer = undefined;
// keyboad movement
if (graph.xIncrement != 0 || graph.yIncrement != 0) {
var translation = graph._getTranslation();
graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement);
}
if (graph.zoomIncrement != 0) {
var center = {
x: graph.frame.canvas.clientWidth / 2,
y: graph.frame.canvas.clientHeight / 2
};
graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
}
/**
* This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
* It reschedules itself at the beginning of the function
*
* @private
*/
Graph.prototype._animationStep = function() {
// reset the timer so a new scheduled animation step can be set
this.timer = undefined;
// handle the keyboad movement
this._handleNavigation();
// this schedules a new animation step
this.start();
graph.start();
graph.start();
graph._redraw();
// start the physics simulation
var calculationTime = Date.now();
var maxSteps = 1;
this._physicsTick();
var timeRequired = Date.now() - calculationTime;
while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxRenderSteps) {
this._physicsTick();
timeRequired = Date.now() - calculationTime;
maxSteps++;
}
// start the rendering process
var renderTime = Date.now();
this._redraw();
this.renderTime = Date.now() - renderTime;
};
}, this.renderTimestep);
/**
* Schedule a animation step with the refreshrate interval.
*
* @poram {Boolean} runCalculationStep
*/
Graph.prototype.start = function() {
if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
if (!this.timer) {
this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
}
}
else {
@ -1777,24 +1814,29 @@ Graph.prototype.start = function() {
}
};
/**
* Debug function, does one step of the graph
* Move the graph according to the keyboard presses.
*
* @private
*/
Graph.prototype.singleStep = function() {
if (this.moving) {
this._initializeForceCalculation();
this._discreteStepNodes();
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin);
this._redraw();
Graph.prototype._handleNavigation = function() {
if (this.xIncrement != 0 || this.yIncrement != 0) {
var translation = this._getTranslation();
this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
}
if (this.zoomIncrement != 0) {
var center = {
x: this.frame.canvas.clientWidth / 2,
y: this.frame.canvas.clientHeight / 2
};
this._zoom(this.scale*(1 + this.zoomIncrement), center);
}
};
/**
* Freeze the animation
* Freeze the _animationStep
*/
Graph.prototype.toggleFreeze = function() {
if (this.freezeSimulation == false) {
@ -1807,24 +1849,43 @@ Graph.prototype.toggleFreeze = function() {
};
Graph.prototype._configureSmoothCurves = function() {
if (this.constants.smoothCurves == true) {
this._createBezierNodes();
}
else {
// delete the support nodes
this.sectors['support']['nodes'] = {};
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
this.edges[edgeId].smooth = false;
this.edges[edgeId].via = null;
}
}
}
this._updateCalculationNodes();
this.moving = true;
this.start();
};
Graph.prototype._createBezierNodes = function() {
if (this.constants.smoothCurves == true) {
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
var edge = this.edges[edgeId];
if (edge.smooth == true) {
if (edge.via == null) {
var nodeId = "edgeId:".concat(edge.id);
this.sectors['support']['nodes'][nodeId] = new Node(
{id:nodeId,
mass:1,
shape:'circle',
internalMultiplier:1,
damping: 1.2},{},{},this.constants);
edge.via = this.sectors['support']['nodes'][nodeId];
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
if (edge.via == null) {
edge.smooth = true;
var nodeId = "edgeId:".concat(edge.id);
this.sectors['support']['nodes'][nodeId] = new Node(
{id:nodeId,
mass:1,
shape:'circle',
internalMultiplier:1
},{},{},this.constants);
edge.via = this.sectors['support']['nodes'][nodeId];
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
}
}
@ -1838,7 +1899,7 @@ Graph.prototype._initializeMixinLoaders = function () {
Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
}
}
}
};

+ 16
- 32
src/graph/Node.js View File

@ -34,6 +34,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.fontSize = constants.nodes.fontSize;
this.fontFace = constants.nodes.fontFace;
this.fontColor = constants.nodes.fontColor;
this.fontDrawThreshold = 3;
this.color = constants.nodes.color;
@ -54,11 +55,15 @@ function Node(properties, imagelist, grouplist, constants) {
this.radiusMax = constants.nodes.radiusMax;
this.imagelist = imagelist;
this.grouplist = grouplist;
this.dampingBase = 0.9;
this.damping = 0.9; // this is manipulated in the updateDamping function
// physics properties
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
this.damping = constants.physics.damping;
this.mass = 1; // kg
this.setProperties(properties, constants);
@ -73,15 +78,9 @@ function Node(properties, imagelist, grouplist, constants) {
this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0;
// mass, force, velocity
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
// variables to tell the node about the graph.
this.graphScaleInv = 1;
this.graphScale = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
this.parentEdgeId = null;
@ -445,15 +444,7 @@ Node.prototype.isFixed = function() {
*/
// TODO: replace this method with calculating the kinetic energy
Node.prototype.isMoving = function(vmin) {
if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) {
// console.log(vmin,this.vx,this.vy);
return true;
}
else {
return false;
}
//return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
};
/**
@ -902,7 +893,7 @@ Node.prototype._drawText = function (ctx) {
Node.prototype._label = function (ctx, text, x, y, align, baseline) {
if (text) {
if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
ctx.fillStyle = this.fontColor || "black";
ctx.textAlign = align || "center";
@ -956,7 +947,7 @@ Node.prototype.inArea = function() {
else {
return true;
}
}
};
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
@ -967,7 +958,7 @@ Node.prototype.inView = function() {
this.x < this.canvasBottomRight.x &&
this.y >= this.canvasTopLeft.y &&
this.y < this.canvasBottomRight.y);
}
};
/**
* This allows the zoom level of the graph to influence the rendering
@ -979,6 +970,7 @@ Node.prototype.inView = function() {
*/
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
this.graphScaleInv = 1.0/scale;
this.graphScale = scale;
this.canvasTopLeft = canvasTopLeft;
this.canvasBottomRight = canvasBottomRight;
};
@ -991,17 +983,9 @@ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight)
*/
Node.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale;
this.graphScale = scale;
};
/**
* This function updates the damping parameter of the node based on its mass,
* heavier nodes have more damping.
*
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function() {
this.damping = Math.min(Math.max(1.2,this.dampingBase),this.dampingBase + 0.01*this.growthIndicator);
};
/**

+ 128
- 0
src/graph/css/graph-manipulation.css View File

@ -0,0 +1,128 @@
div.graph-manipulationDiv {
border-width:0px;
border-bottom: 1px;
border-style:solid;
border-color: #d6d9d8;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
width: 600px;
height:30px;
z-index:10;
position:absolute;
}
div.graph-manipulation-editMode {
height:30px;
z-index:10;
position:absolute;
margin-top:20px;
}
div.graph-manipulation-closeDiv {
height:30px;
width:30px;
z-index:11;
position:absolute;
margin-top:3px;
margin-left:590px;
background-position: 0px 0px;
background-repeat:no-repeat;
background-image: url("../../dist/img/cross.png");
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI {
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
border-radius: 15px;
display:inline-block;
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: -14px 0px 0px 10px;
vertical-align:middle;
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
}
span.graph-manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
}
span.graph-manipulationUI.back {
background-image: url("../../dist/img/backIcon.png");
}
span.graph-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default;
}
span.graph-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
}
span.graph-manipulationUI.none {
padding: 0px 0px 0px 0px;
}
span.graph-manipulationUI.notification{
margin: 2px;
font-weight: bold;
}
span.graph-manipulationUI.add {
background-image: url("../../dist/img/addNodeIcon.png");
}
span.graph-manipulationUI.edit {
background-image: url("../../dist/img/editIcon.png");
}
span.graph-manipulationUI.edit.editmode {
background-color: #fcfcfc;
border-style:solid;
border-width:1px;
border-color: #cccccc;
}
span.graph-manipulationUI.connect {
background-image: url("../../dist/img/connectIcon.png");
}
span.graph-manipulationUI.delete {
background-image: url("../../dist/img/deleteIcon.png");
}
/* top right bottom left */
span.graph-manipulationLabel {
margin: 0px 0px 0px 23px;
line-height: 25px;
}
div.graph-seperatorLine {
display:inline-block;
width:1px;
height:20px;
background-color: #bdbdbd;
margin: 5px 7px 0px 15px;
}

+ 62
- 0
src/graph/css/graph-navigation.css View File

@ -0,0 +1,62 @@
div.graph-navigation {
width:34px;
height:34px;
z-index:10;
-moz-border-radius: 17px;
border-radius: 17px;
position:absolute;
display:inline-block;
background-position: 2px 2px;
background-repeat:no-repeat;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.graph-navigation:hover {
box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
}
div.graph-navigation:active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
div.graph-navigation.up {
background-image: url("../../dist/img/upArrow.png");
margin-top:520px;
margin-left:55px;
}
div.graph-navigation.down {
background-image: url("../../dist/img/downArrow.png");
margin-top:560px;
margin-left:55px;
}
div.graph-navigation.left {
background-image: url("../../dist/img/leftArrow.png");
margin-top:560px;
margin-left:15px;
}
div.graph-navigation.right {
background-image: url("../../dist/img/rightArrow.png");
margin-top:560px;
margin-left:95px;
}
div.graph-navigation.zoomIn {
background-image: url("../../dist/img/plus.png");
margin-top:560px;
margin-left:555px;
}
div.graph-navigation.zoomOut {
background-image: url("../../dist/img/minus.png");
margin-top:560px;
margin-left:515px;
}
div.graph-navigation.zoomExtends {
background-image: url("../../dist/img/zoomExtends.png");
margin-top:520px;
margin-left:555px;
}

+ 5
- 4
src/graph/graphMixins/ClusterMixin.js View File

@ -57,6 +57,7 @@ var ClusterMixin = {
if (level > 0 && reposition == true) {
this.repositionNodes();
}
this._updateCalculationNodes();
},
/**
@ -86,7 +87,7 @@ var ClusterMixin = {
// update the index list, dynamic edges and labels
this._updateNodeIndexList();
this._updateDynamicEdges();
this._setCalculationNodes();
this._updateCalculationNodes();
this.updateLabels();
}
@ -198,7 +199,7 @@ var ClusterMixin = {
}
}
this._setCalculationNodes();
this._updateCalculationNodes();
},
/**
@ -283,7 +284,7 @@ var ClusterMixin = {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,recursive,force);
this._setCalculationNodes();
this._updateCalculationNodes();
}
},
@ -1044,7 +1045,7 @@ var ClusterMixin = {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
var radius = this.constants.physics.springLength * (1 + 0.1*node.mass);
var radius = this.constants.physics.springLength * node.mass;
var angle = 2 * Math.PI * Math.random();
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}

+ 60
- 23
src/graph/graphMixins/ManipulationMixin.js View File

@ -15,7 +15,13 @@ var manipulationMixin = {
}
},
/**
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
* these functions to their original functionality, we saved them in this.cachedFunctions.
* This function restores these functions to their original function.
*
* @private
*/
_restoreOverloadedFunctions : function() {
for (var functionName in this.cachedFunctions) {
if (this.cachedFunctions.hasOwnProperty(functionName)) {
@ -24,15 +30,27 @@ var manipulationMixin = {
}
},
/**
* Enable or disable edit-mode.
*
* @private
*/
_toggleEditMode : function() {
this.editMode = !this.editMode;
var toolbar = document.getElementById("graph-manipulationDiv")
var toolbar = document.getElementById("graph-manipulationDiv");
var closeDiv = document.getElementById("graph-manipulation-closeDiv");
var editModeDiv = document.getElementById("graph-manipulation-editMode");
if (this.editMode == true) {
toolbar.style.display="block";
closeDiv.style.display="block";
editModeDiv.style.display="none";
closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
toolbar.style.display="none";
closeDiv.style.display="none";
editModeDiv.style.display="block";
closeDiv.onclick = null;
}
this._createManipulatorBar()
},
@ -62,37 +80,51 @@ var manipulationMixin = {
}
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI add' id='manipulate-addNode'><span class='manipulationLabel'>Add Node</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI connect' id='manipulate-connectNode'><span class='manipulationLabel'>Add Link</span></span>";
"<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
"<span class='graph-manipulationLabel'>Add Node</span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
"<span class='graph-manipulationLabel'>Add Link</span></span>";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI edit' id='manipulate-editNode'><span class='manipulationLabel'>Edit Node</span></span>";
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
"<span class='graph-manipulationLabel'>Edit Node</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI delete' id='manipulate-delete'><span class='manipulationLabel'>Delete selected</span></span>";
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
"<span class='graph-manipulationLabel'>Delete selected</span></span>";
}
// bind the icons
var addNodeButton = document.getElementById("manipulate-addNode");
var addNodeButton = document.getElementById("graph-manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
var addEdgeButton = document.getElementById("manipulate-connectNode");
var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
var editButton = document.getElementById("manipulate-editNode");
var editButton = document.getElementById("graph-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("manipulate-delete");
var deleteButton = document.getElementById("graph-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
var closeDiv = document.getElementById("graph-manipulation-closeDiv");
closeDiv.onclick = this._toggleEditMode.bind(this);
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
}
else {
this.editModeDiv.innerHTML = "" +
"<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
"<span class='graph-manipulationLabel'>Edit</span></span>"
var editModeButton = document.getElementById("graph-manipulate-editModeButton");
editModeButton.onclick = this._toggleEditMode.bind(this);
}
},
@ -109,12 +141,14 @@ var manipulationMixin = {
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span class='manipulationLabel'>Click in an empty space to place a new node</span></span>";
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>Back</span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>Click in an empty space to place a new node</span></span>";
// bind the icon
var backButton = document.getElementById("manipulate-back");
var backButton = document.getElementById("graph-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
@ -141,12 +175,14 @@ var manipulationMixin = {
this.blockConnectingEdgeSelection = true;
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span id='manipulatorLabel' class='manipulationLabel'>Click on a node and drag the edge to another node.</span></span>";
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>Back</span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>Click on a node and drag the edge to another node to connect them.</span></span>";
// bind the icon
var backButton = document.getElementById("manipulate-back");
var backButton = document.getElementById("graph-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
@ -273,7 +309,6 @@ var manipulationMixin = {
}
}
else {
console.log("didnt use funciton")
this.createNodeOnClick = true;
this.nodesData.add(defaultData);
this.createNodeOnClick = false;
@ -373,6 +408,7 @@ var manipulationMixin = {
this.triggerFunctions.delete(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
this._unselectAll();
me.moving = true;
me.start();
});
@ -384,6 +420,7 @@ var manipulationMixin = {
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this._unselectAll();
this.moving = true;
this.start();
}

+ 37
- 24
src/graph/graphMixins/MixinLoader.js View File

@ -59,6 +59,7 @@ var graphMixinLoaders = {
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
this.constants.physics.damping = this.constants.physics.barnesHut.damping;
this.constants.physics.springGrowthPerMass = this.constants.physics.barnesHut.springGrowthPerMass;
this._loadMixin(barnesHutMixin);
@ -70,6 +71,7 @@ var graphMixinLoaders = {
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
this.constants.physics.damping = this.constants.physics.repulsion.damping;
this.constants.physics.springGrowthPerMass = this.constants.physics.repulsion.springGrowthPerMass;
this._loadMixin(repulsionMixin);
@ -103,12 +105,7 @@ var graphMixinLoaders = {
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = { },
this.sectors["navigation"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = {},
this.sectors["support"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
@ -143,8 +140,7 @@ var graphMixinLoaders = {
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
if (this.constants.dataManipulationToolbar.enabled == true) {
if (this.constants.dataManipulation.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
@ -158,6 +154,28 @@ var graphMixinLoaders = {
}
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
if (this.editModeDiv === undefined) {
this.editModeDiv = document.createElement('div');
this.editModeDiv.className = 'graph-manipulation-editMode';
this.editModeDiv.id = 'graph-manipulation-editMode';
if (this.editMode == true) {
this.editModeDiv.style.display = "none";
}
else {
this.editModeDiv.style.display = "block";
}
this.containerElement.insertBefore(this.editModeDiv, this.frame);
}
if (this.closeDiv === undefined) {
this.closeDiv = document.createElement('div');
this.closeDiv.className = 'graph-manipulation-closeDiv';
this.closeDiv.id = 'graph-manipulation-closeDiv';
this.closeDiv.style.display = this.manipulationDiv.style.display;
this.containerElement.insertBefore(this.closeDiv, this.frame);
}
// load the manipulation functions
this._loadMixin(manipulationMixin);
@ -166,9 +184,17 @@ var graphMixinLoaders = {
}
else {
if (this.manipulationDiv !== undefined) {
// removes all the bindings and overloads
this._createManipulatorBar();
// remove the manipulation divs
this.containerElement.removeChild(this.manipulationDiv);
this.containerElement.removeChild(this.editModeDiv);
this.containerElement.removeChild(this.closeDiv);
this.manipulationDiv = undefined;
this.editModeDiv = undefined;
this.closeDiv = undefined;
// remove the mixin functions
this._clearMixin(manipulationMixin);
}
}
@ -183,24 +209,11 @@ var graphMixinLoaders = {
_loadNavigationControls : function() {
this._loadMixin(NavigationMixin);
// the clean function removes the button divs, this is done to remove the bindings.
this._cleanNavigation();
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
},
/**
* this function exists to avoid errors when not loading the navigation system
*/
_relocateNavigation : function() {
// empty, is overloaded by navigation system
},
}
/**
* this function exists to avoid errors when not loading the navigation system
*/
_unHighlightAll : function() {
// empty, is overloaded by the navigation system
}
}

+ 32
- 104
src/graph/graphMixins/NavigationMixin.js View File

@ -4,35 +4,15 @@
var NavigationMixin = {
/**
* This function moves the navigation controls if the canvas size has been changed. If the arugments
* verticaAlignTop and horizontalAlignLeft are false, the correction will be made
*
* @private
*/
_relocateNavigation : function() {
if (this.sectors !== undefined) {
var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth;
var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight;
this.navigationClientWidth = this.frame.canvas.clientWidth;
this.navigationClientHeight = this.frame.canvas.clientHeight;
var node = null;
for (var nodeId in this.sectors["navigation"]["nodes"]) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) {
node = this.sectors["navigation"]["nodes"][nodeId];
if (!node.horizontalAlignLeft) {
node.x -= xOffset;
}
if (!node.verticalAlignTop) {
node.y -= yOffset;
}
}
}
_cleanNavigation : function() {
// clean up previosu navigation items
var wrapper = document.getElementById('graph-navigation_wrapper');
if (wrapper != null) {
this.containerElement.removeChild(wrapper);
}
document.onmouseup = null;
},
/**
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
@ -42,83 +22,45 @@ var NavigationMixin = {
* @private
*/
_loadNavigationElements : function() {
var DIR = this.constants.navigation.iconPath;
this.navigationClientWidth = this.frame.canvas.clientWidth;
this.navigationClientHeight = this.frame.canvas.clientHeight;
if (this.navigationClientWidth === undefined) {
this.navigationClientWidth = 0;
this.navigationClientHeight = 0;
this._cleanNavigation();
this.navigationDivs = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomToFit'];
this.navigationDivs['wrapper'] = document.createElement('div');
this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDivs[navigationDivs[i]] = document.createElement('div');
this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
var offset = 15;
var intermediateOffset = 7;
var navigationNodes = [
{id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset},
{id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft",
verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight",
verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}
];
var nodeObj = null;
for (var i = 0; i < navigationNodes.length; i++) {
nodeObj = this.sectors["navigation"]['nodes'];
nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants);
}
},
document.onmouseup = this._stopMovement.bind(this);
},
/**
* By setting the clustersize to be larger than 1, we use the clustering drawing method
* to illustrate the buttons are presed. We call this highlighting.
* this stops all movement induced by the navigation buttons
*
* @param {String} elementId
* @private
*/
_highlightNavigationElement : function(elementId) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigation"]["nodes"][elementId].clusterSize = 2;
}
_stopMovement : function() {
this._xStopMoving();
this._yStopMoving();
this._stopZoom();
},
/**
* Reverting back to a normal button
* stops the actions performed by page up and down etc.
*
* @param {String} elementId
* @private
*/
_unHighlightNavigationElement : function(elementId) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigation"]["nodes"][elementId].clusterSize = 1;
}
},
/**
* un-highlight (for lack of a better term) all navigation controls elements
* @param event
* @private
*/
_unHighlightAll : function() {
for (var nodeId in this.sectors['navigation']['nodes']) {
if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) {
this._unHighlightNavigationElement(nodeId);
}
}
},
_preventDefault : function(event) {
if (event !== undefined) {
if (event.preventDefault) {
@ -139,7 +81,7 @@ var NavigationMixin = {
* @private
*/
_moveUp : function(event) {
this._highlightNavigationElement("navigation_up");
console.log("here")
this.yIncrement = this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -151,7 +93,6 @@ var NavigationMixin = {
* @private
*/
_moveDown : function(event) {
this._highlightNavigationElement("navigation_down");
this.yIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -163,7 +104,6 @@ var NavigationMixin = {
* @private
*/
_moveLeft : function(event) {
this._highlightNavigationElement("navigation_left");
this.xIncrement = this.constants.keyboard.speed.x;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -175,7 +115,6 @@ var NavigationMixin = {
* @private
*/
_moveRight : function(event) {
this._highlightNavigationElement("navigation_right");
this.xIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -187,7 +126,6 @@ var NavigationMixin = {
* @private
*/
_zoomIn : function(event) {
this._highlightNavigationElement("navigation_plus");
this.zoomIncrement = this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -199,7 +137,6 @@ var NavigationMixin = {
* @private
*/
_zoomOut : function() {
this._highlightNavigationElement("navigation_min");
this.zoomIncrement = -this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
@ -211,9 +148,6 @@ var NavigationMixin = {
* @private
*/
_stopZoom : function() {
this._unHighlightNavigationElement("navigation_plus");
this._unHighlightNavigationElement("navigation_min");
this.zoomIncrement = 0;
},
@ -223,9 +157,6 @@ var NavigationMixin = {
* @private
*/
_yStopMoving : function() {
this._unHighlightNavigationElement("navigation_up");
this._unHighlightNavigationElement("navigation_down");
this.yIncrement = 0;
},
@ -235,9 +166,6 @@ var NavigationMixin = {
* @private
*/
_xStopMoving : function() {
this._unHighlightNavigationElement("navigation_left");
this._unHighlightNavigationElement("navigation_right");
this.xIncrement = 0;
}

+ 1
- 41
src/graph/graphMixins/SectorsMixin.js View File

@ -84,19 +84,6 @@ var SectorMixin = {
},
/**
* This function sets the global references to nodes, edges and nodeIndices to
* those of the navigation controls sector.
*
* @private
*/
_switchToNavigationSector : function() {
this.nodeIndices = this.sectors["navigation"]["nodeIndices"];
this.nodes = this.sectors["navigation"]["nodes"];
this.edges = this.sectors["navigation"]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the currently active sector.
@ -365,7 +352,7 @@ var SectorMixin = {
this._updateNodeIndexList();
// we refresh the list with calulation nodes and calculation node indices.
this._setCalculationNodes();
this._updateCalculationNodes();
}
}
},
@ -477,33 +464,6 @@ var SectorMixin = {
},
/**
* This runs a function in the navigation controls sector.
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInNavigationSector : function(runFunction,argument) {
this._switchToNavigationSector();
if (argument === undefined) {
this[runFunction]();
}
else {
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
this._loadLatestSector();
},
/**
* This runs a function in all sectors. This is used in the _redraw().
*

+ 2
- 61
src/graph/graphMixins/SelectionMixin.js View File

@ -32,18 +32,6 @@ var SelectionMixin = {
},
/**
* retrieve all nodes in the navigation controls overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllNavigationNodesOverlappingWith : function (object) {
var overlappingNodes = [];
this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes;
},
/**
* Return a position object in canvasspace from a single point in screenspace
*
@ -61,42 +49,6 @@ var SelectionMixin = {
bottom: y};
},
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToScreenPositionObject : function(pointer) {
var x = pointer.x;
var y = pointer.y;
return {left: x,
top: y,
right: x,
bottom: y};
},
/**
* Get the top navigation controls node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getNavigationNodeAt : function (pointer) {
var screenPositionObject = this._pointerToScreenPositionObject(pointer);
var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject);
if (overlappingNodes.length > 0) {
return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
},
/**
* Get the top node at the a specific point (like a click)
@ -426,15 +378,7 @@ var SelectionMixin = {
* @private
*/
_handleTouch : function(pointer) {
if (this.constants.navigation.enabled == true) {
this.pointerPosition = pointer;
var node = this._getNavigationNodeAt(pointer);
if (node != null) {
if (this[node.triggerFunction] !== undefined) {
this[node.triggerFunction]();
}
}
}
},
@ -506,10 +450,7 @@ var SelectionMixin = {
* @private
*/
_handleOnRelease : function(pointer) {
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
this._unHighlightAll();
},

+ 33
- 17
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -72,7 +72,7 @@ var physicsMixin = {
*
* @private
*/
_setCalculationNodes : function() {
_updateCalculationNodes : function() {
if (this.constants.smoothCurves == true) {
this.calculationNodes = {};
this.calculationNodeIndices = [];
@ -113,27 +113,28 @@ var physicsMixin = {
* @private
*/
_calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i;
var dx, dy, distance, node, i;
var nodes = this.calculationNodes;
var gravity = this.constants.physics.centralGravity;
var gravityForce = 0;
for (i = 0; i < this.calculationNodeIndices.length; i++) {
node = nodes[this.calculationNodeIndices[i]];
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx*dx + dy*dy);
gravityForce = gravity / distance;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
node.fx = dx * gravityForce;
node.fy = dy * gravityForce;
}
else {
fx = 0;
fy = 0;
node.fx = 0;
node.fy = 0;
}
node._setForce(fx, fy);
node.updateDamping();
}
},
@ -145,6 +146,7 @@ var physicsMixin = {
*/
_calculateSpringForces : function() {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, length;
var edges = this.edges;
// forces caused by the edges, modelled as springs
@ -157,7 +159,20 @@ var physicsMixin = {
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(edge.from,edge.to,edgeLength);
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
length = Math.sqrt(dx * dx + dy * dy);
springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
fx = dx * springForce;
fy = dy * springForce;
edge.from.fx += fx;
edge.from.fy += fy;
edge.to.fx -= fx;
edge.to.fy -= fy;
}
}
}
@ -211,19 +226,20 @@ var physicsMixin = {
* @private
*/
_calculateSpringForce : function(node1,node2,edgeLength) {
var dx, dy, angle, fx, fy, springForce, length;
var dx, dy, fx, fy, springForce, length;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length);
springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
fx = dx * springForce;
fy = dy * springForce;
node1._addForce(fx, fy);
node2._addForce(-fx, -fy);
node1.fx += fx;
node1.fy += fy;
node2.fx -= fx;
node2.fy -= fy;
}
}

+ 38
- 38
src/graph/graphMixins/physics/barnesHut.js View File

@ -50,49 +50,47 @@ var barnesHutMixin = {
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) { // distance is 0 if it looks to apply a force on itself.
// BarnesHut condition
if (distance * parentBranch.calcSize < this.constants.physics.barnesHut.theta) {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
this._getForceOnNode(parentBranch, node, dx ,dy, distance);
// BarnesHut condition
// original condition : s/d < theta = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
}
else {
this._getForceOnNode(parentBranch, node, dx ,dy, distance);
}
}
}
},
/**
* The gravitational force applied on the node by the mass of the branch.
*
* @param parentBranch
* @param node
* @param dx
* @param dy
* @param distance
* @private
*/
_getForceOnNode : function(parentBranch, node, dx ,dy, distance) {
// even if the parentBranch only has one node, its Center of Mass is at the right place (the node in this case).
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var angle = Math.atan2(dy, dx);
var fx = Math.cos(angle) * gravityForce;
var fy = Math.sin(angle) * gravityForce;
node._addForce(fx, fy);
},
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
@ -177,7 +175,7 @@ var barnesHutMixin = {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
//console.log(parentBranch.children.NW.range.maxX,parentBranch.children.NW.range.maxY, node.x,node.y);
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW");
@ -209,7 +207,9 @@ var barnesHutMixin = {
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += 0.1;
node.x += Math.random();
node.y += Math.random();
this._placeInTree(parentBranch,node, true);
}
else {
this._splitBranch(parentBranch.children[region]);

+ 7
- 9
src/graph/graphMixins/physics/repulsion.js View File

@ -41,8 +41,6 @@ var repulsionMixin = {
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) {
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) {
repulsingForce = 1.0;
}
@ -50,17 +48,17 @@ var repulsionMixin = {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
if (this.sectors['support']['nodes'].hasOwnProperty(node1.id)) {
}
// amplify the repulsion for clusters.
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
repulsingForce = repulsingForce/distance;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1._addForce(-fx, -fy);
node2._addForce(fx, fy);
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}

BIN
src/graph/img/cross.png View File

Before After
Width: 7  |  Height: 7  |  Size: 18 KiB

BIN
src/graph/img/cross2.png View File

Before After
Width: 5  |  Height: 5  |  Size: 17 KiB

Loading…
Cancel
Save