@ -62,8 +62,8 @@ data.add([ | |||||
]); | ]); | ||||
// subscribe to any change in the DataSet | // subscribe to any change in the DataSet | ||||
data.subscribe('*', function (event, params, senderId) { | |||||
console.log('event', event, params); | |||||
data.on('*', function (event, properties, senderId) { | |||||
console.log('event', event, properties); | |||||
}); | }); | ||||
// update an existing item | // update an existing item | ||||
@ -107,7 +107,7 @@ console.log('formatted items', items); | |||||
</p> | </p> | ||||
<pre class="prettyprint lang-js"> | <pre class="prettyprint lang-js"> | ||||
var data = new vis.DataSet(options) | |||||
var data = new vis.DataSet([data] [, options]) | |||||
</pre> | </pre> | ||||
<p> | <p> | ||||
@ -116,6 +116,11 @@ var data = new vis.DataSet(options) | |||||
<a href="#Data_Manipulation">Data Manipulation</a>. | <a href="#Data_Manipulation">Data Manipulation</a>. | ||||
</p> | </p> | ||||
<p> | |||||
The parameter <code>data</code>code> is optional and can be an Array or | |||||
Google DataTable with items. | |||||
</p> | |||||
<p> | <p> | ||||
The parameter <code>options</code> is optional and is an object which can | The parameter <code>options</code> is optional and is an object which can | ||||
contain the following properties: | contain the following properties: | ||||
@ -545,8 +550,8 @@ var items = data.get({ | |||||
<p> | <p> | ||||
One can subscribe on changes in a DataSet. | One can subscribe on changes in a DataSet. | ||||
A subscription can be created using the method <code>subscribe</code>, | |||||
and removed with <code>unsubscribe</code>. | |||||
A subscription can be created using the method <code>on</code>, | |||||
and removed with <code>off</code>. | |||||
</p> | </p> | ||||
<pre class="prettyprint lang-js"> | <pre class="prettyprint lang-js"> | ||||
@ -554,8 +559,8 @@ var items = data.get({ | |||||
var data = new vis.DataSet(); | var data = new vis.DataSet(); | ||||
// subscribe to any change in the DataSet | // subscribe to any change in the DataSet | ||||
data.subscribe('*', function (event, params, senderId) { | |||||
console.log('event:', event, 'params:', params, 'senderId:', senderId); | |||||
data.on('*', function (event, properties, senderId) { | |||||
console.log('event:', event, 'properties:', properties, 'senderId:', senderId); | |||||
}); | }); | ||||
// add an item | // add an item | ||||
@ -565,14 +570,14 @@ data.remove(1); // triggers an 'remove' event | |||||
</pre> | </pre> | ||||
<h3 id="Subscribe">Subscribe</h3> | |||||
<h3 id="On">On</h3> | |||||
<p> | <p> | ||||
Subscribe to an event. | Subscribe to an event. | ||||
</p> | </p> | ||||
Syntax: | Syntax: | ||||
<pre class="prettyprint lang-js">DataSet.subscribe(event, callback)</pre> | |||||
<pre class="prettyprint lang-js">DataSet.on(event, callback)</pre> | |||||
Where: | Where: | ||||
<ul> | <ul> | ||||
@ -587,17 +592,17 @@ Where: | |||||
</li> | </li> | ||||
</ul> | </ul> | ||||
<h3 id="Unsubscribe">Unsubscribe</h3> | |||||
<h3 id="Off">Off</h3> | |||||
<p> | <p> | ||||
Unsubscribe from an event. | Unsubscribe from an event. | ||||
</p> | </p> | ||||
Syntax: | Syntax: | ||||
<pre class="prettyprint lang-js">DataSet.unsubscribe(event, callback)</pre> | |||||
<pre class="prettyprint lang-js">DataSet.off(event, callback)</pre> | |||||
Where <code>event</code> and <code>callback</code> correspond with the | Where <code>event</code> and <code>callback</code> correspond with the | ||||
parameters used to <a href="#Subscribe">subscribe</a> to the event. | |||||
parameters used to <a href="#On">subscribe</a> to the event. | |||||
<h3 id="Events">Events</h3> | <h3 id="Events">Events</h3> | ||||
@ -650,7 +655,7 @@ parameters used to subscribe to the event. | |||||
</p> | </p> | ||||
<pre class="prettyprint lang-js"> | <pre class="prettyprint lang-js"> | ||||
function (event, params, senderId) { | |||||
function (event, properties, senderId) { | |||||
// handle the event | // handle the event | ||||
}); | }); | ||||
</pre> | </pre> | ||||
@ -674,13 +679,13 @@ function (event, params, senderId) { | |||||
</td> | </td> | ||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>params</td> | |||||
<td>properties</td> | |||||
<td>Object | null</td> | <td>Object | null</td> | ||||
<td> | <td> | ||||
Optional parameters providing more information on the event. | |||||
Optional properties providing more information on the event. | |||||
In case of the events <code>add</code>, | In case of the events <code>add</code>, | ||||
<code>update</code>, and <code>remove</code>, | <code>update</code>, and <code>remove</code>, | ||||
<code>params</code> is always an object containing a property | |||||
<code>properties</code> is always an object containing a property | |||||
items, which contains an array with the ids of the affected | items, which contains an array with the ids of the affected | ||||
items. | items. | ||||
</td> | </td> | ||||
@ -0,0 +1,224 @@ | |||||
<!doctype html> | |||||
<html> | |||||
<head> | |||||
<title>Graph | Navigation</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 10pt sans; | |||||
} | |||||
#mygraph { | |||||
position:relative; | |||||
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; | |||||
} | |||||
#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> | |||||
<script type="text/javascript" src="../../dist/vis.js"></script> | |||||
<link type="text/css" rel="stylesheet" href="../../dist/vis.css"> | |||||
<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'; | |||||
}, | |||||
onConnect: function(data,callback) { | |||||
if (data.from == data.to) { | |||||
var r=confirm("Do you want to connect the node to itself?"); | |||||
if (r==true) { | |||||
callback(data); | |||||
} | |||||
} | |||||
else { | |||||
callback(data); | |||||
} | |||||
} | |||||
}; | |||||
graph = new vis.Graph(container, data, options); | |||||
// add event listeners | |||||
graph.on('select', function(params) { | |||||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||||
}); | |||||
graph.on("resize", 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>Editing the dataset</h2> | |||||
<div style="width: 700px; font-size:14px;"> | |||||
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 /> | |||||
<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> | |||||
@ -0,0 +1,373 @@ | |||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> | |||||
<html> | |||||
<head> | |||||
<title>Graph | Multiline text</title> | |||||
<style type="text/css"> | |||||
#mygraph { | |||||
width: 900px; | |||||
height: 900px; | |||||
border: 1px solid lightgray; | |||||
} | |||||
</style> | |||||
<script type="text/javascript" src="../../dist/vis.js"></script> | |||||
<script type="text/javascript"> | |||||
function draw() { | |||||
// create some nodes | |||||
var nodes = [ | |||||
{id:0,"labelHidden":"Myriel","group":1}, | |||||
{id:1,"labelHidden":"Napoleon","group":1}, | |||||
{id:2,"labelHidden":"Mlle.Baptistine","group":1}, | |||||
{id:3,"labelHidden":"Mme.Magloire","group":1}, | |||||
{id:4,"labelHidden":"CountessdeLo","group":1}, | |||||
{id:5,"labelHidden":"Geborand","group":1}, | |||||
{id:6,"labelHidden":"Champtercier","group":1}, | |||||
{id:7,"labelHidden":"Cravatte","group":1}, | |||||
{id:8,"labelHidden":"Count","group":1}, | |||||
{id:9,"labelHidden":"OldMan","group":1}, | |||||
{id:10,"labelHidden":"Labarre","group":2}, | |||||
{id:11,"labelHidden":"Valjean","group":2}, | |||||
{id:12,"labelHidden":"Marguerite","group":3}, | |||||
{id:13,"labelHidden":"Mme.deR","group":2}, | |||||
{id:14,"labelHidden":"Isabeau","group":2}, | |||||
{id:15,"labelHidden":"Gervais","group":2}, | |||||
{id:16,"labelHidden":"Tholomyes","group":3}, | |||||
{id:17,"labelHidden":"Listolier","group":3}, | |||||
{id:18,"labelHidden":"Fameuil","group":3}, | |||||
{id:19,"labelHidden":"Blacheville","group":3}, | |||||
{id:20,"labelHidden":"Favourite","group":3}, | |||||
{id:21,"labelHidden":"Dahlia","group":3}, | |||||
{id:22,"labelHidden":"Zephine","group":3}, | |||||
{id:23,"labelHidden":"Fantine","group":3}, | |||||
{id:24,"labelHidden":"Mme.Thenardier","group":4}, | |||||
{id:25,"labelHidden":"Thenardier","group":4}, | |||||
{id:26,"labelHidden":"Cosette","group":5}, | |||||
{id:27,"labelHidden":"Javert","group":4}, | |||||
{id:28,"labelHidden":"Fauchelevent","group":0}, | |||||
{id:29,"labelHidden":"Bamatabois","group":2}, | |||||
{id:30,"labelHidden":"Perpetue","group":3}, | |||||
{id:31,"labelHidden":"Simplice","group":2}, | |||||
{id:32,"labelHidden":"Scaufflaire","group":2}, | |||||
{id:33,"labelHidden":"Woman1","group":2}, | |||||
{id:34,"labelHidden":"Judge","group":2}, | |||||
{id:35,"labelHidden":"Champmathieu","group":2}, | |||||
{id:36,"labelHidden":"Brevet","group":2}, | |||||
{id:37,"labelHidden":"Chenildieu","group":2}, | |||||
{id:38,"labelHidden":"Cochepaille","group":2}, | |||||
{id:39,"labelHidden":"Pontmercy","group":4}, | |||||
{id:40,"labelHidden":"Boulatruelle","group":6}, | |||||
{id:41,"labelHidden":"Eponine","group":4}, | |||||
{id:42,"labelHidden":"Anzelma","group":4}, | |||||
{id:43,"labelHidden":"Woman2","group":5}, | |||||
{id:44,"labelHidden":"MotherInnocent","group":0}, | |||||
{id:45,"labelHidden":"Gribier","group":0}, | |||||
{id:46,"labelHidden":"Jondrette","group":7}, | |||||
{id:47,"labelHidden":"Mme.Burgon","group":7}, | |||||
{id:48,"labelHidden":"Gavroche","group":8}, | |||||
{id:49,"labelHidden":"Gillenormand","group":5}, | |||||
{id:50,"labelHidden":"Magnon","group":5}, | |||||
{id:51,"labelHidden":"Mlle.Gillenormand","group":5}, | |||||
{id:52,"labelHidden":"Mme.Pontmercy","group":5}, | |||||
{id:53,"labelHidden":"Mlle.Vaubois","group":5}, | |||||
{id:54,"labelHidden":"Lt.Gillenormand","group":5}, | |||||
{id:55,"labelHidden":"Marius","group":8}, | |||||
{id:56,"labelHidden":"BaronessT","group":5}, | |||||
{id:57,"labelHidden":"Mabeuf","group":8}, | |||||
{id:58,"labelHidden":"Enjolras","group":8}, | |||||
{id:59,"labelHidden":"Combeferre","group":8}, | |||||
{id:60,"labelHidden":"Prouvaire","group":8}, | |||||
{id:61,"labelHidden":"Feuilly","group":8}, | |||||
{id:62,"labelHidden":"Courfeyrac","group":8}, | |||||
{id:63,"labelHidden":"Bahorel","group":8}, | |||||
{id:64,"labelHidden":"Bossuet","group":8}, | |||||
{id:65,"labelHidden":"Joly","group":8}, | |||||
{id:66,"labelHidden":"Grantaire","group":8}, | |||||
{id:67,"labelHidden":"MotherPlutarch","group":9}, | |||||
{id:68,"labelHidden":"Gueulemer","group":4}, | |||||
{id:69,"labelHidden":"Babet","group":4}, | |||||
{id:70,"labelHidden":"Claquesous","group":4}, | |||||
{id:71,"labelHidden":"Montparnasse","group":4}, | |||||
{id:72,"labelHidden":"Toussaint","group":5}, | |||||
{id:73,"labelHidden":"Child1","group":10}, | |||||
{id:74,"labelHidden":"Child2","group":10}, | |||||
{id:75,"labelHidden":"Brujon","group":4}, | |||||
{id:76,"labelHidden":"Mme.Hucheloup","group":8} | |||||
]; | |||||
// create some edges | |||||
var edges = [ | |||||
{"from":1,"to":0}, | |||||
{"from":2,"to":0}, | |||||
{"from":3,"to":0}, | |||||
{"from":3,"to":2}, | |||||
{"from":4,"to":0}, | |||||
{"from":5,"to":0}, | |||||
{"from":6,"to":0}, | |||||
{"from":7,"to":0}, | |||||
{"from":8,"to":0}, | |||||
{"from":9,"to":0}, | |||||
{"from":11,"to":10}, | |||||
{"from":11,"to":3}, | |||||
{"from":11,"to":2}, | |||||
{"from":11,"to":0}, | |||||
{"from":12,"to":11}, | |||||
{"from":13,"to":11}, | |||||
{"from":14,"to":11}, | |||||
{"from":15,"to":11}, | |||||
{"from":17,"to":16}, | |||||
{"from":18,"to":16}, | |||||
{"from":18,"to":17}, | |||||
{"from":19,"to":16}, | |||||
{"from":19,"to":17}, | |||||
{"from":19,"to":18}, | |||||
{"from":20,"to":16}, | |||||
{"from":20,"to":17}, | |||||
{"from":20,"to":18}, | |||||
{"from":20,"to":19}, | |||||
{"from":21,"to":16}, | |||||
{"from":21,"to":17}, | |||||
{"from":21,"to":18}, | |||||
{"from":21,"to":19}, | |||||
{"from":21,"to":20}, | |||||
{"from":22,"to":16}, | |||||
{"from":22,"to":17}, | |||||
{"from":22,"to":18}, | |||||
{"from":22,"to":19}, | |||||
{"from":22,"to":20}, | |||||
{"from":22,"to":21}, | |||||
{"from":23,"to":16}, | |||||
{"from":23,"to":17}, | |||||
{"from":23,"to":18}, | |||||
{"from":23,"to":19}, | |||||
{"from":23,"to":20}, | |||||
{"from":23,"to":21}, | |||||
{"from":23,"to":22}, | |||||
{"from":23,"to":12}, | |||||
{"from":23,"to":11}, | |||||
{"from":24,"to":23}, | |||||
{"from":24,"to":11}, | |||||
{"from":25,"to":24}, | |||||
{"from":25,"to":23}, | |||||
{"from":25,"to":11}, | |||||
{"from":26,"to":24}, | |||||
{"from":26,"to":11}, | |||||
{"from":26,"to":16}, | |||||
{"from":26,"to":25}, | |||||
{"from":27,"to":11}, | |||||
{"from":27,"to":23}, | |||||
{"from":27,"to":25}, | |||||
{"from":27,"to":24}, | |||||
{"from":27,"to":26}, | |||||
{"from":28,"to":11}, | |||||
{"from":28,"to":27}, | |||||
{"from":29,"to":23}, | |||||
{"from":29,"to":27}, | |||||
{"from":29,"to":11}, | |||||
{"from":30,"to":23}, | |||||
{"from":31,"to":30}, | |||||
{"from":31,"to":11}, | |||||
{"from":31,"to":23}, | |||||
{"from":31,"to":27}, | |||||
{"from":32,"to":11}, | |||||
{"from":33,"to":11}, | |||||
{"from":33,"to":27}, | |||||
{"from":34,"to":11}, | |||||
{"from":34,"to":29}, | |||||
{"from":35,"to":11}, | |||||
{"from":35,"to":34}, | |||||
{"from":35,"to":29}, | |||||
{"from":36,"to":34}, | |||||
{"from":36,"to":35}, | |||||
{"from":36,"to":11}, | |||||
{"from":36,"to":29}, | |||||
{"from":37,"to":34}, | |||||
{"from":37,"to":35}, | |||||
{"from":37,"to":36}, | |||||
{"from":37,"to":11}, | |||||
{"from":37,"to":29}, | |||||
{"from":38,"to":34}, | |||||
{"from":38,"to":35}, | |||||
{"from":38,"to":36}, | |||||
{"from":38,"to":37}, | |||||
{"from":38,"to":11}, | |||||
{"from":38,"to":29}, | |||||
{"from":39,"to":25}, | |||||
{"from":40,"to":25}, | |||||
{"from":41,"to":24}, | |||||
{"from":41,"to":25}, | |||||
{"from":42,"to":41}, | |||||
{"from":42,"to":25}, | |||||
{"from":42,"to":24}, | |||||
{"from":43,"to":11}, | |||||
{"from":43,"to":26}, | |||||
{"from":43,"to":27}, | |||||
{"from":44,"to":28}, | |||||
{"from":44,"to":11}, | |||||
{"from":45,"to":28}, | |||||
{"from":47,"to":46}, | |||||
{"from":48,"to":47}, | |||||
{"from":48,"to":25}, | |||||
{"from":48,"to":27}, | |||||
{"from":48,"to":11}, | |||||
{"from":49,"to":26}, | |||||
{"from":49,"to":11}, | |||||
{"from":50,"to":49}, | |||||
{"from":50,"to":24}, | |||||
{"from":51,"to":49}, | |||||
{"from":51,"to":26}, | |||||
{"from":51,"to":11}, | |||||
{"from":52,"to":51}, | |||||
{"from":52,"to":39}, | |||||
{"from":53,"to":51}, | |||||
{"from":54,"to":51}, | |||||
{"from":54,"to":49}, | |||||
{"from":54,"to":26}, | |||||
{"from":55,"to":51}, | |||||
{"from":55,"to":49}, | |||||
{"from":55,"to":39}, | |||||
{"from":55,"to":54}, | |||||
{"from":55,"to":26}, | |||||
{"from":55,"to":11}, | |||||
{"from":55,"to":16}, | |||||
{"from":55,"to":25}, | |||||
{"from":55,"to":41}, | |||||
{"from":55,"to":48}, | |||||
{"from":56,"to":49}, | |||||
{"from":56,"to":55}, | |||||
{"from":57,"to":55}, | |||||
{"from":57,"to":41}, | |||||
{"from":57,"to":48}, | |||||
{"from":58,"to":55}, | |||||
{"from":58,"to":48}, | |||||
{"from":58,"to":27}, | |||||
{"from":58,"to":57}, | |||||
{"from":58,"to":11}, | |||||
{"from":59,"to":58}, | |||||
{"from":59,"to":55}, | |||||
{"from":59,"to":48}, | |||||
{"from":59,"to":57}, | |||||
{"from":60,"to":48}, | |||||
{"from":60,"to":58}, | |||||
{"from":60,"to":59}, | |||||
{"from":61,"to":48}, | |||||
{"from":61,"to":58}, | |||||
{"from":61,"to":60}, | |||||
{"from":61,"to":59}, | |||||
{"from":61,"to":57}, | |||||
{"from":61,"to":55}, | |||||
{"from":62,"to":55}, | |||||
{"from":62,"to":58}, | |||||
{"from":62,"to":59}, | |||||
{"from":62,"to":48}, | |||||
{"from":62,"to":57}, | |||||
{"from":62,"to":41}, | |||||
{"from":62,"to":61}, | |||||
{"from":62,"to":60}, | |||||
{"from":63,"to":59}, | |||||
{"from":63,"to":48}, | |||||
{"from":63,"to":62}, | |||||
{"from":63,"to":57}, | |||||
{"from":63,"to":58}, | |||||
{"from":63,"to":61}, | |||||
{"from":63,"to":60}, | |||||
{"from":63,"to":55}, | |||||
{"from":64,"to":55}, | |||||
{"from":64,"to":62}, | |||||
{"from":64,"to":48}, | |||||
{"from":64,"to":63}, | |||||
{"from":64,"to":58}, | |||||
{"from":64,"to":61}, | |||||
{"from":64,"to":60}, | |||||
{"from":64,"to":59}, | |||||
{"from":64,"to":57}, | |||||
{"from":64,"to":11}, | |||||
{"from":65,"to":63}, | |||||
{"from":65,"to":64}, | |||||
{"from":65,"to":48}, | |||||
{"from":65,"to":62}, | |||||
{"from":65,"to":58}, | |||||
{"from":65,"to":61}, | |||||
{"from":65,"to":60}, | |||||
{"from":65,"to":59}, | |||||
{"from":65,"to":57}, | |||||
{"from":65,"to":55}, | |||||
{"from":66,"to":64}, | |||||
{"from":66,"to":58}, | |||||
{"from":66,"to":59}, | |||||
{"from":66,"to":62}, | |||||
{"from":66,"to":65}, | |||||
{"from":66,"to":48}, | |||||
{"from":66,"to":63}, | |||||
{"from":66,"to":61}, | |||||
{"from":66,"to":60}, | |||||
{"from":67,"to":57}, | |||||
{"from":68,"to":25}, | |||||
{"from":68,"to":11}, | |||||
{"from":68,"to":24}, | |||||
{"from":68,"to":27}, | |||||
{"from":68,"to":48}, | |||||
{"from":68,"to":41}, | |||||
{"from":69,"to":25}, | |||||
{"from":69,"to":68}, | |||||
{"from":69,"to":11}, | |||||
{"from":69,"to":24}, | |||||
{"from":69,"to":27}, | |||||
{"from":69,"to":48}, | |||||
{"from":69,"to":41}, | |||||
{"from":70,"to":25}, | |||||
{"from":70,"to":69}, | |||||
{"from":70,"to":68}, | |||||
{"from":70,"to":11}, | |||||
{"from":70,"to":24}, | |||||
{"from":70,"to":27}, | |||||
{"from":70,"to":41}, | |||||
{"from":70,"to":58}, | |||||
{"from":71,"to":27}, | |||||
{"from":71,"to":69}, | |||||
{"from":71,"to":68}, | |||||
{"from":71,"to":70}, | |||||
{"from":71,"to":11}, | |||||
{"from":71,"to":48}, | |||||
{"from":71,"to":41}, | |||||
{"from":71,"to":25}, | |||||
{"from":72,"to":26}, | |||||
{"from":72,"to":27}, | |||||
{"from":72,"to":11}, | |||||
{"from":73,"to":48}, | |||||
{"from":74,"to":48}, | |||||
{"from":74,"to":73}, | |||||
{"from":75,"to":69}, | |||||
{"from":75,"to":68}, | |||||
{"from":75,"to":25}, | |||||
{"from":75,"to":48}, | |||||
{"from":75,"to":41}, | |||||
{"from":75,"to":70}, | |||||
{"from":75,"to":71}, | |||||
{"from":76,"to":64}, | |||||
{"from":76,"to":65}, | |||||
{"from":76,"to":66}, | |||||
{"from":76,"to":63}, | |||||
{"from":76,"to":62}, | |||||
{"from":76,"to":48}, | |||||
{"from":76,"to":58} | |||||
]; | |||||
// create a graph | |||||
var container = document.getElementById('mygraph'); | |||||
var data = { | |||||
nodes: nodes, | |||||
edges: edges | |||||
}; | |||||
var options = {nodes: {shape:'circle'},stabilize: false}; | |||||
var graph = new vis.Graph(container, data, options); | |||||
} | |||||
</script> | |||||
</head> | |||||
<body onload="draw()"> | |||||
<div id="mygraph"></div> | |||||
</body> | |||||
</html> |
@ -0,0 +1,147 @@ | |||||
<!doctype html> | |||||
<html> | |||||
<head> | |||||
<title>Graph | Random nodes</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 10pt sans; | |||||
} | |||||
#mygraph { | |||||
width: 600px; | |||||
height: 600px; | |||||
border: 1px solid lightgray; | |||||
} | |||||
</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]++; | |||||
} | |||||
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 directionInput = document.getElementById("direction"); | |||||
var options = { | |||||
edges: { | |||||
}, | |||||
stabilize: false, | |||||
hierarchicalLayout: { | |||||
direction: directionInput.value | |||||
} | |||||
}; | |||||
graph = new vis.Graph(container, data, options); | |||||
// add event listeners | |||||
graph.on('select', function(params) { | |||||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||||
}); | |||||
} | |||||
</script> | |||||
</head> | |||||
<body onload="draw();"> | |||||
<h2>Hierarchical Layout - Scale-Free-Graph</h2> | |||||
<div style="width:700px; font-size:14px;"> | |||||
This example shows the randomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2. | |||||
In this example, hierarchical layout has been enabled and the vertical levels are determined automatically. | |||||
</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> | |||||
<input type="button" id="btn-UD" value="Up-Down"> | |||||
<input type="button" id="btn-DU" value="Down-Up"> | |||||
<input type="button" id="btn-LR" value="Left-Right"> | |||||
<input type="button" id="btn-RL" value="Right-Left"> | |||||
<input type="hidden" id='direction' value="UD"> | |||||
<script language="javascript"> | |||||
var directionInput = document.getElementById("direction"); | |||||
var btnUD = document.getElementById("btn-UD"); | |||||
btnUD.onclick = function() { | |||||
directionInput.value = "UD"; | |||||
draw(); | |||||
} | |||||
var btnDU = document.getElementById("btn-DU"); | |||||
btnDU.onclick = function() { | |||||
directionInput.value = "DU"; | |||||
draw(); | |||||
}; | |||||
var btnLR = document.getElementById("btn-LR"); | |||||
btnLR.onclick = function() { | |||||
directionInput.value = "LR"; | |||||
draw(); | |||||
}; | |||||
var btnRL = document.getElementById("btn-RL"); | |||||
btnRL.onclick = function() { | |||||
directionInput.value = "RL"; | |||||
draw(); | |||||
}; | |||||
</script> | |||||
<br> | |||||
<div id="mygraph"></div> | |||||
<p id="selection"></p> | |||||
</body> | |||||
</html> |
@ -0,0 +1,139 @@ | |||||
<!doctype html> | |||||
<html> | |||||
<head> | |||||
<title>Graph | Random nodes</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 10pt sans; | |||||
} | |||||
#mygraph { | |||||
width: 600px; | |||||
height: 600px; | |||||
border: 1px solid lightgray; | |||||
} | |||||
</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 | |||||
for (var i = 0; i < 15; i++) { | |||||
nodes.push({ | |||||
id: i, | |||||
label: String(i) | |||||
}); | |||||
} | |||||
edges.push({ | |||||
from: 0, | |||||
to: 1 | |||||
}); | |||||
edges.push({ | |||||
from: 0, | |||||
to: 6 | |||||
}); | |||||
edges.push({ | |||||
from: 0, | |||||
to: 13 | |||||
});edges.push({ | |||||
from: 0, | |||||
to: 11 | |||||
}); | |||||
edges.push({ | |||||
from: 1, | |||||
to: 2 | |||||
}); | |||||
edges.push({ | |||||
from: 2, | |||||
to: 3 | |||||
}); | |||||
edges.push({ | |||||
from: 2, | |||||
to: 4 | |||||
}); | |||||
edges.push({ | |||||
from: 3, | |||||
to: 5 | |||||
}); | |||||
edges.push({ | |||||
from: 1, | |||||
to: 10 | |||||
}); | |||||
edges.push({ | |||||
from: 1, | |||||
to: 7 | |||||
}); | |||||
edges.push({ | |||||
from: 2, | |||||
to: 8 | |||||
}); | |||||
edges.push({ | |||||
from: 2, | |||||
to: 9 | |||||
}); | |||||
edges.push({ | |||||
from: 3, | |||||
to: 14 | |||||
}); | |||||
edges.push({ | |||||
from: 1, | |||||
to: 12 | |||||
}); | |||||
nodes[0]["level"] = 0; | |||||
nodes[1]["level"] = 1; | |||||
nodes[2]["level"] = 3; | |||||
nodes[3]["level"] = 4; | |||||
nodes[4]["level"] = 4; | |||||
nodes[5]["level"] = 5; | |||||
nodes[6]["level"] = 1; | |||||
nodes[7]["level"] = 2; | |||||
nodes[8]["level"] = 4; | |||||
nodes[9]["level"] = 4; | |||||
nodes[10]["level"] = 2; | |||||
nodes[11]["level"] = 1; | |||||
nodes[12]["level"] = 2; | |||||
nodes[13]["level"] = 1; | |||||
nodes[14]["level"] = 5; | |||||
// create a graph | |||||
var container = document.getElementById('mygraph'); | |||||
var data = { | |||||
nodes: nodes, | |||||
edges: edges | |||||
}; | |||||
var options = { | |||||
hierarchicalLayout:true | |||||
}; | |||||
graph = new vis.Graph(container, data, options); | |||||
// add event listeners | |||||
graph.on('select', function(params) { | |||||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||||
}); | |||||
} | |||||
</script> | |||||
</head> | |||||
<body onload="draw();"> | |||||
<h2>Hierarchical Layout - User-defined</h2> | |||||
<div style="width:700px; font-size:14px;"> | |||||
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined. | |||||
</div> | |||||
<br /> | |||||
<div id="mygraph"></div> | |||||
<p id="selection"></p> | |||||
</body> | |||||
</html> |
@ -0,0 +1,110 @@ | |||||
<!doctype html> | |||||
<html> | |||||
<head> | |||||
<title>Graph | Playing with Physics</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 10pt sans; | |||||
} | |||||
#mygraph { | |||||
width: 600px; | |||||
height: 600px; | |||||
border: 1px solid lightgray; | |||||
} | |||||
</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 = 60; | |||||
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: { | |||||
}, | |||||
stabilize: false, | |||||
configurePhysics:true | |||||
}; | |||||
graph = new vis.Graph(container, data, options); | |||||
// add event listeners | |||||
graph.on('select', function(params) { | |||||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||||
}); | |||||
} | |||||
</script> | |||||
</head> | |||||
<body onload="draw();"> | |||||
<h2>Playing with Physics</h2> | |||||
<div style="width: 700px; font-size:14px;"> | |||||
Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, graph has a special option | |||||
that the user can use to explore which settings may be good for him or her. This is ment to be used during the development phase when you are implementing vis.js. Once you have found | |||||
settings you are happy with, you can supply them to graph using the documented physics options. | |||||
On start, the default settings will be loaded. Keep in mind that selecting the hierarchical simulation mode <b>disables</b> smooth curves. These will not be enabled again afterwards. | |||||
</div> | |||||
<br /> | |||||
<div id="mygraph"></div> | |||||
<p id="selection"></p> | |||||
</body> | |||||
</html> |
@ -0,0 +1,65 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Show current and custom time bars</title> | |||||
<style type="text/css"> | |||||
body, html { | |||||
font-family: sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p> | |||||
<input type="text" id="setTime" value="2014-02-07" /> | |||||
<input type="button" id="set" value="Set custom time" /> | |||||
</p> | |||||
<p> | |||||
<input type="button" id="get" value="Get custom time" /> | |||||
<span id="getTime"></span> | |||||
</p> | |||||
<p> | |||||
<code>timechange</code> event: <span id="timechangeEvent"></span> | |||||
</p> | |||||
<p> | |||||
<code>timechanged</code> event: <span id="timechangedEvent"></span> | |||||
</p> | |||||
<div id="visualization"></div> | |||||
<script type="text/javascript"> | |||||
var container = document.getElementById('visualization'); | |||||
var items = []; | |||||
var options = { | |||||
showCurrentTime: true, | |||||
showCustomTime: true, | |||||
start: new Date(Date.now() - 1000 * 60 * 60 * 24), | |||||
end: new Date(Date.now() + 1000 * 60 * 60 * 24 * 6) | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
document.getElementById('set').onclick = function () { | |||||
var time = document.getElementById('setTime').value; | |||||
timeline.setCustomTime(time); | |||||
}; | |||||
document.getElementById('setTime').value = new Date().toISOString().substring(0, 10); | |||||
document.getElementById('get').onclick = function () { | |||||
document.getElementById('getTime').innerHTML = timeline.getCustomTime(); | |||||
}; | |||||
timeline.on('timechange', function (properties) { | |||||
document.getElementById('timechangeEvent').innerHTML = properties.time; | |||||
}); | |||||
timeline.on('timechanged', function (properties) { | |||||
document.getElementById('timechangedEvent').innerHTML = properties.time; | |||||
}); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,90 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Edit items</title> | |||||
<style type="text/css"> | |||||
body, html { | |||||
font-family: sans-serif; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<div id="visualization"></div> | |||||
<p></p> | |||||
<div id="log"></div> | |||||
<script type="text/javascript"> | |||||
var items = new vis.DataSet([ | |||||
{id: 1, content: 'item 1', start: new Date(2013, 3, 20)}, | |||||
{id: 2, content: 'item 2', start: new Date(2013, 3, 14)}, | |||||
{id: 3, content: 'item 3', start: new Date(2013, 3, 18)}, | |||||
{id: 4, content: 'item 4', start: new Date(2013, 3, 16), end: new Date(2013, 3, 19)}, | |||||
{id: 5, content: 'item 5', start: new Date(2013, 3, 25)}, | |||||
{id: 6, content: 'item 6', start: new Date(2013, 3, 27)} | |||||
]); | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
editable: true, | |||||
onAdd: function (item, callback) { | |||||
item.content = prompt('Enter text content for new item:', item.content); | |||||
if (item.content != null) { | |||||
callback(item); // send back adjusted new item | |||||
} | |||||
else { | |||||
callback(null); // cancel item creation | |||||
} | |||||
}, | |||||
onMove: function (item, callback) { | |||||
if (confirm('Do you really want to move the item to\n' + | |||||
'start: ' + item.start + '\n' + | |||||
'end: ' + item.end + '?')) { | |||||
callback(item); // send back item as confirmation (can be changed | |||||
} | |||||
else { | |||||
callback(null); // cancel editing item | |||||
} | |||||
}, | |||||
onUpdate: function (item, callback) { | |||||
item.content = prompt('Edit items text:', item.content); | |||||
if (item.content != null) { | |||||
callback(item); // send back adjusted item | |||||
} | |||||
else { | |||||
callback(null); // cancel updating the item | |||||
} | |||||
}, | |||||
onRemove: function (item, callback) { | |||||
if (confirm('Remove item ' + item.content + '?')) { | |||||
callback(item); // confirm deletion | |||||
} | |||||
else { | |||||
callback(null); // cancel deletion | |||||
} | |||||
} | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
items.on('*', function (event, properties) { | |||||
logEvent(event, properties); | |||||
}); | |||||
function logEvent(event, properties) { | |||||
var log = document.getElementById('log'); | |||||
var msg = document.createElement('div'); | |||||
msg.innerHTML = 'event=' + JSON.stringify(event) + ', ' + | |||||
'properties=' + JSON.stringify(properties); | |||||
log.firstChild ? log.insertBefore(msg, log.firstChild) : log.appendChild(msg); | |||||
} | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,65 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Order groups</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
#visualization { | |||||
box-sizing: border-box; | |||||
width: 100%; | |||||
height: 300px; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p> | |||||
This example demonstrate custom ordering of groups. | |||||
</p> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
var groups = new vis.DataSet([ | |||||
{id: 0, content: 'First', value: 1}, | |||||
{id: 1, content: 'Third', value: 3}, | |||||
{id: 2, content: 'Second', value: 2} | |||||
]); | |||||
// create a dataset with items | |||||
var items = new vis.DataSet([ | |||||
{id: 0, group: 0, content: 'item 0', start: new Date(2014, 3, 17)}, | |||||
{id: 1, group: 0, content: 'item 1', start: new Date(2014, 3, 19)}, | |||||
{id: 2, group: 1, content: 'item 2', start: new Date(2014, 3, 16)}, | |||||
{id: 3, group: 1, content: 'item 3', start: new Date(2014, 3, 23)}, | |||||
{id: 4, group: 1, content: 'item 4', start: new Date(2014, 3, 22)}, | |||||
{id: 5, group: 2, content: 'item 5', start: new Date(2014, 3, 24)} | |||||
]); | |||||
// create visualization | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
// option groupOrder can be a property name or a sort function | |||||
// the sort function must compare two groups and return a value | |||||
// > 0 when a > b | |||||
// < 0 when a < b | |||||
// 0 when a == b | |||||
groupOrder: function (a, b) { | |||||
return a.value - b.value; | |||||
} | |||||
}; | |||||
var timeline = new vis.Timeline(container); | |||||
timeline.setOptions(options); | |||||
timeline.setGroups(groups); | |||||
timeline.setItems(items); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,51 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Limit move and zoom</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p> | |||||
The visible range is limited in this demo: | |||||
</p> | |||||
<ul> | |||||
<li>minimum visible date is limited to 2012-01-01 using option <code>min</code></li> | |||||
<li>maximum visible date is limited to 2013-01-01 (excluded) using option <code>max</code></li> | |||||
<li>visible zoom interval is limited to a minimum of 24 hours using option <code>zoomMin</code></li> | |||||
<li>visible zoom interval is limited to a maximum of about 3 months using option <code>zoomMax</code></li> | |||||
</ul> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
// create some items | |||||
var items = [ | |||||
{'start': new Date(2012, 4, 25), 'content': 'First'}, | |||||
{'start': new Date(2012, 4, 26), 'content': 'Last'} | |||||
]; | |||||
// create visualization | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
height: '300px', | |||||
min: new Date(2012, 0, 1), // lower limit of visible range | |||||
max: new Date(2013, 0, 1), // upper limit of visible range | |||||
zoomMin: 1000 * 60 * 60 * 24, // one day in milliseconds | |||||
zoomMax: 1000 * 60 * 60 * 24 * 31 * 3 // about three months in milliseconds | |||||
}; | |||||
// create the timeline | |||||
var timeline = new vis.Timeline(container); | |||||
timeline.setOptions(options); | |||||
timeline.setItems(items); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -1,89 +0,0 @@ | |||||
/** | |||||
* An event bus can be used to emit events, and to subscribe to events | |||||
* @constructor EventBus | |||||
*/ | |||||
function EventBus() { | |||||
this.subscriptions = []; | |||||
} | |||||
/** | |||||
* Subscribe to an event | |||||
* @param {String | RegExp} event The event can be a regular expression, or | |||||
* a string with wildcards, like 'server.*'. | |||||
* @param {function} callback. Callback are called with three parameters: | |||||
* {String} event, {*} [data], {*} [source] | |||||
* @param {*} [target] | |||||
* @returns {String} id A subscription id | |||||
*/ | |||||
EventBus.prototype.on = function (event, callback, target) { | |||||
var regexp = (event instanceof RegExp) ? | |||||
event : | |||||
new RegExp(event.replace('*', '\\w+')); | |||||
var subscription = { | |||||
id: util.randomUUID(), | |||||
event: event, | |||||
regexp: regexp, | |||||
callback: (typeof callback === 'function') ? callback : null, | |||||
target: target | |||||
}; | |||||
this.subscriptions.push(subscription); | |||||
return subscription.id; | |||||
}; | |||||
/** | |||||
* Unsubscribe from an event | |||||
* @param {String | Object} filter Filter for subscriptions to be removed | |||||
* Filter can be a string containing a | |||||
* subscription id, or an object containing | |||||
* one or more of the fields id, event, | |||||
* callback, and target. | |||||
*/ | |||||
EventBus.prototype.off = function (filter) { | |||||
var i = 0; | |||||
while (i < this.subscriptions.length) { | |||||
var subscription = this.subscriptions[i]; | |||||
var match = true; | |||||
if (filter instanceof Object) { | |||||
// filter is an object. All fields must match | |||||
for (var prop in filter) { | |||||
if (filter.hasOwnProperty(prop)) { | |||||
if (filter[prop] !== subscription[prop]) { | |||||
match = false; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
// filter is a string, filter on id | |||||
match = (subscription.id == filter); | |||||
} | |||||
if (match) { | |||||
this.subscriptions.splice(i, 1); | |||||
} | |||||
else { | |||||
i++; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Emit an event | |||||
* @param {String} event | |||||
* @param {*} [data] | |||||
* @param {*} [source] | |||||
*/ | |||||
EventBus.prototype.emit = function (event, data, source) { | |||||
for (var i =0; i < this.subscriptions.length; i++) { | |||||
var subscription = this.subscriptions[i]; | |||||
if (subscription.regexp.test(event)) { | |||||
if (subscription.callback) { | |||||
subscription.callback(event, data, source); | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,116 +0,0 @@ | |||||
/** | |||||
* Event listener (singleton) | |||||
*/ | |||||
// TODO: replace usage of the event listener for the EventBus | |||||
var events = { | |||||
'listeners': [], | |||||
/** | |||||
* Find a single listener by its object | |||||
* @param {Object} object | |||||
* @return {Number} index -1 when not found | |||||
*/ | |||||
'indexOf': function (object) { | |||||
var listeners = this.listeners; | |||||
for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { | |||||
var listener = listeners[i]; | |||||
if (listener && listener.object == object) { | |||||
return i; | |||||
} | |||||
} | |||||
return -1; | |||||
}, | |||||
/** | |||||
* Add an event listener | |||||
* @param {Object} object | |||||
* @param {String} event The name of an event, for example 'select' | |||||
* @param {function} callback The callback method, called when the | |||||
* event takes place | |||||
*/ | |||||
'addListener': function (object, event, callback) { | |||||
var index = this.indexOf(object); | |||||
var listener = this.listeners[index]; | |||||
if (!listener) { | |||||
listener = { | |||||
'object': object, | |||||
'events': {} | |||||
}; | |||||
this.listeners.push(listener); | |||||
} | |||||
var callbacks = listener.events[event]; | |||||
if (!callbacks) { | |||||
callbacks = []; | |||||
listener.events[event] = callbacks; | |||||
} | |||||
// add the callback if it does not yet exist | |||||
if (callbacks.indexOf(callback) == -1) { | |||||
callbacks.push(callback); | |||||
} | |||||
}, | |||||
/** | |||||
* Remove an event listener | |||||
* @param {Object} object | |||||
* @param {String} event The name of an event, for example 'select' | |||||
* @param {function} callback The registered callback method | |||||
*/ | |||||
'removeListener': function (object, event, callback) { | |||||
var index = this.indexOf(object); | |||||
var listener = this.listeners[index]; | |||||
if (listener) { | |||||
var callbacks = listener.events[event]; | |||||
if (callbacks) { | |||||
index = callbacks.indexOf(callback); | |||||
if (index != -1) { | |||||
callbacks.splice(index, 1); | |||||
} | |||||
// remove the array when empty | |||||
if (callbacks.length == 0) { | |||||
delete listener.events[event]; | |||||
} | |||||
} | |||||
// count the number of registered events. remove listener when empty | |||||
var count = 0; | |||||
var events = listener.events; | |||||
for (var e in events) { | |||||
if (events.hasOwnProperty(e)) { | |||||
count++; | |||||
} | |||||
} | |||||
if (count == 0) { | |||||
delete this.listeners[index]; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Remove all registered event listeners | |||||
*/ | |||||
'removeAllListeners': function () { | |||||
this.listeners = []; | |||||
}, | |||||
/** | |||||
* Trigger an event. All registered event handlers will be called | |||||
* @param {Object} object | |||||
* @param {String} event | |||||
* @param {Object} properties (optional) | |||||
*/ | |||||
'trigger': function (object, event, properties) { | |||||
var index = this.indexOf(object); | |||||
var listener = this.listeners[index]; | |||||
if (listener) { | |||||
var callbacks = listener.events[event]; | |||||
if (callbacks) { | |||||
for (var i = 0, iMax = callbacks.length; i < iMax; i++) { | |||||
callbacks[i](properties); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,245 +0,0 @@ | |||||
/** | |||||
* Created by Alex on 1/22/14. | |||||
*/ | |||||
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; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* 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 | |||||
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
* | |||||
* @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; | |||||
} | |||||
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); | |||||
} | |||||
}, | |||||
/** | |||||
* 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. | |||||
* | |||||
* @param {String} elementId | |||||
* @private | |||||
*/ | |||||
_highlightNavigationElement : function(elementId) { | |||||
if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { | |||||
this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; | |||||
} | |||||
}, | |||||
/** | |||||
* Reverting back to a normal button | |||||
* | |||||
* @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 | |||||
* @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) { | |||||
event.preventDefault(); | |||||
} else { | |||||
event.returnValue = false; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen up | |||||
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
* To avoid this behaviour, we do the translation in the start loop. | |||||
* | |||||
* @private | |||||
*/ | |||||
_moveUp : function(event) { | |||||
this._highlightNavigationElement("navigation_up"); | |||||
this.yIncrement = this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
}, | |||||
/** | |||||
* move the screen down | |||||
* @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); | |||||
}, | |||||
/** | |||||
* move the screen left | |||||
* @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); | |||||
}, | |||||
/** | |||||
* move the screen right | |||||
* @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); | |||||
}, | |||||
/** | |||||
* Zoom in, using the same method as the movement. | |||||
* @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); | |||||
}, | |||||
/** | |||||
* Zoom out | |||||
* @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); | |||||
}, | |||||
/** | |||||
* Stop zooming and unhighlight the zoom controls | |||||
* @private | |||||
*/ | |||||
_stopZoom : function() { | |||||
this._unHighlightNavigationElement("navigation_plus"); | |||||
this._unHighlightNavigationElement("navigation_min"); | |||||
this.zoomIncrement = 0; | |||||
}, | |||||
/** | |||||
* Stop moving in the Y direction and unHighlight the up and down | |||||
* @private | |||||
*/ | |||||
_yStopMoving : function() { | |||||
this._unHighlightNavigationElement("navigation_up"); | |||||
this._unHighlightNavigationElement("navigation_down"); | |||||
this.yIncrement = 0; | |||||
}, | |||||
/** | |||||
* Stop moving in the X direction and unHighlight left and right. | |||||
* @private | |||||
*/ | |||||
_xStopMoving : function() { | |||||
this._unHighlightNavigationElement("navigation_left"); | |||||
this._unHighlightNavigationElement("navigation_right"); | |||||
this.xIncrement = 0; | |||||
} | |||||
}; |
@ -1,515 +0,0 @@ | |||||
var SelectionMixin = { | |||||
/** | |||||
* This function can be called from the _doInAllSectors function | |||||
* | |||||
* @param object | |||||
* @param overlappingNodes | |||||
* @private | |||||
*/ | |||||
_getNodesOverlappingWith : function(object, overlappingNodes) { | |||||
var nodes = this.nodes; | |||||
for (var nodeId in nodes) { | |||||
if (nodes.hasOwnProperty(nodeId)) { | |||||
if (nodes[nodeId].isOverlappingWith(object)) { | |||||
overlappingNodes.push(nodeId); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* retrieve all nodes 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 | |||||
*/ | |||||
_getAllNodesOverlappingWith : function (object) { | |||||
var overlappingNodes = []; | |||||
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); | |||||
return overlappingNodes; | |||||
}, | |||||
/** | |||||
* 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 | |||||
* | |||||
* @param pointer | |||||
* @returns {{left: number, top: number, right: number, bottom: number}} | |||||
* @private | |||||
*/ | |||||
_pointerToPositionObject : function(pointer) { | |||||
var x = this._canvasToX(pointer.x); | |||||
var y = this._canvasToY(pointer.y); | |||||
return {left: x, | |||||
top: y, | |||||
right: x, | |||||
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) | |||||
* | |||||
* @param {{x: Number, y: Number}} pointer | |||||
* @return {Node | null} node | |||||
* @private | |||||
*/ | |||||
_getNodeAt : function (pointer) { | |||||
// we first check if this is an navigation controls element | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
overlappingNodes = this._getAllNodesOverlappingWith(positionObject); | |||||
// if there are overlapping nodes, select the last one, this is the | |||||
// one which is drawn on top of the others | |||||
if (overlappingNodes.length > 0) { | |||||
return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
}, | |||||
/** | |||||
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call | |||||
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. | |||||
* | |||||
* @param pointer | |||||
* @returns {null} | |||||
* @private | |||||
*/ | |||||
_getEdgeAt : function(pointer) { | |||||
return null; | |||||
}, | |||||
/** | |||||
* Add object to the selection array. The this.selection id array may not be needed. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
_addToSelection : function(obj) { | |||||
this.selection.push(obj.id); | |||||
this.selectionObj[obj.id] = obj; | |||||
}, | |||||
/** | |||||
* Remove a single option from selection. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
_removeFromSelection : function(obj) { | |||||
for (var i = 0; i < this.selection.length; i++) { | |||||
if (obj.id == this.selection[i]) { | |||||
this.selection.splice(i,1); | |||||
break; | |||||
} | |||||
} | |||||
delete this.selectionObj[obj.id]; | |||||
}, | |||||
/** | |||||
* Unselect all. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_unselectAll : function(doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
this.selection = []; | |||||
for (var objId in this.selectionObj) { | |||||
if (this.selectionObj.hasOwnProperty(objId)) { | |||||
this.selectionObj[objId].unselect(); | |||||
} | |||||
} | |||||
this.selectionObj = {}; | |||||
if (doNotTrigger == false) { | |||||
this._trigger('select', { | |||||
nodes: this.getSelection() | |||||
}); | |||||
} | |||||
}, | |||||
/** | |||||
* Check if anything is selected | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
_selectionIsEmpty : function() { | |||||
if (this.selection.length == 0) { | |||||
return true; | |||||
} | |||||
else { | |||||
return false; | |||||
} | |||||
}, | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node} node | |||||
* @param {Boolean} append | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_selectNode : function(node, append, doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
if (this._selectionIsEmpty() == false && append == false) { | |||||
this._unselectAll(true); | |||||
} | |||||
if (node.selected == false) { | |||||
node.select(); | |||||
this._addToSelection(node); | |||||
} | |||||
else { | |||||
node.unselect(); | |||||
this._removeFromSelection(node); | |||||
} | |||||
if (doNotTrigger == false) { | |||||
this._trigger('select', { | |||||
nodes: this.getSelection() | |||||
}); | |||||
} | |||||
}, | |||||
/** | |||||
* handles the selection part of the touch, only for navigation controls elements; | |||||
* Touch is triggered before tap, also before hold. Hold triggers after a while. | |||||
* This is the most responsive solution | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleTouch : function(pointer) { | |||||
if (this.constants.navigation.enabled == true) { | |||||
var node = this._getNavigationNodeAt(pointer); | |||||
if (node != null) { | |||||
if (this[node.triggerFunction] !== undefined) { | |||||
this[node.triggerFunction](); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* handles the selection part of the tap; | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleTap : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectNode(node,false); | |||||
} | |||||
else { | |||||
this._unselectAll(); | |||||
} | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* handles the selection part of the double tap and opens a cluster if needed | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleDoubleTap : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null && node !== undefined) { | |||||
// we reset the areaCenter here so the opening of the node will occur | |||||
this.areaCenter = {"x" : this._canvasToX(pointer.x), | |||||
"y" : this._canvasToY(pointer.y)}; | |||||
this.openCluster(node); | |||||
} | |||||
}, | |||||
/** | |||||
* Handle the onHold selection part | |||||
* | |||||
* @param pointer | |||||
* @private | |||||
*/ | |||||
_handleOnHold : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectNode(node,true); | |||||
} | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* handle the onRelease event. These functions are here for the navigation controls module. | |||||
* | |||||
* @private | |||||
*/ | |||||
_handleOnRelease : function() { | |||||
this.xIncrement = 0; | |||||
this.yIncrement = 0; | |||||
this.zoomIncrement = 0; | |||||
this._unHighlightAll(); | |||||
}, | |||||
/** | |||||
* | |||||
* retrieve the currently selected nodes | |||||
* @return {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelection : function() { | |||||
return this.selection.concat([]); | |||||
}, | |||||
/** | |||||
* | |||||
* retrieve the currently selected nodes as objects | |||||
* @return {Objects} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelectionObjects : function() { | |||||
return this.selectionObj; | |||||
}, | |||||
/** | |||||
* // TODO: rework this function, it is from the old system | |||||
* | |||||
* select zero or more nodes | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
setSelection : function(selection) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var node = this.nodes[id]; | |||||
if (!node) { | |||||
throw new RangeError('Node with id "' + id + '" not found'); | |||||
} | |||||
this._selectNode(node,true,true); | |||||
} | |||||
this.redraw(); | |||||
}, | |||||
/** | |||||
* TODO: rework this function, it is from the old system | |||||
* | |||||
* Validate the selection: remove ids of nodes which no longer exist | |||||
* @private | |||||
*/ | |||||
_updateSelection : function () { | |||||
var i = 0; | |||||
while (i < this.selection.length) { | |||||
var nodeId = this.selection[i]; | |||||
if (!this.nodes.hasOwnProperty(nodeId)) { | |||||
this.selection.splice(i, 1); | |||||
delete this.selectionObj[nodeId]; | |||||
} | |||||
else { | |||||
i++; | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Unselect selected nodes. If no selection array is provided, all nodes | |||||
* are unselected | |||||
* @param {Object[]} selection Array with selection objects, each selection | |||||
* object has a parameter row. Optional | |||||
* @param {Boolean} triggerSelect If true (default), the select event | |||||
* is triggered when nodes are unselected | |||||
* @return {Boolean} changed True if the selection is changed | |||||
* @private | |||||
*/ | |||||
/* _unselectNodes : function(selection, triggerSelect) { | |||||
var changed = false; | |||||
var i, iMax, id; | |||||
if (selection) { | |||||
// remove provided selections | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
if (this.nodes.hasOwnProperty(id)) { | |||||
this.nodes[id].unselect(); | |||||
} | |||||
var j = 0; | |||||
while (j < this.selection.length) { | |||||
if (this.selection[j] == id) { | |||||
this.selection.splice(j, 1); | |||||
changed = true; | |||||
} | |||||
else { | |||||
j++; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
else if (this.selection && this.selection.length) { | |||||
// remove all selections | |||||
for (i = 0, iMax = this.selection.length; i < iMax; i++) { | |||||
id = this.selection[i]; | |||||
if (this.nodes.hasOwnProperty(id)) { | |||||
this.nodes[id].unselect(); | |||||
} | |||||
changed = true; | |||||
} | |||||
this.selection = []; | |||||
} | |||||
if (changed && (triggerSelect == true || triggerSelect == undefined)) { | |||||
// fire the select event | |||||
this._trigger('select', { | |||||
nodes: this.getSelection() | |||||
}); | |||||
} | |||||
return changed; | |||||
}, | |||||
*/ | |||||
/** | |||||
* select all nodes on given location x, y | |||||
* @param {Array} selection an array with node ids | |||||
* @param {boolean} append If true, the new selection will be appended to the | |||||
* current selection (except for duplicate entries) | |||||
* @return {Boolean} changed True if the selection is changed | |||||
* @private | |||||
*/ | |||||
/* _selectNodes : function(selection, append) { | |||||
var changed = false; | |||||
var i, iMax; | |||||
// TODO: the selectNodes method is a little messy, rework this | |||||
// check if the current selection equals the desired selection | |||||
var selectionAlreadyThere = true; | |||||
if (selection.length != this.selection.length) { | |||||
selectionAlreadyThere = false; | |||||
} | |||||
else { | |||||
for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { | |||||
if (selection[i] != this.selection[i]) { | |||||
selectionAlreadyThere = false; | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (selectionAlreadyThere) { | |||||
return changed; | |||||
} | |||||
if (append == undefined || append == false) { | |||||
// first deselect any selected node | |||||
var triggerSelect = false; | |||||
changed = this._unselectNodes(undefined, triggerSelect); | |||||
} | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
// add each of the new selections, but only when they are not duplicate | |||||
var id = selection[i]; | |||||
var isDuplicate = (this.selection.indexOf(id) != -1); | |||||
if (!isDuplicate) { | |||||
this.nodes[id].select(); | |||||
this.selection.push(id); | |||||
changed = true; | |||||
} | |||||
} | |||||
if (changed) { | |||||
// fire the select event | |||||
this._trigger('select', { | |||||
nodes: this.getSelection() | |||||
}); | |||||
} | |||||
return changed; | |||||
}, | |||||
*/ | |||||
}; | |||||
@ -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("img/graph/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("img/graph/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("img/graph/addNodeIcon.png"); | |||||
} | |||||
span.graph-manipulationUI.edit { | |||||
background-image: url("img/graph/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("img/graph/connectIcon.png"); | |||||
} | |||||
span.graph-manipulationUI.delete { | |||||
background-image: url("img/graph/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; | |||||
} |
@ -0,0 +1,66 @@ | |||||
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.active { | |||||
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95); | |||||
} | |||||
div.graph-navigation.up { | |||||
background-image: url("img/graph/upArrow.png"); | |||||
bottom:50px; | |||||
left:55px; | |||||
} | |||||
div.graph-navigation.down { | |||||
background-image: url("img/graph/downArrow.png"); | |||||
bottom:10px; | |||||
left:55px; | |||||
} | |||||
div.graph-navigation.left { | |||||
background-image: url("img/graph/leftArrow.png"); | |||||
bottom:10px; | |||||
left:15px; | |||||
} | |||||
div.graph-navigation.right { | |||||
background-image: url("img/graph/rightArrow.png"); | |||||
bottom:10px; | |||||
left:95px; | |||||
} | |||||
div.graph-navigation.zoomIn { | |||||
background-image: url("img/graph/plus.png"); | |||||
bottom:10px; | |||||
right:15px; | |||||
} | |||||
div.graph-navigation.zoomOut { | |||||
background-image: url("img/graph/minus.png"); | |||||
bottom:10px; | |||||
right:55px; | |||||
} | |||||
div.graph-navigation.zoomExtends { | |||||
background-image: url("img/graph/zoomExtends.png"); | |||||
bottom:50px; | |||||
right:15px; | |||||
} |
@ -0,0 +1,311 @@ | |||||
var HierarchicalLayoutMixin = { | |||||
_resetLevels : function() { | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
var node = this.nodes[nodeId]; | |||||
if (node.preassignedLevel == false) { | |||||
node.level = -1; | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This is the main function to layout the nodes in a hierarchical way. | |||||
* It checks if the node details are supplied correctly | |||||
* | |||||
* @private | |||||
*/ | |||||
_setupHierarchicalLayout : function() { | |||||
if (this.constants.hierarchicalLayout.enabled == true) { | |||||
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
this.constants.hierarchicalLayout.levelSeparation *= -1; | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); | |||||
} | |||||
// get the size of the largest hubs and check if the user has defined a level for a node. | |||||
var hubsize = 0; | |||||
var node, nodeId; | |||||
var definedLevel = false; | |||||
var undefinedLevel = false; | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level != -1) { | |||||
definedLevel = true; | |||||
} | |||||
else { | |||||
undefinedLevel = true; | |||||
} | |||||
if (hubsize < node.edges.length) { | |||||
hubsize = node.edges.length; | |||||
} | |||||
} | |||||
} | |||||
// if the user defined some levels but not all, alert and run without hierarchical layout | |||||
if (undefinedLevel == true && definedLevel == true) { | |||||
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); | |||||
this.zoomExtent(true,this.constants.clustering.enabled); | |||||
if (!this.constants.clustering.enabled) { | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
// setup the system to use hierarchical method. | |||||
this._changeConstants(); | |||||
// define levels if undefined by the users. Based on hubsize | |||||
if (undefinedLevel == true) { | |||||
this._determineLevels(hubsize); | |||||
} | |||||
// check the distribution of the nodes per level. | |||||
var distribution = this._getDistribution(); | |||||
// place the nodes on the canvas. This also stablilizes the system. | |||||
this._placeNodesByHierarchy(distribution); | |||||
// start the simulation. | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This function places the nodes on the canvas based on the hierarchial distribution. | |||||
* | |||||
* @param {Object} distribution | obtained by the function this._getDistribution() | |||||
* @private | |||||
*/ | |||||
_placeNodesByHierarchy : function(distribution) { | |||||
var nodeId, node; | |||||
// start placing all the level 0 nodes first. Then recursively position their branches. | |||||
for (nodeId in distribution[0].nodes) { | |||||
if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||||
node = distribution[0].nodes[nodeId]; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (node.xFixed) { | |||||
node.x = distribution[0].minPos; | |||||
node.xFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
else { | |||||
if (node.yFixed) { | |||||
node.y = distribution[0].minPos; | |||||
node.yFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||||
} | |||||
} | |||||
// stabilize the system after positioning. This function calls zoomExtent. | |||||
this._stabilize(); | |||||
}, | |||||
/** | |||||
* This function get the distribution of levels based on hubsize | |||||
* | |||||
* @returns {Object} | |||||
* @private | |||||
*/ | |||||
_getDistribution : function() { | |||||
var distribution = {}; | |||||
var nodeId, node; | |||||
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||||
// the fix of X is removed after the x value has been set. | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
node.xFixed = true; | |||||
node.yFixed = true; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
else { | |||||
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
if (!distribution.hasOwnProperty(node.level)) { | |||||
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||||
} | |||||
distribution[node.level].amount += 1; | |||||
distribution[node.level].nodes[node.id] = node; | |||||
} | |||||
} | |||||
// determine the largest amount of nodes of all levels | |||||
var maxCount = 0; | |||||
for (var level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
if (maxCount < distribution[level].amount) { | |||||
maxCount = distribution[level].amount; | |||||
} | |||||
} | |||||
} | |||||
// set the initial position and spacing of each nodes accordingly | |||||
for (var level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||||
distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||||
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||||
} | |||||
} | |||||
return distribution; | |||||
}, | |||||
/** | |||||
* this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||||
* | |||||
* @param hubsize | |||||
* @private | |||||
*/ | |||||
_determineLevels : function(hubsize) { | |||||
var nodeId, node; | |||||
// determine hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.edges.length == hubsize) { | |||||
node.level = 0; | |||||
} | |||||
} | |||||
} | |||||
// branch from hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level == 0) { | |||||
this._setLevel(1,node.edges,node.id); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Since hierarchical layout does not support: | |||||
* - smooth curves (based on the physics), | |||||
* - clustering (based on dynamic node counts) | |||||
* | |||||
* We disable both features so there will be no problems. | |||||
* | |||||
* @private | |||||
*/ | |||||
_changeConstants : function() { | |||||
this.constants.clustering.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this._loadSelectedForceSolver(); | |||||
this.constants.smoothCurves = false; | |||||
this._configureSmoothCurves(); | |||||
}, | |||||
/** | |||||
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||||
* on a X position that ensures there will be no overlap. | |||||
* | |||||
* @param edges | |||||
* @param parentId | |||||
* @param distribution | |||||
* @param parentLevel | |||||
* @private | |||||
*/ | |||||
_placeBranchNodes : function(edges, parentId, distribution, parentLevel) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||||
var nodeMoved = false; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (childNode.xFixed && childNode.level > parentLevel) { | |||||
childNode.xFixed = false; | |||||
childNode.x = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
else { | |||||
if (childNode.yFixed && childNode.level > parentLevel) { | |||||
childNode.yFixed = false; | |||||
childNode.y = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
if (nodeMoved == true) { | |||||
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||||
if (childNode.edges.length > 1) { | |||||
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||||
* | |||||
* @param level | |||||
* @param edges | |||||
* @param parentId | |||||
* @private | |||||
*/ | |||||
_setLevel : function(level, edges, parentId) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
if (childNode.level == -1 || childNode.level > level) { | |||||
childNode.level = level; | |||||
if (edges.length > 1) { | |||||
this._setLevel(level+1, childNode.edges, childNode.id); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Unfix nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_restoreNodes : function() { | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.nodes[nodeId].xFixed = false; | |||||
this.nodes[nodeId].yFixed = false; | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,435 @@ | |||||
/** | |||||
* Created by Alex on 2/4/14. | |||||
*/ | |||||
var manipulationMixin = { | |||||
/** | |||||
* clears the toolbar div element of children | |||||
* | |||||
* @private | |||||
*/ | |||||
_clearManipulatorBar : function() { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
}, | |||||
/** | |||||
* 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)) { | |||||
this[functionName] = this.cachedFunctions[functionName]; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Enable or disable edit-mode. | |||||
* | |||||
* @private | |||||
*/ | |||||
_toggleEditMode : function() { | |||||
this.editMode = !this.editMode; | |||||
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() | |||||
}, | |||||
/** | |||||
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |||||
* | |||||
* @private | |||||
*/ | |||||
_createManipulatorBar : function() { | |||||
// remove bound functions | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
// restore overloaded functions | |||||
this._restoreOverloadedFunctions(); | |||||
// resume calculation | |||||
this.freezeSimulation = false; | |||||
// reset global variables | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false; | |||||
if (this.editMode == true) { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
// add the icons to the manipulator div | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" + | |||||
"<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" + | |||||
"<div class='graph-seperatorLine'></div>" + | |||||
"<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" + | |||||
"<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>"; | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='graph-seperatorLine'></div>" + | |||||
"<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" + | |||||
"<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>"; | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='graph-seperatorLine'></div>" + | |||||
"<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" + | |||||
"<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>"; | |||||
} | |||||
// bind the icons | |||||
var addNodeButton = document.getElementById("graph-manipulate-addNode"); | |||||
addNodeButton.onclick = this._createAddNodeToolbar.bind(this); | |||||
var addEdgeButton = document.getElementById("graph-manipulate-connectNode"); | |||||
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
var editButton = document.getElementById("graph-manipulate-editNode"); | |||||
editButton.onclick = this._editNode.bind(this); | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
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'>" + this.constants.labels['edit'] + "</span></span>"; | |||||
var editModeButton = document.getElementById("graph-manipulate-editModeButton"); | |||||
editModeButton.onclick = this._toggleEditMode.bind(this); | |||||
} | |||||
}, | |||||
/** | |||||
* Create the toolbar for adding Nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_createAddNodeToolbar : function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
// create the toolbar contents | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" + | |||||
"<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='graph-seperatorLine'></div>" + | |||||
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" + | |||||
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
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. | |||||
this.boundFunction = this._addNode.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
}, | |||||
/** | |||||
* create the toolbar to connect nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_createAddEdgeToolbar : function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
this._unselectAll(true); | |||||
this.freezeSimulation = true; | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
this._unselectAll(); | |||||
this.forceAppendSelection = false; | |||||
this.blockConnectingEdgeSelection = true; | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" + | |||||
"<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='graph-seperatorLine'></div>" + | |||||
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" + | |||||
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
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. | |||||
this.boundFunction = this._handleConnect.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
// temporarily overload functions | |||||
this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
this._handleTouch = this._handleConnect; | |||||
this._handleOnRelease = this._finishConnect; | |||||
// redraw to show the unselect | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
_handleConnect : function(pointer) { | |||||
if (this._getSelectedNodeCount() == 0) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._selectObject(node,false); | |||||
// create a node the temporary line can look at | |||||
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetViaNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; | |||||
// create a temporary edge | |||||
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); | |||||
this.edges['connectionEdge'].from = node; | |||||
this.edges['connectionEdge'].connected = true; | |||||
this.edges['connectionEdge'].smooth = true; | |||||
this.edges['connectionEdge'].selected = true; | |||||
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; | |||||
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; | |||||
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
this._handleOnDrag = function(event) { | |||||
var pointer = this._getPointer(event.gesture.center); | |||||
this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x); | |||||
this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x); | |||||
this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y); | |||||
}; | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
_finishConnect : function(pointer) { | |||||
if (this._getSelectedNodeCount() == 1) { | |||||
// restore the drag function | |||||
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; | |||||
delete this.cachedFunctions["_handleOnDrag"]; | |||||
// remember the edge id | |||||
var connectFromId = this.edges['connectionEdge'].fromId; | |||||
// remove the temporary nodes and edge | |||||
delete this.edges['connectionEdge']; | |||||
delete this.sectors['support']['nodes']['targetNode']; | |||||
delete this.sectors['support']['nodes']['targetViaNode']; | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._createEdge(connectFromId,node.id); | |||||
this._createManipulatorBar(); | |||||
} | |||||
} | |||||
this._unselectAll(); | |||||
} | |||||
}, | |||||
/** | |||||
* Adds a node on the specified location | |||||
* | |||||
* @param {Object} pointer | |||||
*/ | |||||
_addNode : function() { | |||||
if (this._selectionIsEmpty() && this.editMode == true) { | |||||
var positionObject = this._pointerToPositionObject(this.pointerPosition); | |||||
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; | |||||
if (this.triggerFunctions.add) { | |||||
if (this.triggerFunctions.add.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.add(defaultData, function(finalizedData) { | |||||
me.nodesData.add(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels['addError']); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.nodesData.add(defaultData); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* connect two nodes with a new edge. | |||||
* | |||||
* @private | |||||
*/ | |||||
_createEdge : function(sourceNodeId,targetNodeId) { | |||||
if (this.editMode == true) { | |||||
var defaultData = {from:sourceNodeId, to:targetNodeId}; | |||||
if (this.triggerFunctions.connect) { | |||||
if (this.triggerFunctions.connect.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.connect(defaultData, function(finalizedData) { | |||||
me.edgesData.add(finalizedData); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["linkError"]); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.add(defaultData); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. | |||||
* | |||||
* @private | |||||
*/ | |||||
_editNode : function() { | |||||
if (this.triggerFunctions.edit && this.editMode == true) { | |||||
var node = this._getSelectedNode(); | |||||
var data = {id:node.id, | |||||
label: node.label, | |||||
group: node.group, | |||||
shape: node.shape, | |||||
color: { | |||||
background:node.color.background, | |||||
border:node.color.border, | |||||
highlight: { | |||||
background:node.color.highlight.background, | |||||
border:node.color.highlight.border | |||||
} | |||||
}}; | |||||
if (this.triggerFunctions.edit.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.edit(data, function (finalizedData) { | |||||
me.nodesData.update(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editError"]); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editBoundError"]); | |||||
} | |||||
}, | |||||
/** | |||||
* delete everything in the selection | |||||
* | |||||
* @private | |||||
*/ | |||||
_deleteSelected : function() { | |||||
if (!this._selectionIsEmpty() && this.editMode == true) { | |||||
if (!this._clusterInSelection()) { | |||||
var selectedNodes = this.getSelectedNodes(); | |||||
var selectedEdges = this.getSelectedEdges(); | |||||
if (this.triggerFunctions.del) { | |||||
var me = this; | |||||
var data = {nodes: selectedNodes, edges: selectedEdges}; | |||||
if (this.triggerFunctions.del.length = 2) { | |||||
this.triggerFunctions.del(data, function (finalizedData) { | |||||
me.edgesData.remove(finalizedData.edges); | |||||
me.nodesData.remove(finalizedData.nodes); | |||||
me._unselectAll(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteError"]) | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.remove(selectedEdges); | |||||
this.nodesData.remove(selectedNodes); | |||||
this._unselectAll(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteClusterError"]); | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,199 @@ | |||||
/** | |||||
* Created by Alex on 2/10/14. | |||||
*/ | |||||
var graphMixinLoaders = { | |||||
/** | |||||
* Load a mixin into the graph object | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
_loadMixin: function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
Graph.prototype[mixinFunction] = sourceVariable[mixinFunction]; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* removes a mixin from the graph object. | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
_clearMixin: function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
Graph.prototype[mixinFunction] = undefined; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the physics system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadPhysicsSystem: function () { | |||||
this._loadMixin(physicsMixin); | |||||
this._loadSelectedForceSolver(); | |||||
if (this.constants.configurePhysics == true) { | |||||
this._loadPhysicsConfiguration(); | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the cluster system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadClusterSystem: function () { | |||||
this.clusterSession = 0; | |||||
this.hubThreshold = 5; | |||||
this._loadMixin(ClusterMixin); | |||||
}, | |||||
/** | |||||
* Mixin the sector system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadSectorSystem: function () { | |||||
this.sectors = { }, | |||||
this.activeSector = ["default"]; | |||||
this.sectors["active"] = { }, | |||||
this.sectors["active"]["default"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.sectors["frozen"] = {}, | |||||
this.sectors["support"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields | |||||
this._loadMixin(SectorMixin); | |||||
}, | |||||
/** | |||||
* Mixin the selection system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadSelectionSystem: function () { | |||||
this.selectionObj = {nodes: {}, edges: {}}; | |||||
this._loadMixin(SelectionMixin); | |||||
}, | |||||
/** | |||||
* Mixin the navigationUI (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadManipulationSystem: function () { | |||||
// reset global variables -- these are used by the selection of nodes and edges. | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false | |||||
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'); | |||||
this.manipulationDiv.className = 'graph-manipulationDiv'; | |||||
this.manipulationDiv.id = 'graph-manipulationDiv'; | |||||
if (this.editMode == true) { | |||||
this.manipulationDiv.style.display = "block"; | |||||
} | |||||
else { | |||||
this.manipulationDiv.style.display = "none"; | |||||
} | |||||
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); | |||||
// create the manipulator toolbar | |||||
this._createManipulatorBar(); | |||||
} | |||||
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); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the navigation (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_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(); | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the hierarchical layout system. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadHierarchySystem: function () { | |||||
this._loadMixin(HierarchicalLayoutMixin); | |||||
} | |||||
}; |
@ -0,0 +1,205 @@ | |||||
/** | |||||
* Created by Alex on 1/22/14. | |||||
*/ | |||||
var NavigationMixin = { | |||||
_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 | |||||
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadNavigationElements : function() { | |||||
this._cleanNavigation(); | |||||
this.navigationDivs = {}; | |||||
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; | |||||
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; | |||||
this.navigationDivs['wrapper'] = document.createElement('div'); | |||||
this.navigationDivs['wrapper'].id = "graph-navigation_wrapper"; | |||||
this.navigationDivs['wrapper'].style.position = "absolute"; | |||||
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; | |||||
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; | |||||
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); | |||||
} | |||||
document.onmouseup = this._stopMovement.bind(this); | |||||
}, | |||||
/** | |||||
* this stops all movement induced by the navigation buttons | |||||
* | |||||
* @private | |||||
*/ | |||||
_stopMovement : function() { | |||||
this._xStopMoving(); | |||||
this._yStopMoving(); | |||||
this._stopZoom(); | |||||
}, | |||||
/** | |||||
* stops the actions performed by page up and down etc. | |||||
* | |||||
* @param event | |||||
* @private | |||||
*/ | |||||
_preventDefault : function(event) { | |||||
if (event !== undefined) { | |||||
if (event.preventDefault) { | |||||
event.preventDefault(); | |||||
} else { | |||||
event.returnValue = false; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen up | |||||
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
* To avoid this behaviour, we do the translation in the start loop. | |||||
* | |||||
* @private | |||||
*/ | |||||
_moveUp : function(event) { | |||||
this.yIncrement = this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen down | |||||
* @private | |||||
*/ | |||||
_moveDown : function(event) { | |||||
this.yIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['down'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen left | |||||
* @private | |||||
*/ | |||||
_moveLeft : function(event) { | |||||
this.xIncrement = this.constants.keyboard.speed.x; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen right | |||||
* @private | |||||
*/ | |||||
_moveRight : function(event) { | |||||
this.xIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['right'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Zoom in, using the same method as the movement. | |||||
* @private | |||||
*/ | |||||
_zoomIn : function(event) { | |||||
this.zoomIncrement = this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Zoom out | |||||
* @private | |||||
*/ | |||||
_zoomOut : function() { | |||||
this.zoomIncrement = -this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomOut'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Stop zooming and unhighlight the zoom controls | |||||
* @private | |||||
*/ | |||||
_stopZoom : function() { | |||||
this.zoomIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); | |||||
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); | |||||
} | |||||
}, | |||||
/** | |||||
* Stop moving in the Y direction and unHighlight the up and down | |||||
* @private | |||||
*/ | |||||
_yStopMoving : function() { | |||||
this.yIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); | |||||
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); | |||||
} | |||||
}, | |||||
/** | |||||
* Stop moving in the X direction and unHighlight left and right. | |||||
* @private | |||||
*/ | |||||
_xStopMoving : function() { | |||||
this.xIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); | |||||
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,570 @@ | |||||
var SelectionMixin = { | |||||
/** | |||||
* This function can be called from the _doInAllSectors function | |||||
* | |||||
* @param object | |||||
* @param overlappingNodes | |||||
* @private | |||||
*/ | |||||
_getNodesOverlappingWith : function(object, overlappingNodes) { | |||||
var nodes = this.nodes; | |||||
for (var nodeId in nodes) { | |||||
if (nodes.hasOwnProperty(nodeId)) { | |||||
if (nodes[nodeId].isOverlappingWith(object)) { | |||||
overlappingNodes.push(nodeId); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* retrieve all nodes 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 | |||||
*/ | |||||
_getAllNodesOverlappingWith : function (object) { | |||||
var overlappingNodes = []; | |||||
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); | |||||
return overlappingNodes; | |||||
}, | |||||
/** | |||||
* Return a position object in canvasspace from a single point in screenspace | |||||
* | |||||
* @param pointer | |||||
* @returns {{left: number, top: number, right: number, bottom: number}} | |||||
* @private | |||||
*/ | |||||
_pointerToPositionObject : function(pointer) { | |||||
var x = this._canvasToX(pointer.x); | |||||
var y = this._canvasToY(pointer.y); | |||||
return {left: x, | |||||
top: y, | |||||
right: x, | |||||
bottom: y}; | |||||
}, | |||||
/** | |||||
* Get the top node at the a specific point (like a click) | |||||
* | |||||
* @param {{x: Number, y: Number}} pointer | |||||
* @return {Node | null} node | |||||
* @private | |||||
*/ | |||||
_getNodeAt : function (pointer) { | |||||
// we first check if this is an navigation controls element | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); | |||||
// if there are overlapping nodes, select the last one, this is the | |||||
// one which is drawn on top of the others | |||||
if (overlappingNodes.length > 0) { | |||||
return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
}, | |||||
/** | |||||
* retrieve all edges overlapping with given object, selector is around center | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
_getEdgesOverlappingWith : function (object, overlappingEdges) { | |||||
var edges = this.edges; | |||||
for (var edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
if (edges[edgeId].isOverlappingWith(object)) { | |||||
overlappingEdges.push(edgeId); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* retrieve all nodes 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 | |||||
*/ | |||||
_getAllEdgesOverlappingWith : function (object) { | |||||
var overlappingEdges = []; | |||||
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); | |||||
return overlappingEdges; | |||||
}, | |||||
/** | |||||
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call | |||||
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. | |||||
* | |||||
* @param pointer | |||||
* @returns {null} | |||||
* @private | |||||
*/ | |||||
_getEdgeAt : function(pointer) { | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); | |||||
if (overlappingEdges.length > 0) { | |||||
return this.edges[overlappingEdges[overlappingEdges.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
}, | |||||
/** | |||||
* Add object to the selection array. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
_addToSelection : function(obj) { | |||||
if (obj instanceof Node) { | |||||
this.selectionObj.nodes[obj.id] = obj; | |||||
} | |||||
else { | |||||
this.selectionObj.edges[obj.id] = obj; | |||||
} | |||||
}, | |||||
/** | |||||
* Remove a single option from selection. | |||||
* | |||||
* @param {Object} obj | |||||
* @private | |||||
*/ | |||||
_removeFromSelection : function(obj) { | |||||
if (obj instanceof Node) { | |||||
delete this.selectionObj.nodes[obj.id]; | |||||
} | |||||
else { | |||||
delete this.selectionObj.edges[obj.id]; | |||||
} | |||||
}, | |||||
/** | |||||
* Unselect all. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_unselectAll : function(doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
this.selectionObj.nodes[nodeId].unselect(); | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
this.selectionObj.edges[edgeId].unselect();; | |||||
} | |||||
} | |||||
this.selectionObj = {nodes:{},edges:{}}; | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}, | |||||
/** | |||||
* Unselect all clusters. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_unselectClusters : function(doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
this.selectionObj.nodes[nodeId].unselect(); | |||||
this._removeFromSelection(this.selectionObj.nodes[nodeId]); | |||||
} | |||||
} | |||||
} | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}, | |||||
/** | |||||
* return the number of selected nodes | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedNodeCount : function() { | |||||
var count = 0; | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}, | |||||
/** | |||||
* return the number of selected nodes | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedNode : function() { | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
return null; | |||||
}, | |||||
/** | |||||
* return the number of selected edges | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedEdgeCount : function() { | |||||
var count = 0; | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}, | |||||
/** | |||||
* return the number of selected objects. | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedObjectCount : function() { | |||||
var count = 0; | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}, | |||||
/** | |||||
* Check if anything is selected | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
_selectionIsEmpty : function() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
return true; | |||||
}, | |||||
/** | |||||
* check if one of the selected nodes is a cluster. | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
_clusterInSelection : function() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
}, | |||||
/** | |||||
* select the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
_selectConnectedEdges : function(node) { | |||||
for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
var edge = node.dynamicEdges[i]; | |||||
edge.select(); | |||||
this._addToSelection(edge); | |||||
} | |||||
}, | |||||
/** | |||||
* unselect the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
_unselectConnectedEdges : function(node) { | |||||
for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
var edge = node.dynamicEdges[i]; | |||||
edge.unselect(); | |||||
this._removeFromSelection(edge); | |||||
} | |||||
}, | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @param {Boolean} append | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_selectObject : function(object, append, doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { | |||||
this._unselectAll(true); | |||||
} | |||||
if (object.selected == false) { | |||||
object.select(); | |||||
this._addToSelection(object); | |||||
if (object instanceof Node && this.blockConnectingEdgeSelection == false) { | |||||
this._selectConnectedEdges(object); | |||||
} | |||||
} | |||||
else { | |||||
object.unselect(); | |||||
this._removeFromSelection(object); | |||||
} | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}, | |||||
/** | |||||
* handles the selection part of the touch, only for navigation controls elements; | |||||
* Touch is triggered before tap, also before hold. Hold triggers after a while. | |||||
* This is the most responsive solution | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleTouch : function(pointer) { | |||||
}, | |||||
/** | |||||
* handles the selection part of the tap; | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleTap : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectObject(node,false); | |||||
} | |||||
else { | |||||
var edge = this._getEdgeAt(pointer); | |||||
if (edge != null) { | |||||
this._selectObject(edge,false); | |||||
} | |||||
else { | |||||
this._unselectAll(); | |||||
} | |||||
} | |||||
this.emit("click", this.getSelection()); | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* handles the selection part of the double tap and opens a cluster if needed | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleDoubleTap : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null && node !== undefined) { | |||||
// we reset the areaCenter here so the opening of the node will occur | |||||
this.areaCenter = {"x" : this._canvasToX(pointer.x), | |||||
"y" : this._canvasToY(pointer.y)}; | |||||
this.openCluster(node); | |||||
} | |||||
this.emit("doubleClick", this.getSelection()); | |||||
}, | |||||
/** | |||||
* Handle the onHold selection part | |||||
* | |||||
* @param pointer | |||||
* @private | |||||
*/ | |||||
_handleOnHold : function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectObject(node,true); | |||||
} | |||||
else { | |||||
var edge = this._getEdgeAt(pointer); | |||||
if (edge != null) { | |||||
this._selectObject(edge,true); | |||||
} | |||||
} | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* handle the onRelease event. These functions are here for the navigation controls module. | |||||
* | |||||
* @private | |||||
*/ | |||||
_handleOnRelease : function(pointer) { | |||||
}, | |||||
/** | |||||
* | |||||
* retrieve the currently selected objects | |||||
* @return {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelection : function() { | |||||
var nodeIds = this.getSelectedNodes(); | |||||
var edgeIds = this.getSelectedEdges(); | |||||
return {nodes:nodeIds, edges:edgeIds}; | |||||
}, | |||||
/** | |||||
* | |||||
* retrieve the currently selected nodes | |||||
* @return {String} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelectedNodes : function() { | |||||
var idArray = []; | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
idArray.push(nodeId); | |||||
} | |||||
} | |||||
return idArray | |||||
}, | |||||
/** | |||||
* | |||||
* retrieve the currently selected edges | |||||
* @return {Array} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelectedEdges : function() { | |||||
var idArray = []; | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
idArray.push(edgeId); | |||||
} | |||||
} | |||||
return idArray; | |||||
}, | |||||
/** | |||||
* select zero or more nodes | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
setSelection : function(selection) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var node = this.nodes[id]; | |||||
if (!node) { | |||||
throw new RangeError('Node with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(node,true,true); | |||||
} | |||||
this.redraw(); | |||||
}, | |||||
/** | |||||
* Validate the selection: remove ids of nodes which no longer exist | |||||
* @private | |||||
*/ | |||||
_updateSelection : function () { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (!this.nodes.hasOwnProperty(nodeId)) { | |||||
delete this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
if (!this.edges.hasOwnProperty(edgeId)) { | |||||
delete this.selectionObj.edges[edgeId]; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
@ -0,0 +1,375 @@ | |||||
/** | |||||
* Created by Alex on 2/10/14. | |||||
*/ | |||||
var barnesHutMixin = { | |||||
/** | |||||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
* The Barnes Hut method is used to speed up this N-body simulation. | |||||
* | |||||
* @private | |||||
*/ | |||||
_calculateNodeForces : function() { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != 0) { | |||||
var node; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
var nodeCount = nodeIndices.length; | |||||
this._formBarnesHutTree(nodes,nodeIndices); | |||||
var barnesHutTree = this.barnesHutTree; | |||||
// place the nodes one by one recursively | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
// starting with root is irrelevant, it never passes the BarnesHut condition | |||||
this._getForceContribution(barnesHutTree.root.children.NW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.NE,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SE,node); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
_getForceContribution : function(parentBranch,node) { | |||||
// we get no force contribution from an empty region | |||||
if (parentBranch.childrenCount > 0) { | |||||
var dx,dy,distance; | |||||
// get the distance from the center of mass to the node. | |||||
dx = parentBranch.centerOfMass.x - node.x; | |||||
dy = parentBranch.centerOfMass.y - node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// 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.1*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; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
* | |||||
* @param nodes | |||||
* @param nodeIndices | |||||
* @private | |||||
*/ | |||||
_formBarnesHutTree : function(nodes,nodeIndices) { | |||||
var node; | |||||
var nodeCount = nodeIndices.length; | |||||
var minX = Number.MAX_VALUE, | |||||
minY = Number.MAX_VALUE, | |||||
maxX =-Number.MAX_VALUE, | |||||
maxY =-Number.MAX_VALUE; | |||||
// get the range of the nodes | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
var x = nodes[nodeIndices[i]].x; | |||||
var y = nodes[nodeIndices[i]].y; | |||||
if (x < minX) { minX = x; } | |||||
if (x > maxX) { maxX = x; } | |||||
if (y < minY) { minY = y; } | |||||
if (y > maxY) { maxY = y; } | |||||
} | |||||
// make the range a square | |||||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||||
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||||
var minimumTreeSize = 1e-5; | |||||
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||||
var halfRootSize = 0.5 * rootSize; | |||||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
// construct the barnesHutTree | |||||
var barnesHutTree = {root:{ | |||||
centerOfMass:{x:0,y:0}, // Center of Mass | |||||
mass:0, | |||||
range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize, | |||||
minY:centerY-halfRootSize,maxY:centerY+halfRootSize}, | |||||
size: rootSize, | |||||
calcSize: 1 / rootSize, | |||||
children: {data:null}, | |||||
maxWidth: 0, | |||||
level: 0, | |||||
childrenCount: 4 | |||||
}}; | |||||
this._splitBranch(barnesHutTree.root); | |||||
// place the nodes one by one recursively | |||||
for (i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
this._placeInTree(barnesHutTree.root,node); | |||||
} | |||||
// make global | |||||
this.barnesHutTree = barnesHutTree | |||||
}, | |||||
_updateBranchMass : function(parentBranch, node) { | |||||
var totalMass = parentBranch.mass + node.mass; | |||||
var totalMassInv = 1/totalMass; | |||||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass; | |||||
parentBranch.centerOfMass.x *= totalMassInv; | |||||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass; | |||||
parentBranch.centerOfMass.y *= totalMassInv; | |||||
parentBranch.mass = totalMass; | |||||
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
}, | |||||
_placeInTree : function(parentBranch,node,skipMassUpdate) { | |||||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
// update the mass of the branch. | |||||
this._updateBranchMass(parentBranch,node); | |||||
} | |||||
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"); | |||||
} | |||||
else { // in SW | |||||
this._placeInRegion(parentBranch,node,"SW"); | |||||
} | |||||
} | |||||
else { // in NE or SE | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
this._placeInRegion(parentBranch,node,"NE"); | |||||
} | |||||
else { // in SE | |||||
this._placeInRegion(parentBranch,node,"SE"); | |||||
} | |||||
} | |||||
}, | |||||
_placeInRegion : function(parentBranch,node,region) { | |||||
switch (parentBranch.children[region].childrenCount) { | |||||
case 0: // place node here | |||||
parentBranch.children[region].children.data = node; | |||||
parentBranch.children[region].childrenCount = 1; | |||||
this._updateBranchMass(parentBranch.children[region],node); | |||||
break; | |||||
case 1: // convert into children | |||||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
// 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 += Math.random(); | |||||
node.y += Math.random(); | |||||
} | |||||
else { | |||||
this._splitBranch(parentBranch.children[region]); | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
} | |||||
break; | |||||
case 4: // place in branch | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
break; | |||||
} | |||||
}, | |||||
/** | |||||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
* after the split is complete. | |||||
* | |||||
* @param parentBranch | |||||
* @private | |||||
*/ | |||||
_splitBranch : function(parentBranch) { | |||||
// if the branch is filled with a node, replace the node in the new subset. | |||||
var containedNode = null; | |||||
if (parentBranch.childrenCount == 1) { | |||||
containedNode = parentBranch.children.data; | |||||
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||||
} | |||||
parentBranch.childrenCount = 4; | |||||
parentBranch.children.data = null; | |||||
this._insertRegion(parentBranch,"NW"); | |||||
this._insertRegion(parentBranch,"NE"); | |||||
this._insertRegion(parentBranch,"SW"); | |||||
this._insertRegion(parentBranch,"SE"); | |||||
if (containedNode != null) { | |||||
this._placeInTree(parentBranch,containedNode); | |||||
} | |||||
}, | |||||
/** | |||||
* This function subdivides the region into four new segments. | |||||
* Specifically, this inserts a single new segment. | |||||
* It fills the children section of the parentBranch | |||||
* | |||||
* @param parentBranch | |||||
* @param region | |||||
* @param parentRange | |||||
* @private | |||||
*/ | |||||
_insertRegion : function(parentBranch, region) { | |||||
var minX,maxX,minY,maxY; | |||||
var childSize = 0.5 * parentBranch.size; | |||||
switch (region) { | |||||
case "NW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "NE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "SW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
case "SE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
} | |||||
parentBranch.children[region] = { | |||||
centerOfMass:{x:0,y:0}, | |||||
mass:0, | |||||
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||||
size: 0.5 * parentBranch.size, | |||||
calcSize: 2 * parentBranch.calcSize, | |||||
children: {data:null}, | |||||
maxWidth: 0, | |||||
level: parentBranch.level+1, | |||||
childrenCount: 0 | |||||
}; | |||||
}, | |||||
/** | |||||
* This function is for debugging purposed, it draws the tree. | |||||
* | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
_drawTree : function(ctx,color) { | |||||
if (this.barnesHutTree !== undefined) { | |||||
ctx.lineWidth = 1; | |||||
this._drawBranch(this.barnesHutTree.root,ctx,color); | |||||
} | |||||
}, | |||||
/** | |||||
* This function is for debugging purposes. It draws the branches recursively. | |||||
* | |||||
* @param branch | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
_drawBranch : function(branch,ctx,color) { | |||||
if (color === undefined) { | |||||
color = "#FF0000"; | |||||
} | |||||
if (branch.childrenCount == 4) { | |||||
this._drawBranch(branch.children.NW,ctx); | |||||
this._drawBranch(branch.children.NE,ctx); | |||||
this._drawBranch(branch.children.SE,ctx); | |||||
this._drawBranch(branch.children.SW,ctx); | |||||
} | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.minY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.minY); | |||||
ctx.stroke(); | |||||
/* | |||||
if (branch.mass > 0) { | |||||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
ctx.stroke(); | |||||
} | |||||
*/ | |||||
} | |||||
}; |
@ -0,0 +1,64 @@ | |||||
/** | |||||
* Created by Alex on 2/10/14. | |||||
*/ | |||||
var hierarchalRepulsionMixin = { | |||||
/** | |||||
* Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
_calculateNodeForces: function () { | |||||
var dx, dy, distance, fx, fy, combinedClusterSize, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// approximation constants | |||||
var b = 5; | |||||
var a_base = 0.5 * -b; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; | |||||
var minimumDistance = nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
var a = a_base / minimumDistance; | |||||
if (distance < 2 * minimumDistance) { | |||||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
// normalize force with | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
else { | |||||
repulsingForce = repulsingForce / distance; | |||||
} | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,665 @@ | |||||
/** | |||||
* Created by Alex on 2/6/14. | |||||
*/ | |||||
var physicsMixin = { | |||||
/** | |||||
* Toggling barnes Hut calculation on and off. | |||||
* | |||||
* @private | |||||
*/ | |||||
_toggleBarnesHut: function () { | |||||
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; | |||||
this._loadSelectedForceSolver(); | |||||
this.moving = true; | |||||
this.start(); | |||||
}, | |||||
/** | |||||
* This loads the node force solver based on the barnes hut or repulsion algorithm | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadSelectedForceSolver: function () { | |||||
// this overloads the this._calculateNodeForces | |||||
if (this.constants.physics.barnesHut.enabled == true) { | |||||
this._clearMixin(repulsionMixin); | |||||
this._clearMixin(hierarchalRepulsionMixin); | |||||
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._loadMixin(barnesHutMixin); | |||||
} | |||||
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
this._clearMixin(barnesHutMixin); | |||||
this._clearMixin(repulsionMixin); | |||||
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; | |||||
this._loadMixin(hierarchalRepulsionMixin); | |||||
} | |||||
else { | |||||
this._clearMixin(barnesHutMixin); | |||||
this._clearMixin(hierarchalRepulsionMixin); | |||||
this.barnesHutTree = undefined; | |||||
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._loadMixin(repulsionMixin); | |||||
} | |||||
}, | |||||
/** | |||||
* Before calculating the forces, we check if we need to cluster to keep up performance and we check | |||||
* if there is more than one node. If it is just one node, we dont calculate anything. | |||||
* | |||||
* @private | |||||
*/ | |||||
_initializeForceCalculation: function () { | |||||
// stop calculation if there is only one node | |||||
if (this.nodeIndices.length == 1) { | |||||
this.nodes[this.nodeIndices[0]]._setForce(0, 0); | |||||
} | |||||
else { | |||||
// if there are too many nodes on screen, we cluster without repositioning | |||||
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { | |||||
this.clusterToFit(this.constants.clustering.reduceToNodes, false); | |||||
} | |||||
// we now start the force calculation | |||||
this._calculateForces(); | |||||
} | |||||
}, | |||||
/** | |||||
* Calculate the external forces acting on the nodes | |||||
* Forces are caused by: edges, repulsing forces between nodes, gravity | |||||
* @private | |||||
*/ | |||||
_calculateForces: function () { | |||||
// Gravity is required to keep separated groups from floating off | |||||
// the forces are reset to zero in this loop by using _setForce instead | |||||
// of _addForce | |||||
this._calculateGravitationalForces(); | |||||
this._calculateNodeForces(); | |||||
if (this.constants.smoothCurves == true) { | |||||
this._calculateSpringForcesWithSupport(); | |||||
} | |||||
else { | |||||
this._calculateSpringForces(); | |||||
} | |||||
}, | |||||
/** | |||||
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
* handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
* This function joins the datanodes and invisible (called support) nodes into one object. | |||||
* We do this so we do not contaminate this.nodes with the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
_updateCalculationNodes: function () { | |||||
if (this.constants.smoothCurves == true) { | |||||
this.calculationNodes = {}; | |||||
this.calculationNodeIndices = []; | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId] = this.nodes[nodeId]; | |||||
} | |||||
} | |||||
var supportNodes = this.sectors['support']['nodes']; | |||||
for (var supportNodeId in supportNodes) { | |||||
if (supportNodes.hasOwnProperty(supportNodeId)) { | |||||
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { | |||||
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
} | |||||
else { | |||||
supportNodes[supportNodeId]._setForce(0, 0); | |||||
} | |||||
} | |||||
} | |||||
for (var idx in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(idx)) { | |||||
this.calculationNodeIndices.push(idx); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
this.calculationNodes = this.nodes; | |||||
this.calculationNodeIndices = this.nodeIndices; | |||||
} | |||||
}, | |||||
/** | |||||
* this function applies the central gravity effect to keep groups from floating off | |||||
* | |||||
* @private | |||||
*/ | |||||
_calculateGravitationalForces: function () { | |||||
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" && gravity != 0) { | |||||
dx = -node.x; | |||||
dy = -node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
node.fx = dx * gravityForce; | |||||
node.fy = dy * gravityForce; | |||||
} | |||||
else { | |||||
node.fx = 0; | |||||
node.fy = 0; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||||
* | |||||
* @private | |||||
*/ | |||||
_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 | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
length = Math.sqrt(dx * dx + dy * dy); | |||||
if (length == 0) { | |||||
length = 0.01; | |||||
} | |||||
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; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
_calculateSpringForcesWithSupport: function () { | |||||
var edgeLength, edge, edgeId, combinedClusterSize; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
if (edge.via != null) { | |||||
var node1 = edge.to; | |||||
var node2 = edge.via; | |||||
var node3 = edge.from; | |||||
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; | |||||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||||
* | |||||
* @param node1 | |||||
* @param node2 | |||||
* @param edgeLength | |||||
* @private | |||||
*/ | |||||
_calculateSpringForce: function (node1, node2, edgeLength) { | |||||
var dx, dy, fx, fy, springForce, length; | |||||
dx = (node1.x - node2.x); | |||||
dy = (node1.y - node2.y); | |||||
length = Math.sqrt(dx * dx + dy * dy); | |||||
if (length == 0) { | |||||
length = 0.01; | |||||
} | |||||
springForce = this.constants.physics.springConstant * (edgeLength - length) / length; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
node1.fx += fx; | |||||
node1.fy += fy; | |||||
node2.fx -= fx; | |||||
node2.fy -= fy; | |||||
}, | |||||
/** | |||||
* Load the HTML for the physics config and bind it | |||||
* @private | |||||
*/ | |||||
_loadPhysicsConfiguration: function () { | |||||
if (this.physicsConfiguration === undefined) { | |||||
this.backupConstants = {}; | |||||
util.copyObject(this.constants, this.backupConstants); | |||||
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; | |||||
this.physicsConfiguration = document.createElement('div'); | |||||
this.physicsConfiguration.className = "PhysicsConfiguration"; | |||||
this.physicsConfiguration.innerHTML = '' + | |||||
'<table><tr><td><b>Simulation Mode:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_BH_table" style="display:none">' + | |||||
'<tr><td><b>Barnes Hut</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_R_table" style="display:none">' + | |||||
'<tr><td><b>Repulsion</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_H_table" style="display:none">' + | |||||
'<tr><td width="150"><b>Hierarchical</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table><tr><td><b>Options:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' + | |||||
'</tr>' + | |||||
'</table>' | |||||
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); | |||||
this.optionsDiv = document.createElement("div"); | |||||
this.optionsDiv.style.fontSize = "14px"; | |||||
this.optionsDiv.style.fontFamily = "verdana"; | |||||
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); | |||||
var rangeElement; | |||||
rangeElement = document.getElementById('graph_BH_gc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); | |||||
rangeElement = document.getElementById('graph_BH_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_BH_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_BH_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_BH_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_R_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_R_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_R_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_R_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_R_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_H_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_H_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_H_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_H_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_direction'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); | |||||
rangeElement = document.getElementById('graph_H_levsep'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); | |||||
rangeElement = document.getElementById('graph_H_nspac'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
var radioButton3 = document.getElementById("graph_physicsMethod3"); | |||||
radioButton2.checked = true; | |||||
if (this.constants.physics.barnesHut.enabled) { | |||||
radioButton1.checked = true; | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled) { | |||||
radioButton3.checked = true; | |||||
} | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
var graph_repositionNodes = document.getElementById("graph_repositionNodes"); | |||||
var graph_generateOptions = document.getElementById("graph_generateOptions"); | |||||
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); | |||||
graph_repositionNodes.onclick = graphRepositionNodes.bind(this); | |||||
graph_generateOptions.onclick = graphGenerateOptions.bind(this); | |||||
if (this.constants.smoothCurves == true) { | |||||
graph_toggleSmooth.style.background = "#A4FF56"; | |||||
} | |||||
else { | |||||
graph_toggleSmooth.style.background = "#FF8532"; | |||||
} | |||||
switchConfigurations.apply(this); | |||||
radioButton1.onchange = switchConfigurations.bind(this); | |||||
radioButton2.onchange = switchConfigurations.bind(this); | |||||
radioButton3.onchange = switchConfigurations.bind(this); | |||||
} | |||||
}, | |||||
_overWriteGraphConstants: function (constantsVariableName, value) { | |||||
var nameArray = constantsVariableName.split("_"); | |||||
if (nameArray.length == 1) { | |||||
this.constants[nameArray[0]] = value; | |||||
} | |||||
else if (nameArray.length == 2) { | |||||
this.constants[nameArray[0]][nameArray[1]] = value; | |||||
} | |||||
else if (nameArray.length == 3) { | |||||
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; | |||||
} | |||||
} | |||||
}; | |||||
function graphToggleSmoothCurves () { | |||||
this.constants.smoothCurves = !this.constants.smoothCurves; | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this._configureSmoothCurves(false); | |||||
}; | |||||
function graphRepositionNodes () { | |||||
for (var nodeId in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; | |||||
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; | |||||
} | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled == true) { | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
else { | |||||
this.repositionNodes(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
}; | |||||
function graphGenerateOptions () { | |||||
var options = "No options are required, default values used."; | |||||
var optionsSpecific = []; | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
if (radioButton1.checked == true) { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
if (optionsSpecific.length == 0) {options = "var options = {";} | |||||
else {options += ", "} | |||||
options += "smoothCurves: " + this.constants.smoothCurves; | |||||
} | |||||
if (options != "No options are required, default values used.") { | |||||
options += '};' | |||||
} | |||||
} | |||||
else if (radioButton2.checked == true) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {enabled: false}"; | |||||
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += ", repulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (optionsSpecific.length == 0) {options += "}"} | |||||
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
options += ", smoothCurves: " + this.constants.smoothCurves; | |||||
} | |||||
options += '};' | |||||
} | |||||
else { | |||||
options = "var options = {"; | |||||
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += "physics: {hierarchicalRepulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", "; | |||||
} | |||||
} | |||||
options += '}},'; | |||||
} | |||||
options += 'hierarchicalLayout: {'; | |||||
optionsSpecific = []; | |||||
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} | |||||
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} | |||||
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} | |||||
if (optionsSpecific.length != 0) { | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}' | |||||
} | |||||
else { | |||||
options += "enabled:true}"; | |||||
} | |||||
options += '};' | |||||
} | |||||
this.optionsDiv.innerHTML = options; | |||||
}; | |||||
function switchConfigurations () { | |||||
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; | |||||
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; | |||||
var tableId = "graph_" + radioButton + "_table"; | |||||
var table = document.getElementById(tableId); | |||||
table.style.display = "block"; | |||||
for (var i = 0; i < ids.length; i++) { | |||||
if (ids[i] != tableId) { | |||||
table = document.getElementById(ids[i]); | |||||
table.style.display = "none"; | |||||
} | |||||
} | |||||
this._restoreNodes(); | |||||
if (radioButton == "R") { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
} | |||||
else if (radioButton == "H") { | |||||
this.constants.hierarchicalLayout.enabled = true; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = true; | |||||
} | |||||
this._loadSelectedForceSolver(); | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
function showValueOfRange (id,map,constantsVariableName) { | |||||
var valueId = id + "_value"; | |||||
var rangeValue = document.getElementById(id).value; | |||||
if (map instanceof Array) { | |||||
document.getElementById(valueId).value = map[parseInt(rangeValue)]; | |||||
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); | |||||
} | |||||
else { | |||||
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); | |||||
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); | |||||
} | |||||
if (constantsVariableName == "hierarchicalLayout_direction" || | |||||
constantsVariableName == "hierarchicalLayout_levelSeparation" || | |||||
constantsVariableName == "hierarchicalLayout_nodeSpacing") { | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
}; | |||||
@ -0,0 +1,66 @@ | |||||
/** | |||||
* Created by Alex on 2/10/14. | |||||
*/ | |||||
var repulsionMixin = { | |||||
/** | |||||
* Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
_calculateNodeForces: function () { | |||||
var dx, dy, angle, distance, fx, fy, combinedClusterSize, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// approximation constants | |||||
var a_base = -2 / 3; | |||||
var b = 4 / 3; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||||
var minimumDistance = nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); | |||||
var a = a_base / minimumDistance; | |||||
if (distance < 2 * minimumDistance) { | |||||
if (distance < 0.5 * minimumDistance) { | |||||
repulsingForce = 1.0; | |||||
} | |||||
else { | |||||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
} | |||||
// amplify the repulsion for clusters. | |||||
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; | |||||
repulsingForce = repulsingForce / distance; | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,172 +0,0 @@ | |||||
/** | |||||
* @constructor Controller | |||||
* | |||||
* A Controller controls the reflows and repaints of all visual components | |||||
*/ | |||||
function Controller () { | |||||
this.id = util.randomUUID(); | |||||
this.components = {}; | |||||
this.repaintTimer = undefined; | |||||
this.reflowTimer = undefined; | |||||
} | |||||
/** | |||||
* Add a component to the controller | |||||
* @param {Component} component | |||||
*/ | |||||
Controller.prototype.add = function add(component) { | |||||
// validate the component | |||||
if (component.id == undefined) { | |||||
throw new Error('Component has no field id'); | |||||
} | |||||
if (!(component instanceof Component) && !(component instanceof Controller)) { | |||||
throw new TypeError('Component must be an instance of ' + | |||||
'prototype Component or Controller'); | |||||
} | |||||
// add the component | |||||
component.controller = this; | |||||
this.components[component.id] = component; | |||||
}; | |||||
/** | |||||
* Remove a component from the controller | |||||
* @param {Component | String} component | |||||
*/ | |||||
Controller.prototype.remove = function remove(component) { | |||||
var id; | |||||
for (id in this.components) { | |||||
if (this.components.hasOwnProperty(id)) { | |||||
if (id == component || this.components[id] == component) { | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (id) { | |||||
delete this.components[id]; | |||||
} | |||||
}; | |||||
/** | |||||
* Request a reflow. The controller will schedule a reflow | |||||
* @param {Boolean} [force] If true, an immediate reflow is forced. Default | |||||
* is false. | |||||
*/ | |||||
Controller.prototype.requestReflow = function requestReflow(force) { | |||||
if (force) { | |||||
this.reflow(); | |||||
} | |||||
else { | |||||
if (!this.reflowTimer) { | |||||
var me = this; | |||||
this.reflowTimer = setTimeout(function () { | |||||
me.reflowTimer = undefined; | |||||
me.reflow(); | |||||
}, 0); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Request a repaint. The controller will schedule a repaint | |||||
* @param {Boolean} [force] If true, an immediate repaint is forced. Default | |||||
* is false. | |||||
*/ | |||||
Controller.prototype.requestRepaint = function requestRepaint(force) { | |||||
if (force) { | |||||
this.repaint(); | |||||
} | |||||
else { | |||||
if (!this.repaintTimer) { | |||||
var me = this; | |||||
this.repaintTimer = setTimeout(function () { | |||||
me.repaintTimer = undefined; | |||||
me.repaint(); | |||||
}, 0); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Repaint all components | |||||
*/ | |||||
Controller.prototype.repaint = function repaint() { | |||||
var changed = false; | |||||
// cancel any running repaint request | |||||
if (this.repaintTimer) { | |||||
clearTimeout(this.repaintTimer); | |||||
this.repaintTimer = undefined; | |||||
} | |||||
var done = {}; | |||||
function repaint(component, id) { | |||||
if (!(id in done)) { | |||||
// first repaint the components on which this component is dependent | |||||
if (component.depends) { | |||||
component.depends.forEach(function (dep) { | |||||
repaint(dep, dep.id); | |||||
}); | |||||
} | |||||
if (component.parent) { | |||||
repaint(component.parent, component.parent.id); | |||||
} | |||||
// repaint the component itself and mark as done | |||||
changed = component.repaint() || changed; | |||||
done[id] = true; | |||||
} | |||||
} | |||||
util.forEach(this.components, repaint); | |||||
// immediately reflow when needed | |||||
if (changed) { | |||||
this.reflow(); | |||||
} | |||||
// TODO: limit the number of nested reflows/repaints, prevent loop | |||||
}; | |||||
/** | |||||
* Reflow all components | |||||
*/ | |||||
Controller.prototype.reflow = function reflow() { | |||||
var resized = false; | |||||
// cancel any running repaint request | |||||
if (this.reflowTimer) { | |||||
clearTimeout(this.reflowTimer); | |||||
this.reflowTimer = undefined; | |||||
} | |||||
var done = {}; | |||||
function reflow(component, id) { | |||||
if (!(id in done)) { | |||||
// first reflow the components on which this component is dependent | |||||
if (component.depends) { | |||||
component.depends.forEach(function (dep) { | |||||
reflow(dep, dep.id); | |||||
}); | |||||
} | |||||
if (component.parent) { | |||||
reflow(component.parent, component.parent.id); | |||||
} | |||||
// reflow the component itself and mark as done | |||||
resized = component.reflow() || resized; | |||||
done[id] = true; | |||||
} | |||||
} | |||||
util.forEach(this.components, reflow); | |||||
// immediately repaint when needed | |||||
if (resized) { | |||||
this.repaint(); | |||||
} | |||||
// TODO: limit the number of nested reflows/repaints, prevent loop | |||||
}; |
@ -1,113 +0,0 @@ | |||||
/** | |||||
* A content panel can contain a groupset or an itemset, and can handle | |||||
* vertical scrolling | |||||
* @param {Component} [parent] | |||||
* @param {Component[]} [depends] Components on which this components depends | |||||
* (except for the parent) | |||||
* @param {Object} [options] Available parameters: | |||||
* {String | Number | function} [left] | |||||
* {String | Number | function} [top] | |||||
* {String | Number | function} [width] | |||||
* {String | Number | function} [height] | |||||
* {String | function} [className] | |||||
* @constructor ContentPanel | |||||
* @extends Panel | |||||
*/ | |||||
function ContentPanel(parent, depends, options) { | |||||
this.id = util.randomUUID(); | |||||
this.parent = parent; | |||||
this.depends = depends; | |||||
this.options = options || {}; | |||||
} | |||||
ContentPanel.prototype = new Component(); | |||||
/** | |||||
* Set options. Will extend the current options. | |||||
* @param {Object} [options] Available parameters: | |||||
* {String | function} [className] | |||||
* {String | Number | function} [left] | |||||
* {String | Number | function} [top] | |||||
* {String | Number | function} [width] | |||||
* {String | Number | function} [height] | |||||
*/ | |||||
ContentPanel.prototype.setOptions = Component.prototype.setOptions; | |||||
/** | |||||
* Get the container element of the panel, which can be used by a child to | |||||
* add its own widgets. | |||||
* @returns {HTMLElement} container | |||||
*/ | |||||
ContentPanel.prototype.getContainer = function () { | |||||
return this.frame; | |||||
}; | |||||
/** | |||||
* Repaint the component | |||||
* @return {Boolean} changed | |||||
*/ | |||||
ContentPanel.prototype.repaint = function () { | |||||
var changed = 0, | |||||
update = util.updateProperty, | |||||
asSize = util.option.asSize, | |||||
options = this.options, | |||||
frame = this.frame; | |||||
if (!frame) { | |||||
frame = document.createElement('div'); | |||||
frame.className = 'content-panel'; | |||||
var className = options.className; | |||||
if (className) { | |||||
if (typeof className == 'function') { | |||||
util.addClassName(frame, String(className())); | |||||
} | |||||
else { | |||||
util.addClassName(frame, String(className)); | |||||
} | |||||
} | |||||
this.frame = frame; | |||||
changed += 1; | |||||
} | |||||
if (!frame.parentNode) { | |||||
if (!this.parent) { | |||||
throw new Error('Cannot repaint panel: no parent attached'); | |||||
} | |||||
var parentContainer = this.parent.getContainer(); | |||||
if (!parentContainer) { | |||||
throw new Error('Cannot repaint panel: parent has no container element'); | |||||
} | |||||
parentContainer.appendChild(frame); | |||||
changed += 1; | |||||
} | |||||
changed += update(frame.style, 'top', asSize(options.top, '0px')); | |||||
changed += update(frame.style, 'left', asSize(options.left, '0px')); | |||||
changed += update(frame.style, 'width', asSize(options.width, '100%')); | |||||
changed += update(frame.style, 'height', asSize(options.height, '100%')); | |||||
return (changed > 0); | |||||
}; | |||||
/** | |||||
* Reflow the component | |||||
* @return {Boolean} resized | |||||
*/ | |||||
ContentPanel.prototype.reflow = function () { | |||||
var changed = 0, | |||||
update = util.updateProperty, | |||||
frame = this.frame; | |||||
if (frame) { | |||||
changed += update(this, 'top', frame.offsetTop); | |||||
changed += update(this, 'left', frame.offsetLeft); | |||||
changed += update(this, 'width', frame.offsetWidth); | |||||
changed += update(this, 'height', frame.offsetHeight); | |||||
} | |||||
else { | |||||
changed += 1; | |||||
} | |||||
return (changed > 0); | |||||
}; |