Browse Source

Merge branch 'develop' into hammerjs2

Conflicts:
	lib/network/Network.js
	lib/network/mixins/ManipulationMixin.js
	lib/network/mixins/NavigationMixin.js
	lib/timeline/Core.js
	test/timeline.html
flowchartTest
jos 9 years ago
parent
commit
6c01a1471d
60 changed files with 29727 additions and 27361 deletions
  1. +80
    -0
      HISTORY.md
  2. +4
    -0
      README.md
  3. +1
    -1
      bower.json
  4. +31
    -17
      dist/vis.css
  5. +27404
    -26684
      dist/vis.js
  6. +1
    -1
      dist/vis.map
  7. +1
    -1
      dist/vis.min.css
  8. +16
    -16
      dist/vis.min.js
  9. +24
    -0
      docs/dataset.html
  10. +120
    -2
      docs/dataview.html
  11. +22
    -0
      docs/graph2d.html
  12. +145
    -16
      docs/network.html
  13. +57
    -5
      docs/timeline.html
  14. +64
    -0
      examples/graph2d/19_labels.html
  15. +11
    -7
      examples/network/06_groups.html
  16. +17
    -4
      examples/network/26_staticSmoothCurves.html
  17. +2
    -0
      examples/network/27_world_cup_network.html
  18. +10
    -27
      examples/network/29_neighbourhood_highlight.html
  19. +166
    -0
      examples/network/38_node_as_icon.html
  20. +1
    -0
      examples/network/index.html
  21. +54
    -0
      examples/timeline/33_custom_snapping.html
  22. +74
    -0
      examples/timeline/34_add_custom_timebar.html
  23. +2
    -1
      examples/timeline/index.html
  24. +2
    -1
      gulpfile.js
  25. +24
    -1
      lib/DOMutil.js
  26. +11
    -2
      lib/DataSet.js
  27. +47
    -0
      lib/DataView.js
  28. +84
    -33
      lib/network/Edge.js
  29. +46
    -19
      lib/network/Groups.js
  30. +18
    -9
      lib/network/Images.js
  31. +406
    -159
      lib/network/Network.js
  32. +108
    -28
      lib/network/Node.js
  33. +11
    -16
      lib/network/Popup.js
  34. +18
    -17
      lib/network/css/network-manipulation.css
  35. +13
    -0
      lib/network/css/network-tooltip.css
  36. +85
    -93
      lib/network/mixins/ClusterMixin.js
  37. +1
    -1
      lib/network/mixins/HierarchicalLayoutMixin.js
  38. +75
    -49
      lib/network/mixins/ManipulationMixin.js
  39. +15
    -19
      lib/network/mixins/NavigationMixin.js
  40. +2
    -2
      lib/network/mixins/SelectionMixin.js
  41. +6
    -3
      lib/network/mixins/physics/PhysicsMixin.js
  42. +121
    -11
      lib/timeline/Core.js
  43. +0
    -12
      lib/timeline/DataStep.js
  44. +2
    -3
      lib/timeline/Graph2d.js
  45. +5
    -4
      lib/timeline/Range.js
  46. +34
    -31
      lib/timeline/TimeStep.js
  47. +21
    -6
      lib/timeline/Timeline.js
  48. +16
    -3
      lib/timeline/component/CustomTime.js
  49. +0
    -10
      lib/timeline/component/DataAxis.js
  50. +79
    -21
      lib/timeline/component/ItemSet.js
  51. +9
    -1
      lib/timeline/component/LineGraph.js
  52. +7
    -12
      lib/timeline/component/TimeAxis.js
  53. +1
    -1
      lib/timeline/component/graph2d_types/points.js
  54. +42
    -0
      lib/util.js
  55. +1
    -3
      misc/how_to_publish.md
  56. +9
    -9
      package.json
  57. +9
    -0
      test/DataSet.test.js
  58. +81
    -0
      test/DataView.test.js
  59. +5
    -0
      test/timeline.html
  60. +6
    -0
      test/timeline_groups.html

+ 80
- 0
HISTORY.md View File

@ -2,6 +2,86 @@
http://visjs.org http://visjs.org
## not yet released, version 3.10.1-SNAPSHOT
### Network
- (added gradient coloring for lines, but set for release in 4.0 due to required refactoring of options)
- Fixed bug where a network that has frozen physics would resume redrawing after setData, setOptions etc.
- (add docs) Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default.
- (add docs) Added two new static smooth curves modes: curveCW and curve CCW.
- Added request redraw for certain internal processes to reduce number of draw calls.
- Added pull request for usage of Icons. Thanks @Dude9177!
- Allow hierarchical view to be set in setOptions.
- Fixed manipulation bar for mobile.
### Graph2d
### Timeline
- Fixed not property initializing with a DataView for groups.
- Merged add custom timebar functionality, thanks @aytech!
- Fixed #664: end of item not restored when canceling a move event.
## 2015-02-11, version 3.10.0
### Network
- Added option bindToWindow (default true) to choose whether the keyboard binds are global or to the network div.
- Improved images handling so broken images are shown on all references of images that are broken.
- Added getConnectedNodes method.
- Added fontSizeMin, fontSizeMax, fontSizeMaxVisible, scaleFontWithValue, fontDrawThreshold to Nodes.
- Added fade in of labels (on nodes) near the fontDrawThreshold.
- Added nodes option to zoomExtent to zoom in on specific set of nodes.
- Added stabilizationIterationsDone event which fires at the end of the internal stabilization run. Does not imply that the network is stabilized.
- Added freezeSimulation method.
- Added clusterByZoom option.
- Added class name 'network-tooltip' to the tooltip, allowing custom styling.
- Fixed bug when redrawing was not right on zoomed-out browsers.
- Added opacity option to edges. Opacity is only used for the unselected state.
- Fixed bug where selections from removed data elements persisted.
### Timeline
- `Timeline.redraw()` now also recalculates the size of items.
- Implemented option `snap: function` to customize snapping to nice dates
when dragging items.
- Implemented option `timeAxis: {scale: string, step: number}` to set a
fixed scale.
- Fixed width of range items not always being maintained when moving due to
snapping to nice dates.
- Fixed not being able to drag items to an other group on mobile devices.
- Fixed `setWindow` not working when applying an interval larger than the
configured `zoomMax`.
### DataSet/DataView
- Added property `length` holding the total number of items to the `DataSet`
and `DataView`.
- Added a method `refresh()` to the `DataView`, to update filter results.
- Fixed a bug in the `DataSet` returning an empty object instead of `null` when
no item was found when using both a filter and specifying fields.
## 2015-01-16, version 3.9.1
### General
- Fixed wrong distribution file deployed on the website and the downloadable
zip file.
### Network
- Fixed bug where opening a cluster with smoothCurves off caused one child to go crazy.
- Fixed bug where zoomExtent does not work as expected.
- Fixed nodes color data being overridden when having a group and a dataset update query.
- Decoupled animation from physics simulation.
- Fixed scroll being blocked if zoomable is false.
## 2015-01-16, version 3.9.0 ## 2015-01-16, version 3.9.0
### Network ### Network

+ 4
- 0
README.md View File

@ -28,6 +28,10 @@ Install via bower:
bower install vis bower install vis
Link via cdnjs:
http://cdnjs.com
Or download the library from the github project: Or download the library from the github project:
[https://github.com/almende/vis.git](https://github.com/almende/vis.git). [https://github.com/almende/vis.git](https://github.com/almende/vis.git).

+ 1
- 1
bower.json View File

@ -1,6 +1,6 @@
{ {
"name": "vis", "name": "vis",
"version": "3.9.0",
"version": "3.10.1-SNAPSHOT",
"main": ["dist/vis.min.js", "dist/vis.min.css"], "main": ["dist/vis.min.js", "dist/vis.min.css"],
"description": "A dynamic, browser-based visualization library.", "description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/", "homepage": "http://visjs.org/",

+ 31
- 17
dist/vis.css View File

@ -613,9 +613,8 @@ div.network-manipulationDiv {
div.network-manipulation-editMode { div.network-manipulation-editMode {
position:absolute; position:absolute;
left: 0; left: 0;
top: 0;
top: 15px;
height: 30px; height: 30px;
margin-top:20px;
} }
div.network-manipulation-closeDiv { div.network-manipulation-closeDiv {
@ -641,7 +640,9 @@ div.network-manipulation-closeDiv:hover {
opacity: 0.6; opacity: 0.6;
} }
span.network-manipulationUI {
div.network-manipulationUI {
position:relative;
top:-7px;
font-family: verdana; font-family: verdana;
font-size: 12px; font-size: 12px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
@ -650,7 +651,7 @@ span.network-manipulationUI {
background-position: 0px 0px; background-position: 0px 0px;
background-repeat:no-repeat; background-repeat:no-repeat;
height:24px; height:24px;
margin: -14px 0px 0px 10px;
margin: 0px 0px 0px 10px;
vertical-align:middle; vertical-align:middle;
cursor: pointer; cursor: pointer;
padding: 0px 8px 0px 8px; padding: 0px 8px 0px 8px;
@ -662,57 +663,57 @@ span.network-manipulationUI {
user-select: none; user-select: none;
} }
span.network-manipulationUI:hover {
div.network-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
} }
span.network-manipulationUI:active {
div.network-manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
} }
span.network-manipulationUI.back {
div.network-manipulationUI.back {
background-image: url("img/network/backIcon.png"); background-image: url("img/network/backIcon.png");
} }
span.network-manipulationUI.none:hover {
div.network-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default; cursor: default;
} }
span.network-manipulationUI.none:active {
div.network-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
} }
span.network-manipulationUI.none {
div.network-manipulationUI.none {
padding: 0; padding: 0;
} }
span.network-manipulationUI.notification{
div.network-manipulationUI.notification{
margin: 2px; margin: 2px;
font-weight: bold; font-weight: bold;
} }
span.network-manipulationUI.add {
div.network-manipulationUI.add {
background-image: url("img/network/addNodeIcon.png"); background-image: url("img/network/addNodeIcon.png");
} }
span.network-manipulationUI.edit {
div.network-manipulationUI.edit {
background-image: url("img/network/editIcon.png"); background-image: url("img/network/editIcon.png");
} }
span.network-manipulationUI.edit.editmode {
div.network-manipulationUI.edit.editmode {
background-color: #fcfcfc; background-color: #fcfcfc;
border-style:solid; border-style:solid;
border-width:1px; border-width:1px;
border-color: #cccccc; border-color: #cccccc;
} }
span.network-manipulationUI.connect {
div.network-manipulationUI.connect {
background-image: url("img/network/connectIcon.png"); background-image: url("img/network/connectIcon.png");
} }
span.network-manipulationUI.delete {
div.network-manipulationUI.delete {
background-image: url("img/network/deleteIcon.png"); background-image: url("img/network/deleteIcon.png");
} }
/* top right bottom left */ /* top right bottom left */
span.network-manipulationLabel {
div.network-manipulationLabel {
margin: 0px 0px 0px 23px; margin: 0px 0px 0px 23px;
line-height: 25px; line-height: 25px;
} }
@ -791,4 +792,17 @@ div.network-navigation.zoomExtends {
background-image: url("img/network/zoomExtends.png"); background-image: url("img/network/zoomExtends.png");
bottom:50px; bottom:50px;
right:15px; right:15px;
}
div.network-tooltip {
position: absolute;
visibility: hidden;
padding: 5px;
white-space: nowrap;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
border: 1px solid;
box-shadow: 3px 3px 10px rgba(128, 128, 128, 0.5);
} }

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


+ 1
- 1
dist/vis.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


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


+ 24
- 0
docs/dataset.html View File

@ -21,6 +21,7 @@
<li><a href="#Example">Example</a></li> <li><a href="#Example">Example</a></li>
<li><a href="#Construction">Construction</a></li> <li><a href="#Construction">Construction</a></li>
<li><a href="#Methods">Methods</a></li> <li><a href="#Methods">Methods</a></li>
<li><a href="#Properties">Properties</a></li>
<li><a href="#Subscriptions">Subscriptions</a></li> <li><a href="#Subscriptions">Subscriptions</a></li>
<li><a href="#Data_Manipulation">Data Manipulation</a></li> <li><a href="#Data_Manipulation">Data Manipulation</a></li>
<li><a href="#Data_Selection">Data Selection</a></li> <li><a href="#Data_Selection">Data Selection</a></li>
@ -373,6 +374,29 @@ var data = new vis.DataSet([data] [, options])
</table> </table>
<h2 id="Properties">Properties</h2>
<p>DataSet contains the following properties.</p>
<table>
<colgroup>
<col width="200">
</colgroup>
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>length</td>
<td>Number</td>
<td>The number of items in the DataSet.</td>
</tr>
</table>
<h2 id="Subscriptions">Subscriptions</h2> <h2 id="Subscriptions">Subscriptions</h2>
<p> <p>

+ 120
- 2
docs/dataview.html View File

@ -20,6 +20,8 @@
<li><a href="#Overview">Overview</a></li> <li><a href="#Overview">Overview</a></li>
<li><a href="#Example">Example</a></li> <li><a href="#Example">Example</a></li>
<li><a href="#Construction">Construction</a></li> <li><a href="#Construction">Construction</a></li>
<li><a href="#Methods">Methods</a></li>
<li><a href="#Properties">Properties</a></li>
<li><a href="#Getting_Data">Getting Data</a></li> <li><a href="#Getting_Data">Getting Data</a></li>
<li><a href="#Subscriptions">Subscriptions</a></li> <li><a href="#Subscriptions">Subscriptions</a></li>
<li><a href="#Data_Policy">Data Policy</a></li> <li><a href="#Data_Policy">Data Policy</a></li>
@ -152,6 +154,122 @@ var data = new vis.DataView(dataset, options)
</li> </li>
</ul> </ul>
<h2 id="Methods">Methods</h2>
<p>DataView contains the following methods.</p>
<table>
<colgroup>
<col width="200">
</colgroup>
<tr>
<th>Method</th>
<th>Return Type</th>
<th>Description</th>
</tr>
<tr>
<td>
get([options] [, data])<br>
get(id [,options] [, data])<br>
get(ids [, options] [, data])
</td>
<td>Object | Array | DataTable</td>
<td>
Get a single item, multiple items, or all items from the DataView.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>. If parameter <code>data</code> is provided, items will be appended to this array or table, which is required in case of Google DataTable.
</td>
</tr>
<tr>
<td>
getDataSet()
</td>
<td>DataSet</td>
<td>
Get the DataSet to which the DataView is connected.
</td>
</tr>
<tr>
<td>
getIds([options])
</td>
<td>Number[]</td>
<td>
Get ids of all items or of a filtered set of items.
Available <code>options</code> are described in section <a href="dataset.html#Data_Selection">Data Selection</a>, except that options <code>fields</code> and <code>type</code> are not applicable in case of <code>getIds</code>.
</td>
</tr>
<tr>
<td>off(event, callback)</td>
<td>none</td>
<td>
Unsubscribe from an event, remove an event listener. See section <a href="#Subscriptions">Subscriptions</a>.
</td>
</tr>
<tr>
<td>on(event, callback)</td>
<td>none</td>
<td>
Subscribe to an event, add an event listener. See section <a href="#Subscriptions">Subscriptions</a>.
</td>
</tr>
<tr>
<td>refresh()</td>
<td>none</td>
<td>
Refresh the filter results of a DataView. Useful when the filter function contains dynamic properties, like:
<pre class="prettyprint lang-js">var data = new vis.DataSet(...);
var view = new vis.DataView(data, {
filter: function (item) {
return item.value > threshold;
}
});</pre>
In this example, <code>threshold</code> is an external parameter. When the value of <code>threshold</code> changes, the DataView must be notified that the filter results may have changed by calling <code>DataView.refresh()</code>.
</td>
</tr>
<tr>
<td>
setDataSet(data)
</td>
<td>none</td>
<td>
Replace the DataSet of the DataView. Parameter <code>data</code> can be a DataSet or a DataView.
</td>
</tr>
</table>
<h2 id="Properties">Properties</h2>
<p>DataView contains the following properties.</p>
<table>
<colgroup>
<col width="200">
</colgroup>
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>length</td>
<td>Number</td>
<td>The number of items in the DataView.</td>
</tr>
</table>
<h2 id="Getting_Data">Getting Data</h2> <h2 id="Getting_Data">Getting Data</h2>
<p> <p>
@ -165,8 +283,8 @@ var items = view.get();
<p> <p>
Data of a DataView can be filtered and formatted again, in exactly the Data of a DataView can be filtered and formatted again, in exactly the
same way as in a DataSet. See sections same way as in a DataSet. See sections
<a href="dataset.html#Data_Filtering">Data Filtering</a> and
<a href="dataset.html#Data_Formatting">Data Formatting</a> for more
<a href="dataset.html#Data_Manipulation">Data Manipulation</a> and
<a href="dataset.html#Data_Selection">Data Selection</a> for more
information. information.
</p> </p>

+ 22
- 0
docs/graph2d.html View File

@ -175,6 +175,12 @@ var items = [
<td>no</td> <td>no</td>
<td>The ID of the group this point belongs to.</td> <td>The ID of the group this point belongs to.</td>
</tr> </tr>
<tr>
<td>label</td>
<td>object</td>
<td>no</td>
<td>A label object which will be displayed near to the item. A label object has one requirement - a <b> content </b> property. In addition you can set the <b> xOffset, yOffset and className </b> for further appearance customisations </td>
</tr>
</table> </table>
<h3 id="groups">Groups</h3> <h3 id="groups">Groups</h3>
@ -737,6 +743,22 @@ The options colored in green can also be used as options for the groups. All opt
If not provided, the earliest date present in the events is taken as start date.</td> If not provided, the earliest date present in the events is taken as start date.</td>
</tr> </tr>
<tr>
<td>timeAxis.scale</td>
<td>string</td>
<td>none</td>
<td>Set a fixed scale for the time axis of the Timeline. Choose from <code>'millisecond'</code>, <code>'second'</code>, <code>'minute'</code>, <code>'hour'</code>, <code>'weekday'</code>, <code>'day'</code>, <code>'month'</code>, <code>'year'</code>.</td>
</tr>
<tr>
<td>timeAxis.step</td>
<td>number</td>
<td>1</td>
<td>
Set a fixed step size for the time axis. Only applicable when used together with <code>timeAxis.scale</code>.
Choose for example 1, 2, 5, or 10.</td>
</tr>
<tr> <tr>
<td>width</td> <td>width</td>
<td>String</td> <td>String</td>

+ 145
- 16
docs/network.html View File

@ -303,7 +303,8 @@ When using a DataSet, the network is automatically updating to changes in the Da
<td>level</td> <td>level</td>
<td>number</td> <td>number</td>
<td>no</td> <td>no</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything.</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything. This must be a postive number (min value: 0).
Fractions are possible but only integers are supported.</td>
</tr> </tr>
<tr> <tr>
@ -342,7 +343,7 @@ When using a DataSet, the network is automatically updating to changes in the Da
<td>no</td> <td>no</td>
<td>Horizontal position in pixels. <td>Horizontal position in pixels.
The horizontal position of the node will be fixed unless combined with the allowedToMoveX:true option. The horizontal position of the node will be fixed unless combined with the allowedToMoveX:true option.
The vertical position y may remain undefined.</td>
The vertical position y may remain undefined. This does not work with hierarchical layout.</td>
</tr> </tr>
<tr> <tr>
<td>y</td> <td>y</td>
@ -350,7 +351,7 @@ When using a DataSet, the network is automatically updating to changes in the Da
<td>no</td> <td>no</td>
<td>Vertical position in pixels. <td>Vertical position in pixels.
The vertical position of the node will be fixed unless combined with the allowedToMoveY:true option. The vertical position of the node will be fixed unless combined with the allowedToMoveY:true option.
The horizontal position x may remain undefined.</td>
The horizontal position x may remain undefined. This does not work with hierarchical layout.</td>
</tr> </tr>
</table> </table>
@ -848,6 +849,13 @@ var options = {
inside an object <code>nodes</code> in the networks options object.</p> All options in green boxes can be defined per-node as well. inside an object <code>nodes</code> in the networks options object.</p> All options in green boxes can be defined per-node as well.
All options defined per-node override these global settings. All options defined per-node override these global settings.
<table> <table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr> <tr>
<td class="greenField">borderWidth</td> <td class="greenField">borderWidth</td>
<td>Number</td> <td>Number</td>
@ -860,6 +868,31 @@ All options defined per-node override these global settings.
<td>undefined</td> <td>undefined</td>
<td>The width of the border of the node when it is selected. If left at undefined, double the borderWidth will be used.</td> <td>The width of the border of the node when it is selected. If left at undefined, double the borderWidth will be used.</td>
</tr> </tr>
<tr>
<td>customScalingFunction</td>
<td>Function</td>
<td>Function</td>
<td>This is a function you can override to make the nodes scale the way you want them based on their values. The default function is this: <br>
<pre class="prettyprint lang-js">
function (min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
};
</pre>
The function receives the minimum value of the set, the maximum value, the total sum of all values and finally the value of the node or edge it works on. It has to return a value between 0 and 1.
The nodes and edges then calculate their size as follows:
<pre class="prettyprint lang-js">
var scale = customScalingFunction(min,max,total,value);
var diff = maxSize - minSize;
mySize = minSize + diff * scale;</pre>
</td>
</tr>
<tr> <tr>
<td class="greenField">color</td> <td class="greenField">color</td>
<td>String | Object</td> <td>String | Object</td>
@ -926,6 +959,38 @@ All options defined per-node override these global settings.
<td>14</td> <td>14</td>
<td>Font size in pixels for label in the node.</td> <td>Font size in pixels for label in the node.</td>
</tr> </tr>
<tr>
<td>scaleFontWithValue</td>
<td>Boolean</td>
<td>false</td>
<td>When using values, you can let the font scale with the size of the nodes if you enable the this option.</td>
</tr>
<tr>
<td>fontSizeMin</td>
<td>Number</td>
<td>14</td>
<td>When using values, you can let the font scale with the size of the nodes if you enable the scaleFontWithValue option. This is the minimum value of the fontSize.</td>
</tr>
<tr>
<td></td>fontSizeMax</td>
<td>Number</td>
<td>30</td>
<td>When using values, you can let the font scale with the size of the nodes if you enable the scaleFontWithValue option. This is the maximum value of the fontSize.</td>
</tr>
<tr>
<td>fontSizeMaxVisible</td>
<td>Number</td>
<td>30</td>
<td>When using values, you can let the font scale with the size of the nodes if you enable the scaleFontWithValue option. If you have a wide distribution of values and have a large max fontSize,
the text will become huge if you zoom in on it. This option limits the percieved fontSize to avoid this. If you set it to 20, no label will be larger than fontsize 20 (at scale = 1) regardless of the scale.</td>
</tr>
<tr>
<td>fontDrawThreshold</td>
<td>Number</td>
<td>3</td>
<td>When zooming out, the text becomes smaller. This option sets the minimum size of the label before not being drawn. Just like the fontSizeMaxVisible option, this is the relative fontSize (fontSize * scale).
You can combine this with the min and max values to have the labels of influential nodes show earlier when zooming in.</td>
</tr>
<tr> <tr>
<td class="greenField">fontFill</td> <td class="greenField">fontFill</td>
<td>String</td> <td>String</td>
@ -944,7 +1009,6 @@ All options defined per-node override these global settings.
<td>'white'</td> <td>'white'</td>
<td>The color of the label stroke.</td> <td>The color of the label stroke.</td>
</tr> </tr>
<tr> <tr>
<td class="greenField">shape</td> <td class="greenField">shape</td>
<td>string</td> <td>string</td>
@ -953,7 +1017,7 @@ All options defined per-node override these global settings.
Choose from Choose from
<code>ellipse</code> (default), <code>circle</code>, <code>box</code>, <code>ellipse</code> (default), <code>circle</code>, <code>box</code>,
<code>database</code>, <code>image</code>, <code>circularImage</code>, <code>label</code>, <code>dot</code>, <code>database</code>, <code>image</code>, <code>circularImage</code>, <code>label</code>, <code>dot</code>,
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, and <code>square</code>.
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, <code>square</code> and <code>icon</code>.
<br><br> <br><br>
In case of <code>image</code> and <code>circularImage</code>, a property with name <code>image</code> must In case of <code>image</code> and <code>circularImage</code>, a property with name <code>image</code> must
@ -1000,13 +1064,13 @@ All options defined per-node override these global settings.
<td>widthMin</td> <td>widthMin</td>
<td>Number</td> <td>Number</td>
<td>16</td> <td>16</td>
<td>The minimum width for a scaled image. Only applicable to shape <code>image</code>.</td>
<td>The minimum width for a scaled image. Only applicable to shape <code>image</code>. This only does something if you supply a value.</td>
</tr> </tr>
<tr> <tr>
<td>widthMax</td> <td>widthMax</td>
<td>Number</td> <td>Number</td>
<td>64</td> <td>64</td>
<td>The maximum width for a scaled image. Only applicable to shape <code>image</code>.</td>
<td>The maximum width for a scaled image. Only applicable to shape <code>image</code>. This only does something if you supply a value.</td>
</tr> </tr>
<tr> <tr>
@ -1021,15 +1085,39 @@ All options defined per-node override these global settings.
<td>Number</td> <td>Number</td>
<td>10</td> <td>10</td>
<td>The minimum radius for a scaled node. Only applicable to shapes <code>dot</code>, <td>The minimum radius for a scaled node. Only applicable to shapes <code>dot</code>,
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, and <code>square</code>.</td>
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, and <code>square</code>. This only does something if you supply a value.</td>
</tr> </tr>
<tr> <tr>
<td>radiusMax</td> <td>radiusMax</td>
<td>Number</td> <td>Number</td>
<td>30</td> <td>30</td>
<td>The maximum radius for a scaled node. Only applicable to shapes <code>dot</code>, <td>The maximum radius for a scaled node. Only applicable to shapes <code>dot</code>,
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, and <code>square</code>.</td>
<code>star</code>, <code>triangle</code>, <code>triangleDown</code>, and <code>square</code>. This only does something if you supply a value.</td>
</tr> </tr>
<tr>
<td class="greenField">iconFontFace</td>
<td>String</td>
<td>undefined</td>
<td>Font face for icons, for example <code>FontAwesome</code> or <code>Ionicon</code>.<br /><em>You have to link to the css defining the font by yourself (see Examples)</em></td>
</tr>
<tr>
<td class="greenField">icon</td>
<td>String</td>
<td>undefined</td>
<td>Unicode of the icon f.e. <code>\uf0c0</code> (user-icon in FontAwesome)</td>
</tr>
<tr>
<td class="greenField">iconSize</td>
<td>Number</td>
<td>50</td>
<td>Size of the icon</td>
</tr>
<tr>
<td class="greenField">color</td>
<td>String</td>
<td>black</td>
<td>Color of the icon</td>
</tr>
</table> </table>
@ -1196,7 +1284,12 @@ var options = {
<td>Possible values: <code>"line-above", "line-center", "line-below"</code>. The alignment of the label when drawn on the edge. <td>Possible values: <code>"line-above", "line-center", "line-below"</code>. The alignment of the label when drawn on the edge.
If <code>horizontal</code> it will align the label absolute horizontial.</td> If <code>horizontal</code> it will align the label absolute horizontial.</td>
</tr> </tr>
<tr>
<td class="greenField">opacity</td>
<td>Number</td>
<td>1.0</td>
<td>Possible values: <code>[0 .. 1]</code>. This opacity value is added on top of the color information. This only happens for the unselected state.</td>
</tr>
<tr> <tr>
<td class="greenField">style</td> <td class="greenField">style</td>
<td>string</td> <td>string</td>
@ -1224,13 +1317,13 @@ var options = {
<td>widthMin</td> <td>widthMin</td>
<td>Number</td> <td>Number</td>
<td>1</td> <td>1</td>
<td>The minimum thickness of the line when using per-edge defined values.</td>
<td>The minimum thickness of the line when using per-edge defined values. This does nothing if you have not defined a value.</td>
</tr> </tr>
<tr> <tr>
<td>widthMax</td> <td>widthMax</td>
<td>Number</td> <td>Number</td>
<td>15</td> <td>15</td>
<td>The maximum thickness of the line when using per-edge defined values.</td>
<td>The maximum thickness of the line when using per-edge defined values. This does nothing if you have not defined a value.</td>
</tr> </tr>
</table> </table>
@ -1726,7 +1819,8 @@ var options = {
radius: 1}, radius: 1},
maxNodeSizeIncrements: 600, maxNodeSizeIncrements: 600,
activeAreaBoxSize: 100, activeAreaBoxSize: 100,
clusterLevelDifference: 2
clusterLevelDifference: 2,
clusterByZoom: true
} }
} }
// OR to just load the module with default values: // OR to just load the module with default values:
@ -1869,6 +1963,12 @@ var options = {
If the highest level of your network at any given time is 3, nodes that have not clustered or If the highest level of your network at any given time is 3, nodes that have not clustered or
have clustered only once will join their neighbour with the lowest cluster level.</td> have clustered only once will join their neighbour with the lowest cluster level.</td>
</tr> </tr>
<tr>
<td>clusterByZoom</td>
<td>Boolean</td>
<td>true</td>
<td>You can toggle the clustering by zoom level using this option.</td>
</tr>
</table> </table>
<h3 id="Navigation_controls">Navigation controls</h3> <h3 id="Navigation_controls">Navigation controls</h3>
@ -1905,7 +2005,8 @@ var options = {
x: 10, x: 10,
y: 10, y: 10,
zoom: 0.02 zoom: 0.02
}
},
bindToWindow: true
} }
} }
</pre> </pre>
@ -1937,6 +2038,12 @@ var options = {
<td>0.02</td> <td>0.02</td>
<td>This defines the zoomspeed when using the keyboard navigation.</td> <td>This defines the zoomspeed when using the keyboard navigation.</td>
</tr> </tr>
<tr>
<td>bindToWindow</td>
<td>Boolean</td>
<td>true</td>
<td>If this is true, global keyboard events will be used. If it is false, the keyboard events are only used when the network is active. It is activated on mouseOver automatically.</td>
</tr>
</table> </table>
@ -2201,11 +2308,18 @@ var options = {
</td> </td>
</tr> </tr>
<tr> <tr>
<td>getBoundingBox()</td>
<td>getBoundingBox(nodeId)</td>
<td>Object</td> <td>Object</td>
<td>Returns a bounding box for the node including label in the format: {top:Number,left:Number,right:Number,bottom:Number}. These values are in canvas space. <td>Returns a bounding box for the node including label in the format: {top:Number,left:Number,right:Number,bottom:Number}. These values are in canvas space.
</td> </td>
</tr> </tr>
<tr>
<td>getConnectedNodes(nodeId)</td>
<td>Array</td>
<td>Returns an array with nodeIds of nodes that are connected to this node. Network keeps track of the connected nodes so this function allows you
to quickly get them without iterating over all edges manually. This is a lot faster for cases with many edges.
</td>
</tr>
<tr> <tr>
<td>getSelection()</td> <td>getSelection()</td>
<td>Array of ids</td> <td>Array of ids</td>
@ -2232,6 +2346,13 @@ var options = {
easeInQuint, easeOutQuint, easeInOutQuint </code> <br /><br /> easeInQuint, easeOutQuint, easeInOutQuint </code> <br /><br />
</td> </td>
</tr> </tr>
<tr>
<td>freezeSimulation(Boolean)</td>
<td>none</td>
<td>Calling freezeSimulation(true) immmediately stops the simulation and triggerst the stabilized event. This does not mean that the network
is physically stabilized but the nodes are not moving anymore. To continue the simulation call freezeSimulation(false).
</td>
</tr>
<tr> <tr>
<td>releaseNode()</td> <td>releaseNode()</td>
<td>none</td> <td>none</td>
@ -2321,6 +2442,12 @@ var options = {
nodes with id 3 and 5. The highlisghEdges boolean can be used to automatically select the edges connected to the node. nodes with id 3 and 5. The highlisghEdges boolean can be used to automatically select the edges connected to the node.
</td> </td>
</tr> </tr>
<tr>
<td>getConnectedNodes(nodeId)</td>
<td>Array</td>
<td>Get an array of (unique) nodeIds that are directly connected to this node.
</td>
</tr>
<tr> <tr>
<td>selectEdges(selection)</td> <td>selectEdges(selection)</td>
<td>none</td> <td>none</td>
@ -2340,7 +2467,7 @@ var options = {
or in percentages.</td> or in percentages.</td>
</tr> </tr>
<tr> <tr>
<td>getPositions([ids])</td>
<td>getPositions([nodeIds])</td>
<td>Object</td> <td>Object</td>
<td>This will return an object of all nodes' positions. Data can be accessed with object[nodeId].x and .y. You can optionally supply an id as string or number or an array of ids. If no id or array of ids have been supplied, all positions are returned. <td>This will return an object of all nodes' positions. Data can be accessed with object[nodeId].x and .y. You can optionally supply an id as string or number or an array of ids. If no id or array of ids have been supplied, all positions are returned.
</td> </td>
@ -2362,6 +2489,7 @@ var options = {
options can just be a boolean. When true, the zoom is animated, when false there is no animation. options can just be a boolean. When true, the zoom is animated, when false there is no animation.
Alternatively, you can supply an object. Alternatively, you can supply an object.
<br /><br /> The object can consist of:<br /> <br /><br /> The object can consist of:<br />
<b><code>nodes: [nodeIds]</code></b><br /> - an optional subset of nodes to zoom in on,<br />
<b><code>duration: Number</code></b><br /> - the duration of the animation in milliseconds,<br /> <b><code>duration: Number</code></b><br /> - the duration of the animation in milliseconds,<br />
<b><code>easingFunction: String</code></b><br /> - the easing function of the animation, available are:<br /> <b><code>easingFunction: String</code></b><br /> - the easing function of the animation, available are:<br />
<code>linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, <code>linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic,
@ -2370,6 +2498,7 @@ var options = {
</td> </td>
</tr> </tr>
</table> </table>
<h2 id="Events">Events</h2> <h2 id="Events">Events</h2>

+ 57
- 5
docs/timeline.html View File

@ -742,6 +742,7 @@ var options = {
<code>showMinorLabels</code> are false, no horizontal axis will be <code>showMinorLabels</code> are false, no horizontal axis will be
visible.</td> visible.</td>
</tr> </tr>
<tr> <tr>
<td>stack</td> <td>stack</td>
<td>Boolean</td> <td>Boolean</td>
@ -749,6 +750,16 @@ var options = {
<td>If true (default), items will be stacked on top of each other such that they do not overlap.</td> <td>If true (default), items will be stacked on top of each other such that they do not overlap.</td>
</tr> </tr>
<tr>
<td>snap</td>
<td>function | null</td>
<td>function</td>
<td>When moving items on the Timeline, they will be snapped to nice dates like full hours or days, depending on the current scale. The <code>snap</code> function can be replaced with a custom function, or can be set to <code>null</code> to disable snapping. The signature of the snap function is:
<pre class="prettyprint lang-js">function snap(date: Date, scale: string, step: number) : Date | number</pre>
The parameter <code>scale</code> can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'day, 'month, or 'year'. The parameter <code>step</code> is a number like 1, 2, 4, 5.
</td>
</tr>
<tr> <tr>
<td>start</td> <td>start</td>
<td>Date | Number | String</td> <td>Date | Number | String</td>
@ -764,6 +775,26 @@ var options = {
<td>A template function used to generate the contents of the items. The function is called by the Timeline with an items data as argument, and must return HTML code as result. When the option template is specified, the items do not need to have a field <code>content</code>. See section <a href="#Templates">Templates</a> for a detailed explanation.</td> <td>A template function used to generate the contents of the items. The function is called by the Timeline with an items data as argument, and must return HTML code as result. When the option template is specified, the items do not need to have a field <code>content</code>. See section <a href="#Templates">Templates</a> for a detailed explanation.</td>
</tr> </tr>
<tr>
<td>timeAxis.scale</td>
<td>string</td>
<td>none</td>
<td>Set a fixed scale for the time axis of the Timeline. Choose from <code>'millisecond'</code>, <code>'second'</code>, <code>'minute'</code>, <code>'hour'</code>, <code>'weekday'</code>, <code>'day'</code>, <code>'month'</code>, <code>'year'</code>. Example usage:
<pre class="prettyprint lang-js">var options = {
timeAxis: {scale: 'minute', step: 5}
}</pre>
</td>
</tr>
<tr>
<td>timeAxis.step</td>
<td>number</td>
<td>1</td>
<td>
Set a fixed step size for the time axis. Only applicable when used together with <code>timeAxis.scale</code>.
Choose for example 1, 2, 5, or 10.</td>
</tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>String</td> <td>String</td>
@ -823,6 +854,15 @@ var options = {
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr>
<td>addCustomTime(time[, id])</td>
<td>Number | String</td>
<td>
Only applicable when the option showCustomTime is true.<br>
Add new vertical bar representing custom time that can be dragged by the user. Parameter <code>time</code> can be a Date, Number, or String. Parameter <code>id</code> can be Number or String. If <code>id</code> is provided, it will be used as ID for the new vertical bar, otherwise the ID will be auto generated.<br>
Returns ID of the newly created bar.
</td>
</tr>
<tr> <tr>
<td>clear([what])</td> <td>clear([what])</td>
<td>none</td> <td>none</td>
@ -872,9 +912,9 @@ timeline.clear({options: true}); // clear options only
</tr> </tr>
<tr> <tr>
<td>getCustomTime()</td>
<td>getCustomTime([id])</td>
<td>Date</td> <td>Date</td>
<td>Retrieve the custom time. Only applicable when the option <code>showCustomTime</code> is true.
<td>Retrieve the custom time. Only applicable when the option <code>showCustomTime</code> is true. If parameter <code>id</code> is provided, time of the custom time bar under that ID is returned.
</td> </td>
</tr> </tr>
@ -921,7 +961,17 @@ timeline.clear({options: true}); // clear options only
<tr> <tr>
<td>redraw()</td> <td>redraw()</td>
<td>none</td> <td>none</td>
<td>Force a redraw of the Timeline. Can be useful to manually redraw when option autoResize=false.
<td>Force a redraw of the Timeline. The size of all items will be recalculated.
Can be useful to manually redraw when option <code>autoResize=false</code> and the window
has been resized, or when the items CSS has been changed.
</td>
</tr>
<tr>
<td>removeCustomTime(id)</td>
<td>none</td>
<td>
Remove vertical bars previously added to the timeline via <code>addCustomTime</code> method. Parameter <code>id</code> is the ID of the custom vertical bar returned by <code>addCustomTime</code> method.
</td> </td>
</tr> </tr>
@ -934,9 +984,9 @@ timeline.clear({options: true}); // clear options only
</tr> </tr>
<tr> <tr>
<td>setCustomTime(time)</td>
<td>setCustomTime(time [, id])</td>
<td>none</td> <td>none</td>
<td>Adjust the custom time bar. Only applicable when the option <code>showCustomTime</code> is true. <code>time</code> can be a Date object, numeric timestamp, or ISO date string.
<td>Adjust the custom time bar. Only applicable when the option <code>showCustomTime</code> is true. Parameter <code>time</code> can be a Date object, numeric timestamp, or ISO date string. Parameter <code>id</code> represents ID of the custom time bar, provided by <code>addCustomTime</code> method and can be a Number or String.
</td> </td>
</tr> </tr>
@ -1097,6 +1147,7 @@ timeline.off('select', onSelect);
</td> </td>
<td> <td>
<ul> <ul>
<li><code>id</code> (Number | String): Vertical bar ID.</li>
<li><code>time</code> (Date): the current time.</li> <li><code>time</code> (Date): the current time.</li>
</ul> </ul>
</td> </td>
@ -1109,6 +1160,7 @@ timeline.off('select', onSelect);
</td> </td>
<td> <td>
<ul> <ul>
<li><code>id</code> (Number | String): Vertical bar ID.</li>
<li><code>time</code> (Date): the current time.</li> <li><code>time</code> (Date): the current time.</li>
</ul> </ul>
</td> </td>

+ 64
- 0
examples/graph2d/19_labels.html View File

@ -0,0 +1,64 @@
<!DOCTYPE HTML>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<title>Graph2d | Basic Example</title>
<style type="text/css">
body, html {
font-family: sans-serif;
}
.red {
fill:red;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h2>Graph2d | Label Example</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
This example shows the how to add a label to each point in Graph2d. Each item can have a label object which contains the content and CSS class.In addition, xOffset and yOffset will adjust the location of the label relative to the point being labelled.
<br /><br />
</div>
<br />
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var label1 = {
content: "offset label",
xOffset: 20,
yOffset: 20
}
var label2 = {
content: "Label2",
className: "red"
}
var items = [
{x: '2014-06-11', y: 10,label:label1},
{x: '2014-06-12', y: 25,label:label2},
{x: '2014-06-13', y: 30},
{x: '2014-06-14', y: 10},
{x: '2014-06-15', y: 15},
{x: '2014-06-16', y: 30}
];
var dataset = new vis.DataSet(items);
var options = {
start: '2014-06-10',
end: '2014-06-18',
};
var graph2d = new vis.Graph2d(container, dataset, options);
</script>
</body>
</html>

+ 11
- 7
examples/network/06_groups.html View File

@ -8,9 +8,10 @@
font: 10pt arial; font: 10pt arial;
} }
#mynetwork { #mynetwork {
width: 600px;
height: 600px;
width: 1900px;
height: 900px;
border: 1px solid lightgray; border: 1px solid lightgray;
background-color:#222222;
} }
</style> </style>
@ -139,11 +140,14 @@
edges: edges edges: edges
}; };
var options = { var options = {
stabilize: false,
stabilize: true,
nodes: { nodes: {
shape: 'dot'
shape: 'dot',
radius:30,
fontColor:'#ffffff',
borderWidth:2
}, },
physics: {barnesHut:{springLength: 200}}
physics: {barnesHut:{springLength: 100}}
}; };
network = new vis.Network(container, data, options); network = new vis.Network(container, data, options);
} }
@ -154,9 +158,9 @@
<body onload="draw()"> <body onload="draw()">
<form onsubmit= "javascript: draw(); return false;"> <form onsubmit= "javascript: draw(); return false;">
Number of groups: Number of groups:
<input type="text" value="6" id="groupCount" style="width: 50px;">
<input type="text" value="20" id="groupCount" style="width: 50px;">
Number of nodes per group: Number of nodes per group:
<input type="text" value="7" id="nodeCount" style="width: 50px;">
<input type="text" value="1" id="nodeCount" style="width: 50px;">
<input type="submit" value="Go"> <input type="submit" value="Go">
</form> </form>
<br> <br>

+ 17
- 4
examples/network/26_staticSmoothCurves.html View File

@ -32,19 +32,26 @@
Smooth curve type: Smooth curve type:
<select id="dropdownID"> <select id="dropdownID">
<option value="continuous">continuous</option>
<option value="continuous" selected="selected">continuous</option>
<option value="discrete">discrete</option> <option value="discrete">discrete</option>
<option value="diagonalCross">diagonalCross</option> <option value="diagonalCross">diagonalCross</option>
<option value="straightCross">straightCross</option> <option value="straightCross">straightCross</option>
<option value="horizontal">horizontal</option> <option value="horizontal">horizontal</option>
<option value="vertical">vertical</option> <option value="vertical">vertical</option>
</select>
<option value="curvedCW">curvedCW</option>
<option value="curvedCCW">curvedCCW</option>
</select><br/>
Roundness (0..1): <input type="range" min="0" max="1" value="0.5" step="0.05" style="width:200px" id="roundnessSlider"> <input id="roundnessScreen" value="0.5"> (0.5 is max roundness for continuous, 1.0 for the others)
<div id="mynetwork"></div> <div id="mynetwork"></div>
<script type="text/javascript"> <script type="text/javascript">
var dropdown = document.getElementById("dropdownID"); var dropdown = document.getElementById("dropdownID");
dropdown.onchange = update; dropdown.onchange = update;
var roundnessSlider = document.getElementById("roundnessSlider");
roundnessSlider.onchange = update;
var roundnessScreen = document.getElementById("roundnessScreen");
// create an array with nodes // create an array with nodes
var nodes = [ var nodes = [
{id: 1, label: 'Node 1'}, {id: 1, label: 'Node 1'},
@ -53,7 +60,7 @@ dropdown.onchange = update;
// create an array with edges // create an array with edges
var edges = [ var edges = [
{from: 1, to: 2}
{from: 1, to: 2, style:"arrow"}
]; ];
// create a network // create a network
@ -68,8 +75,14 @@ dropdown.onchange = update;
function update() { function update() {
var type = dropdown.value; var type = dropdown.value;
network.setOptions({smoothCurves:{type:type}});
var roundness = roundnessSlider.value;
roundnessScreen.value = roundness;
var options = {smoothCurves:{type:type, roundness:roundness}}
network.setOptions(options);
} }
update();
</script> </script>
</body> </body>

+ 2
- 0
examples/network/27_world_cup_network.html View File

@ -39,6 +39,8 @@ Smooth curve type:
<option value="straightCross">straightCross</option> <option value="straightCross">straightCross</option>
<option value="horizontal">horizontal</option> <option value="horizontal">horizontal</option>
<option value="vertical">vertical</option> <option value="vertical">vertical</option>
<option value="curvedCW">curvedCW</option>
<option value="curvedCCW">curvedCCW</option>
</select><br/> </select><br/>
inheritColor option: inheritColor option:
<select id="inheritColor"> <select id="inheritColor">

+ 10
- 27
examples/network/29_neighbourhood_highlight.html View File

@ -10027,7 +10027,12 @@ function redrawAll() {
radiusMin: 10, radiusMin: 10,
radiusMax: 30, radiusMax: 30,
fontSize: 12, fontSize: 12,
fontFace: "Tahoma"
fontFace: "Tahoma",
scaleFontWithValue:true,
fontSizeMin:8,
fontSizeMax:20,
fontThreshold:12,
fontSizeMaxVisible:20
}, },
edges: { edges: {
width: 0.15, width: 0.15,
@ -10071,8 +10076,6 @@ function onClick(selectedItems) {
} }
} }
else { else {
var allEdges = edges.get();
// we clear the level of separation in all nodes. // we clear the level of separation in all nodes.
clearLevelOfSeperation(allNodes); clearLevelOfSeperation(allNodes);
@ -10083,7 +10086,7 @@ function onClick(selectedItems) {
// any data can be added to a node, this is just stored in the nodeObject. // any data can be added to a node, this is just stored in the nodeObject.
storeLevelOfSeperation(connectedNodes,0, allNodes); storeLevelOfSeperation(connectedNodes,0, allNodes);
for (var i = 1; i < degrees + 1; i++) { for (var i = 1; i < degrees + 1; i++) {
appendConnectedNodes(connectedNodes, allEdges);
appendConnectedNodes(connectedNodes);
storeLevelOfSeperation(connectedNodes, i, allNodes); storeLevelOfSeperation(connectedNodes, i, allNodes);
} }
for (nodeId in allNodes) { for (nodeId in allNodes) {
@ -10153,7 +10156,7 @@ function clearLevelOfSeperation(allNodes) {
* *
* *
*/ */
function appendConnectedNodes(sourceNodes, allEdges) {
function appendConnectedNodes(sourceNodes) {
var tempSourceNodes = []; var tempSourceNodes = [];
// first we make a copy of the nodes so we do not extend the array we loop over. // first we make a copy of the nodes so we do not extend the array we loop over.
for (var i = 0; i < sourceNodes.length; i++) { for (var i = 0; i < sourceNodes.length; i++) {
@ -10165,7 +10168,8 @@ function appendConnectedNodes(sourceNodes, allEdges) {
if (sourceNodes.indexOf(nodeId) == -1) { if (sourceNodes.indexOf(nodeId) == -1) {
sourceNodes.push(nodeId); sourceNodes.push(nodeId);
} }
addUnique(getConnectedNodes(nodeId, allEdges),sourceNodes);
var connectedNodes = network.getConnectedNodes(nodeId);
addUnique(connectedNodes,sourceNodes);
} }
tempSourceNodes = null; tempSourceNodes = null;
} }
@ -10183,27 +10187,6 @@ function addUnique(fromArray, toArray) {
} }
} }
/**
* Get a list of nodes that are connected to the supplied nodeId with edges.
* @param nodeId
* @returns {Array}
*/
function getConnectedNodes(nodeId, allEdges) {
var edgesArray = allEdges;
var connectedNodes = [];
for (var i = 0; i < edgesArray.length; i++) {
var edge = edgesArray[i];
if (edge.to == nodeId) {
connectedNodes.push(edge.from);
}
else if (edge.from == nodeId) {
connectedNodes.push(edge.to)
}
}
return connectedNodes;
}
redrawAll() redrawAll()
</script> </script>

+ 166
- 0
examples/network/38_node_as_icon.html View File

@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Network | node as icon</title>
<script type="text/javascript" src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<script language="JavaScript">
function draw() {
/*
* Example for FontAwesome
*/
var optionsFA = {
height: '300px',
groups: {
usergroups: {
shape: 'icon',
iconFontFace: 'FontAwesome',
icon: '\uf0c0',
iconSize: 50,
iconColor: '#57169a'
},
users: {
shape: 'icon',
iconFontFace: 'FontAwesome',
icon: '\uf007',
iconSize: 50,
iconColor: '#aa00ff'
}
}
};
// create an array with nodes
var nodesFA = [{
id: 1,
label: 'User 1',
group: 'users'
}, {
id: 2,
label: 'User 2',
group: 'users'
}, {
id: 3,
label: 'Usergroup 1',
group: 'usergroups'
}, {
id: 4,
label: 'Usergroup 2',
group: 'usergroups'
}, {
id: 5,
label: 'Organisation 1',
shape: 'icon',
iconFontFace: 'FontAwesome',
icon: '\uf1ad',
iconSize: 50,
iconColor: '#f0a30a'
}];
// create an array with edges
var edges = [{
from: 1,
to: 3
}, {
from: 1,
to: 4
}, {
from: 2,
to: 4
}, {
from: 3,
to: 5
}, {
from: 4,
to: 5
}];
// create a network
var containerFA = document.getElementById('mynetworkFA');
var dataFA = {
nodes: nodesFA,
edges: edges
};
var networkFA = new vis.Network(containerFA, dataFA, optionsFA);
/*
* Example for Ionicons
*/
var optionsIO = {
height: '300px',
groups: {
usergroups: {
shape: 'icon',
iconFontFace: 'Ionicons',
icon: '\uf47c',
iconSize: 50,
iconColor: '#57169a'
},
users: {
shape: 'icon',
iconFontFace: 'Ionicons',
icon: '\uf47e',
iconSize: 50,
iconColor: '#aa00ff'
}
}
};
// create an array with nodes
var nodesIO = [{
id: 1,
label: 'User 1',
group: 'users'
}, {
id: 2,
label: 'User 2',
group: 'users'
}, {
id: 3,
label: 'Usergroup 1',
group: 'usergroups'
}, {
id: 4,
label: 'Usergroup 2',
group: 'usergroups'
}, {
id: 5,
label: 'Organisation 1',
shape: 'icon',
iconFontFace: 'Ionicons',
icon: '\uf276',
iconSize: 50,
iconColor: '#f0a30a'
}];
// create a network
var containerIO = document.getElementById('mynetworkIO');
var dataIO = {
nodes: nodesIO,
edges: edges
};
var networkIO = new vis.Network(containerIO, dataIO, optionsIO);
}
</script>
</head>
<body onload="draw()">
<h2>
<i class="fa fa-flag"></i> Use FontAwesome-icons for node</h2>
<div id="mynetworkFA"></div>
<h2>
<i class="ion ion-ionic"></i> Use Ionicons-icons for node</h2>
<div id="mynetworkIO"></div>
</body>
</html>

+ 1
- 0
examples/network/index.html View File

@ -49,6 +49,7 @@
<p><a href="35_label_stroke.html">35_label_stroke.html</a></p> <p><a href="35_label_stroke.html">35_label_stroke.html</a></p>
<p><a href="36_HTML_in_Nodes.html">36_HTML_in_Nodes.html</a></p> <p><a href="36_HTML_in_Nodes.html">36_HTML_in_Nodes.html</a></p>
<p><a href="37_label_alignment.html">37_label_alignment.html</a></p> <p><a href="37_label_alignment.html">37_label_alignment.html</a></p>
<p><a href="38_node_as_icon.html">38_node_as_icon.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p> <p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div> </div>

+ 54
- 0
examples/timeline/33_custom_snapping.html View File

@ -0,0 +1,54 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Custom snapping</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
When moving the items in on the Timeline below, they will snap to full hours,
independent of being zoomed in or out.
</p>
<div id="visualization"></div>
<script type="text/javascript">
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
{id: 1, content: 'A', start: '2015-02-09T04:00:00'},
{id: 2, content: 'B', start: '2015-02-09T14:00:00'},
{id: 3, content: 'C', start: '2015-02-09T16:00:00'},
{id: 4, content: 'D', start: '2015-02-09T17:00:00'},
{id: 5, content: 'E', start: '2015-02-10T03:00:00'}
]);
// Configuration for the Timeline
var options = {
editable: true,
// always snap to full hours, independent of the scale
snap: function (date, scale, step) {
var hour = 60 * 60 * 1000;
return Math.round(date / hour) * hour;
}
// to configure no snapping at all:
//
// snap: null
//
// or let the snap function return the date unchanged:
//
// snap: function (date, scale, step) {
// return date;
// }
};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 74
- 0
examples/timeline/34_add_custom_timebar.html View File

@ -0,0 +1,74 @@
<!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="button" id="add" value="Add custom vertical bar">
<input type="text" id="barId" placeholder="custom bar ID">
</p>
<p>
<input type="button" id="remove" value="Remove custom vertical bar">
<input type="text" id="barIndex" value="1" placeholder="custom bar ID">
</p>
<p>
<code><strong>timechange</strong></code> bar index: <span id="timechangeBar"></span>. Time: <span id="timechangeEvent"></span>
</p>
<p>
<code><strong>timechanged</strong></code> bar index: <span id="timechangedBar"></span>. Time: <span id="timechangedEvent"></span>
</p><br>
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = new vis.DataSet();
var customDate = new Date();
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);
// Set first time bar
customDate = new Date(customDate.getFullYear(), customDate.getMonth(), customDate.getDate() + 1);
timeline.addCustomTime(customDate, 1);
document.getElementById('add').onclick = function () {
customDate = new Date(customDate.getFullYear(), customDate.getMonth(), customDate.getDate() + 1);
var barId = document.getElementById('barId').value || undefined;
timeline.addCustomTime(customDate, barId);
document.getElementById('barId').value = '';
};
document.getElementById('remove').onclick = function () {
timeline.removeCustomTime(document.getElementById('barIndex').value);
document.getElementById('barIndex').value = '';
};
timeline.on('timechange', function (properties) {
document.getElementById('timechangeBar').innerHTML = properties.id;
document.getElementById('timechangeEvent').innerHTML = properties.time;
});
timeline.on('timechanged', function (properties) {
document.getElementById('timechangedBar').innerHTML = properties.id;
document.getElementById('timechangedEvent').innerHTML = properties.time;
});
</script>
</body>
</html>

+ 2
- 1
examples/timeline/index.html View File

@ -43,7 +43,8 @@
<p><a href="30_subgroups.html">30_subgroups.html</a></p> <p><a href="30_subgroups.html">30_subgroups.html</a></p>
<p><a href="31_background_areas_with_groups.html">31_background_areas_with_groups.html</a></p> <p><a href="31_background_areas_with_groups.html">31_background_areas_with_groups.html</a></p>
<p><a href="32_grid_styling.html">32_grid_styling.html</a></p> <p><a href="32_grid_styling.html">32_grid_styling.html</a></p>
<p><a href="33_custom_snapping.html">33_custom_snapping.html</a></p>
<p><a href="34_add_custom_timebar.html">34_add_custom_timebar.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p> <p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>
</div> </div>

+ 2
- 1
gulpfile.js View File

@ -97,7 +97,8 @@ gulp.task('bundle-css', ['clean'], function () {
'./lib/timeline/component/css/pathStyles.css', './lib/timeline/component/css/pathStyles.css',
'./lib/network/css/network-manipulation.css', './lib/network/css/network-manipulation.css',
'./lib/network/css/network-navigation.css'
'./lib/network/css/network-navigation.css',
'./lib/network/css/network-tooltip.css'
]; ];
return gulp.src(files) return gulp.src(files)

+ 24
- 1
lib/DOMutil.js View File

@ -130,9 +130,10 @@ exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, inse
* @param group * @param group
* @param JSONcontainer * @param JSONcontainer
* @param svgContainer * @param svgContainer
* @param labelObj
* @returns {*} * @returns {*}
*/ */
exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer, labelObj) {
var point; var point;
if (group.options.drawPoints.style == 'circle') { if (group.options.drawPoints.style == 'circle') {
point = exports.getSVGElement('circle',JSONcontainer,svgContainer); point = exports.getSVGElement('circle',JSONcontainer,svgContainer);
@ -152,6 +153,28 @@ exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
point.setAttributeNS(null, "style", group.group.options.drawPoints.styles); point.setAttributeNS(null, "style", group.group.options.drawPoints.styles);
} }
point.setAttributeNS(null, "class", group.className + " point"); point.setAttributeNS(null, "class", group.className + " point");
//handle label
var label = exports.getSVGElement('text',JSONcontainer,svgContainer);
if (labelObj){
if (labelObj.xOffset) {
x = x + labelObj.xOffset;
}
if (labelObj.yOffset) {
y = y + labelObj.yOffset;
}
if (labelObj.content) {
label.textContent = labelObj.content;
}
if (labelObj.className) {
label.setAttributeNS(null, "class", labelObj.className + " label");
}
}
label.setAttributeNS(null, "x", x);
label.setAttributeNS(null, "y", y);
return point; return point;
}; };

+ 11
- 2
lib/DataSet.js View File

@ -53,6 +53,7 @@ function DataSet (data, options) {
this._options = options || {}; this._options = options || {};
this._data = {}; // map with data indexed by id this._data = {}; // map with data indexed by id
this.length = 0; // number of items in the DataSet
this._fieldId = this._options.fieldId || 'id'; // name of the field containing id this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
this._type = {}; // internal field types (NOTE: this can differ from this._options.type) this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
@ -650,12 +651,16 @@ DataSet.prototype.map = function (callback, options) {
/** /**
* Filter the fields of an item * Filter the fields of an item
* @param {Object} item
* @param {Object | null} item
* @param {String[]} fields Field names * @param {String[]} fields Field names
* @return {Object} filteredItem
* @return {Object | null} filteredItem or null if no item is provided
* @private * @private
*/ */
DataSet.prototype._filterFields = function (item, fields) { DataSet.prototype._filterFields = function (item, fields) {
if (!item) { // item is null
return item;
}
var filteredItem = {}; var filteredItem = {};
for (var field in item) { for (var field in item) {
@ -737,6 +742,7 @@ DataSet.prototype._remove = function (id) {
if (util.isNumber(id) || util.isString(id)) { if (util.isNumber(id) || util.isString(id)) {
if (this._data[id]) { if (this._data[id]) {
delete this._data[id]; delete this._data[id];
this.length--;
return id; return id;
} }
} }
@ -744,6 +750,7 @@ DataSet.prototype._remove = function (id) {
var itemId = id[this._fieldId]; var itemId = id[this._fieldId];
if (itemId && this._data[itemId]) { if (itemId && this._data[itemId]) {
delete this._data[itemId]; delete this._data[itemId];
this.length--;
return itemId; return itemId;
} }
} }
@ -759,6 +766,7 @@ DataSet.prototype.clear = function (senderId) {
var ids = Object.keys(this._data); var ids = Object.keys(this._data);
this._data = {}; this._data = {};
this.length = 0;
this._trigger('remove', {items: ids}, senderId); this._trigger('remove', {items: ids}, senderId);
@ -884,6 +892,7 @@ DataSet.prototype._addItem = function (item) {
} }
} }
this._data[id] = d; this._data[id] = d;
this.length++;
return id; return id;
}; };

+ 47
- 0
lib/DataView.js View File

@ -14,6 +14,7 @@ var DataSet = require('./DataSet');
function DataView (data, options) { function DataView (data, options) {
this._data = null; this._data = null;
this._ids = {}; // ids of the items currently in memory (just contains a boolean true) this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
this.length = 0; // number of items in the DataView
this._options = options || {}; this._options = options || {};
this._fieldId = 'id'; // name of the field containing id this._fieldId = 'id'; // name of the field containing id
this._subscribers = {}; // event subscribers this._subscribers = {}; // event subscribers
@ -50,6 +51,7 @@ DataView.prototype.setData = function (data) {
} }
} }
this._ids = {}; this._ids = {};
this.length = 0;
this._trigger('remove', {items: ids}); this._trigger('remove', {items: ids});
} }
@ -67,6 +69,7 @@ DataView.prototype.setData = function (data) {
id = ids[i]; id = ids[i];
this._ids[id] = true; this._ids[id] = true;
} }
this.length = ids.length;
this._trigger('add', {items: ids}); this._trigger('add', {items: ids});
// subscribe to new dataset // subscribe to new dataset
@ -76,6 +79,48 @@ DataView.prototype.setData = function (data) {
} }
}; };
/**
* Refresh the DataView. Useful when the DataView has a filter function
* containing a variable parameter.
*/
DataView.prototype.refresh = function () {
var id;
var ids = this._data.getIds({filter: this._options && this._options.filter});
var newIds = {};
var added = [];
var removed = [];
// check for additions
for (var i = 0; i < ids.length; i++) {
id = ids[i];
newIds[id] = true;
if (!this._ids[id]) {
added.push(id);
this._ids[id] = true;
this.length++;
}
}
// check for removals
for (id in this._ids) {
if (this._ids.hasOwnProperty(id)) {
if (!newIds[id]) {
removed.push(id);
delete this._ids[id];
this.length--;
}
}
}
// trigger events
if (added.length) {
this._trigger('add', {items: added});
}
if (removed.length) {
this._trigger('remove', {items: removed});
}
};
/** /**
* Get data from the data view * Get data from the data view
* *
@ -277,6 +322,8 @@ DataView.prototype._onEvent = function (event, params, senderId) {
break; break;
} }
this.length += added.length - removed.length;
if (added.length) { if (added.length) {
this._trigger('add', {items: added}, senderId); this._trigger('add', {items: added}, senderId);
} }

+ 84
- 33
lib/network/Edge.js View File

@ -40,6 +40,7 @@ function Edge (properties, network, networkConstants) {
this.hover = false; this.hover = false;
this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
this.dirtyLabel = true; this.dirtyLabel = true;
this.colorDirty = true;
this.from = null; // a node this.from = null; // a node
this.to = null; // a node this.to = null; // a node
@ -71,12 +72,14 @@ function Edge (properties, network, networkConstants) {
* @param {Object} constants and object with default, global properties * @param {Object} constants and object with default, global properties
*/ */
Edge.prototype.setProperties = function(properties) { Edge.prototype.setProperties = function(properties) {
this.colorDirty = true;
if (!properties) { if (!properties) {
return; return;
} }
var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width', var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width',
'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment'
'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity',
'customScalingFunction','useGradients'
]; ];
util.selectiveDeepExtend(fields, this.options, properties); util.selectiveDeepExtend(fields, this.options, properties);
@ -103,7 +106,9 @@ Edge.prototype.setProperties = function(properties) {
} }
} }
// A node is connected when it has a from and to node.
// A node is connected when it has a from and to node.
this.connect(); this.connect();
this.widthFixed = this.widthFixed || (properties.width !== undefined); this.widthFixed = this.widthFixed || (properties.width !== undefined);
@ -119,9 +124,9 @@ Edge.prototype.setProperties = function(properties) {
case 'dash-line': this.draw = this._drawDashLine; break; case 'dash-line': this.draw = this._drawDashLine; break;
default: this.draw = this._drawLine; break; default: this.draw = this._drawLine; break;
} }
}; };
/** /**
* Connect an edge to its nodes * Connect an edge to its nodes
*/ */
@ -186,10 +191,11 @@ Edge.prototype.getValue = function() {
* @param {Number} min * @param {Number} min
* @param {Number} max * @param {Number} max
*/ */
Edge.prototype.setValueRange = function(min, max) {
Edge.prototype.setValueRange = function(min, max, total) {
if (!this.widthFixed && this.value !== undefined) { if (!this.widthFixed && this.value !== undefined) {
var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
this.options.width= (this.value - min) * scale + this.options.widthMin;
var scale = this.options.customScalingFunction(min, max, total, this.value);
var widthDiff = this.options.widthMax - this.options.widthMin;
this.options.width = this.options.widthMin + scale * widthDiff;
this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
} }
}; };
@ -228,23 +234,51 @@ Edge.prototype.isOverlappingWith = function(obj) {
} }
}; };
Edge.prototype._getColor = function() {
Edge.prototype._getColor = function(ctx) {
var colorObj = this.options.color; var colorObj = this.options.color;
if (this.options.inheritColor == "to") {
colorObj = {
highlight: this.to.options.color.highlight.border,
hover: this.to.options.color.hover.border,
color: this.to.options.color.border
};
if (this.options.useGradients == true) {
var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
var fromColor, toColor;
fromColor = this.from.options.color.highlight.border;
toColor = this.to.options.color.highlight.border;
if (this.from.selected == false && this.to.selected == false) {
fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity);
toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity);
}
else if (this.from.selected == true && this.to.selected == false) {
toColor = this.to.options.color.border;
}
else if (this.from.selected == false && this.to.selected == true) {
fromColor = this.from.options.color.border;
}
grd.addColorStop(0, fromColor);
grd.addColorStop(1, toColor);
return grd;
} }
else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
colorObj = {
highlight: this.from.options.color.highlight.border,
hover: this.from.options.color.hover.border,
color: this.from.options.color.border
};
if (this.colorDirty === true) {
if (this.options.inheritColor == "to") {
colorObj = {
highlight: this.to.options.color.highlight.border,
hover: this.to.options.color.hover.border,
color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
};
}
else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
colorObj = {
highlight: this.from.options.color.highlight.border,
hover: this.from.options.color.hover.border,
color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
};
}
this.options.color = colorObj;
this.colorDirty = false;
} }
if (this.selected == true) {return colorObj.highlight;} if (this.selected == true) {return colorObj.highlight;}
else if (this.hover == true) {return colorObj.hover;} else if (this.hover == true) {return colorObj.hover;}
else {return colorObj.color;} else {return colorObj.color;}
@ -260,7 +294,7 @@ Edge.prototype._getColor = function() {
*/ */
Edge.prototype._drawLine = function(ctx) { Edge.prototype._drawLine = function(ctx) {
// set style // set style
ctx.strokeStyle = this._getColor();
ctx.strokeStyle = this._getColor(ctx);
ctx.lineWidth = this._getLineWidth(); ctx.lineWidth = this._getLineWidth();
if (this.from != this.to) { if (this.from != this.to) {
@ -334,7 +368,6 @@ Edge.prototype._getViaCoordinates = function () {
var yVia = null; var yVia = null;
var factor = this.options.smoothCurves.roundness; var factor = this.options.smoothCurves.roundness;
var type = this.options.smoothCurves.type; var type = this.options.smoothCurves.type;
var dx = Math.abs(this.from.x - this.to.x); var dx = Math.abs(this.from.x - this.to.x);
var dy = Math.abs(this.from.y - this.to.y); var dy = Math.abs(this.from.y - this.to.y);
if (type == 'discrete' || type == 'diagonalCross') { if (type == 'discrete' || type == 'diagonalCross') {
@ -427,17 +460,39 @@ Edge.prototype._getViaCoordinates = function () {
yVia = this.to.y + (1 - factor) * dy; yVia = this.to.y + (1 - factor) * dy;
} }
} }
else if (type == 'curvedCW') {
var dx = this.to.x - this.from.x;
var dy = this.from.y - this.to.y;
var radius = Math.sqrt(dx*dx + dy*dy);
var pi = Math.PI;
var originalAngle = Math.atan2(dy,dx);
var myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi);
xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle);
yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle);
}
else if (type == 'curvedCCW') {
var dx = this.to.x - this.from.x;
var dy = this.from.y - this.to.y;
var radius = Math.sqrt(dx*dx + dy*dy);
var pi = Math.PI;
var originalAngle = Math.atan2(dy,dx);
var myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi);
xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle);
yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle);
}
else { // continuous else { // continuous
if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
if (this.from.y > this.to.y) { if (this.from.y > this.to.y) {
if (this.from.x < this.to.x) { if (this.from.x < this.to.x) {
// console.log(1)
xVia = this.from.x + factor * dy; xVia = this.from.x + factor * dy;
yVia = this.from.y - factor * dy; yVia = this.from.y - factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia; xVia = this.to.x < xVia ? this.to.x : xVia;
} }
else if (this.from.x > this.to.x) { else if (this.from.x > this.to.x) {
// console.log(2)
xVia = this.from.x - factor * dy; xVia = this.from.x - factor * dy;
yVia = this.from.y - factor * dy; yVia = this.from.y - factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia; xVia = this.to.x > xVia ? this.to.x : xVia;
@ -445,13 +500,11 @@ Edge.prototype._getViaCoordinates = function () {
} }
else if (this.from.y < this.to.y) { else if (this.from.y < this.to.y) {
if (this.from.x < this.to.x) { if (this.from.x < this.to.x) {
// console.log(3)
xVia = this.from.x + factor * dy; xVia = this.from.x + factor * dy;
yVia = this.from.y + factor * dy; yVia = this.from.y + factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia; xVia = this.to.x < xVia ? this.to.x : xVia;
} }
else if (this.from.x > this.to.x) { else if (this.from.x > this.to.x) {
// console.log(4, this.from.x, this.to.x)
xVia = this.from.x - factor * dy; xVia = this.from.x - factor * dy;
yVia = this.from.y + factor * dy; yVia = this.from.y + factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia; xVia = this.to.x > xVia ? this.to.x : xVia;
@ -461,13 +514,11 @@ Edge.prototype._getViaCoordinates = function () {
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
if (this.from.y > this.to.y) { if (this.from.y > this.to.y) {
if (this.from.x < this.to.x) { if (this.from.x < this.to.x) {
// console.log(5)
xVia = this.from.x + factor * dx; xVia = this.from.x + factor * dx;
yVia = this.from.y - factor * dx; yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia; yVia = this.to.y > yVia ? this.to.y : yVia;
} }
else if (this.from.x > this.to.x) { else if (this.from.x > this.to.x) {
// console.log(6)
xVia = this.from.x - factor * dx; xVia = this.from.x - factor * dx;
yVia = this.from.y - factor * dx; yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia; yVia = this.to.y > yVia ? this.to.y : yVia;
@ -475,13 +526,11 @@ Edge.prototype._getViaCoordinates = function () {
} }
else if (this.from.y < this.to.y) { else if (this.from.y < this.to.y) {
if (this.from.x < this.to.x) { if (this.from.x < this.to.x) {
// console.log(7)
xVia = this.from.x + factor * dx; xVia = this.from.x + factor * dx;
yVia = this.from.y + factor * dx; yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia; yVia = this.to.y < yVia ? this.to.y : yVia;
} }
else if (this.from.x > this.to.x) { else if (this.from.x > this.to.x) {
// console.log(8)
xVia = this.from.x - factor * dx; xVia = this.from.x - factor * dx;
yVia = this.from.y + factor * dx; yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia; yVia = this.to.y < yVia ? this.to.y : yVia;
@ -517,6 +566,8 @@ Edge.prototype._line = function (ctx) {
// this.via.y = via.y; // this.via.y = via.y;
ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
ctx.stroke(); ctx.stroke();
//ctx.circle(via.x,via.y,2)
//ctx.stroke();
return via; return via;
} }
} }
@ -705,7 +756,7 @@ Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSi
*/ */
Edge.prototype._drawDashLine = function(ctx) { Edge.prototype._drawDashLine = function(ctx) {
// set style // set style
ctx.strokeStyle = this._getColor();
ctx.strokeStyle = this._getColor(ctx);
ctx.lineWidth = this._getLineWidth(); ctx.lineWidth = this._getLineWidth();
var via = null; var via = null;
@ -810,7 +861,7 @@ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
Edge.prototype._drawArrowCenter = function(ctx) { Edge.prototype._drawArrowCenter = function(ctx) {
var point; var point;
// set style // set style
ctx.strokeStyle = this._getColor();
ctx.strokeStyle = this._getColor(ctx);
ctx.fillStyle = ctx.strokeStyle; ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this._getLineWidth(); ctx.lineWidth = this._getLineWidth();
@ -946,7 +997,7 @@ Edge.prototype._findBorderPosition = function(from,ctx) {
*/ */
Edge.prototype._drawArrow = function(ctx) { Edge.prototype._drawArrow = function(ctx) {
// set style // set style
ctx.strokeStyle = this._getColor();
ctx.strokeStyle = this._getColor(ctx);
ctx.fillStyle = ctx.strokeStyle; ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this._getLineWidth(); ctx.lineWidth = this._getLineWidth();
@ -1164,7 +1215,7 @@ Edge.prototype.positionBezierNode = function() {
this.via.x = 0.5 * (this.from.x + this.to.x); this.via.x = 0.5 * (this.from.x + this.to.x);
this.via.y = 0.5 * (this.from.y + this.to.y); this.via.y = 0.5 * (this.from.y + this.to.y);
} }
else {
else if (this.via !== null) {
this.via.x = 0; this.via.x = 0;
this.via.y = 0; this.via.y = 0;
} }

+ 46
- 19
lib/network/Groups.js View File

@ -7,6 +7,9 @@ var util = require('../util');
function Groups() { function Groups() {
this.clear(); this.clear();
this.defaultIndex = 0; this.defaultIndex = 0;
this.groupsArray = [];
this.groupIndex = 0;
this.useDefaultGroups = true;
} }
@ -14,16 +17,29 @@ function Groups() {
* default constants for group colors * default constants for group colors
*/ */
Groups.DEFAULT = [ Groups.DEFAULT = [
{border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
{border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
{border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
{border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
{border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
{border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
{border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
{border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
{border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
{border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
{border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue
{border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow
{border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red
{border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green
{border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta
{border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple
{border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange
{border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue
{border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink
{border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint
{border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red
{border: "#FF6000", background: "#FF6000", highlight: {border: "#FF6000", background: "#FF6000"}, hover: {border: "#FF6000", background: "#FF6000"}}, // 12: real orange
{border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 13: blue
{border: "#399605", background: "#255C03", highlight: {border: "#399605", background: "#255C03"}, hover: {border: "#399605", background: "#255C03"}}, // 14: green
{border: "#B70054", background: "#FF007E", highlight: {border: "#B70054", background: "#FF007E"}, hover: {border: "#B70054", background: "#FF007E"}}, // 15: magenta
{border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 16: purple
{border: "#4557FA", background: "#000EA1", highlight: {border: "#6E6EFD", background: "#000EA1"}, hover: {border: "#6E6EFD", background: "#000EA1"}}, // 17: darkblue
{border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 18: pink
{border: "#C2FABC", background: "#74D66A", highlight: {border: "#E6FFE3", background: "#74D66A"}, hover: {border: "#E6FFE3", background: "#74D66A"}}, // 19: mint
{border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 20:bright red
]; ];
@ -54,12 +70,22 @@ Groups.prototype.clear = function () {
Groups.prototype.get = function (groupname) { Groups.prototype.get = function (groupname) {
var group = this.groups[groupname]; var group = this.groups[groupname];
if (group == undefined) { if (group == undefined) {
// create new group
var index = this.defaultIndex % Groups.DEFAULT.length;
this.defaultIndex++;
group = {};
group.color = Groups.DEFAULT[index];
this.groups[groupname] = group;
if (this.useDefaultGroups === false && this.groupsArray.length > 0) {
// create new group
var index = this.groupIndex % this.groupsArray.length;
this.groupIndex++;
group = {};
group.color = this.groups[this.groupsArray[index]];
this.groups[groupname] = group;
}
else {
// create new group
var index = this.defaultIndex % Groups.DEFAULT.length;
this.defaultIndex++;
group = {};
group.color = Groups.DEFAULT[index];
this.groups[groupname] = group;
}
} }
return group; return group;
@ -67,13 +93,14 @@ Groups.prototype.get = function (groupname) {
/** /**
* Add a custom group style * Add a custom group style
* @param {String} groupname
* @param {String} groupName
* @param {Object} style An object containing borderColor, * @param {Object} style An object containing borderColor,
* backgroundColor, etc. * backgroundColor, etc.
* @return {Object} group The created group object * @return {Object} group The created group object
*/ */
Groups.prototype.add = function (groupname, style) {
this.groups[groupname] = style;
Groups.prototype.add = function (groupName, style) {
this.groups[groupName] = style;
this.groupsArray.push(groupName);
return style; return style;
}; };

+ 18
- 9
lib/network/Images.js View File

@ -52,16 +52,25 @@ Images.prototype.load = function(url, brokenUrl) {
me.callback(this); me.callback(this);
} }
} }
else if (me.imageBroken[url] === true) {
console.error("Could not load brokenImage:", brokenUrl);
delete this.src;
if (me.callback) {
me.callback(this);
}
}
else { else {
this.src = brokenUrl;
me.imageBroken[url] = true;
if (me.imageBroken[url] === true) {
if (this.src == brokenUrl) {
console.error("Could not load brokenImage:", brokenUrl);
delete this.src;
if (me.callback) {
me.callback(this);
}
}
else {
console.error("Could not load image:", url);
this.src = brokenUrl;
}
}
else {
console.error("Could not load image:", url);
this.src = brokenUrl;
me.imageBroken[url] = true;
}
} }
}; };

+ 406
- 159
lib/network/Network.js
File diff suppressed because it is too large
View File


+ 108
- 28
lib/network/Node.js View File

@ -13,7 +13,7 @@ var util = require('../util');
* "database", "circle", "ellipse", * "database", "circle", "ellipse",
* "box", "image", "text", "dot", * "box", "image", "text", "dot",
* "star", "triangle", "triangleDown", * "star", "triangle", "triangleDown",
* "square"
* "square", "icon"
* {string} image An image url * {string} image An image url
* {string} title An title text, can be HTML * {string} title An title text, can be HTML
* {anytype} group A group name or number * {anytype} group A group name or number
@ -36,8 +36,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.dynamicEdges = []; this.dynamicEdges = [];
this.reroutedEdges = {}; this.reroutedEdges = {};
this.fontDrawThreshold = 3;
// set defaults for the properties // set defaults for the properties
this.id = undefined; this.id = undefined;
this.allowedToMoveX = false; this.allowedToMoveX = false;
@ -64,6 +62,7 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.vy = 0.0; // velocity y this.vy = 0.0; // velocity y
this.x = null; this.x = null;
this.y = null; this.y = null;
this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate
// used for reverting to previous position on stabilization // used for reverting to previous position on stabilization
this.previousState = {vx:0,vy:0,x:0,y:0}; this.previousState = {vx:0,vy:0,x:0,y:0};
@ -75,12 +74,11 @@ function Node(properties, imagelist, grouplist, networkConstants) {
// creating the variables for clustering // creating the variables for clustering
this.resetCluster(); this.resetCluster();
this.dynamicEdgesLength = 0;
this.clusterSession = 0; this.clusterSession = 0;
this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width; this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height; this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius; this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0; this.growthIndicator = 0;
// variables to tell the node about the network. // variables to tell the node about the network.
@ -126,7 +124,6 @@ Node.prototype.attachEdge = function(edge) {
if (this.dynamicEdges.indexOf(edge) == -1) { if (this.dynamicEdges.indexOf(edge) == -1) {
this.dynamicEdges.push(edge); this.dynamicEdges.push(edge);
} }
this.dynamicEdgesLength = this.dynamicEdges.length;
}; };
/** /**
@ -142,7 +139,6 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) { if (index != -1) {
this.dynamicEdges.splice(index, 1); this.dynamicEdges.splice(index, 1);
} }
this.dynamicEdgesLength = this.dynamicEdges.length;
}; };
@ -157,7 +153,8 @@ Node.prototype.setProperties = function(properties, constants) {
} }
var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor', var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor',
'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass'
'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold',
'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction','iconFontFace', 'icon', 'iconColor', 'iconSize'
]; ];
util.selectiveDeepExtend(fields, this.options, properties); util.selectiveDeepExtend(fields, this.options, properties);
@ -165,8 +162,8 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.id !== undefined) {this.id = properties.id;} if (properties.id !== undefined) {this.id = properties.id;}
if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;} if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
if (properties.title !== undefined) {this.title = properties.title;} if (properties.title !== undefined) {this.title = properties.title;}
if (properties.x !== undefined) {this.x = properties.x;}
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.x !== undefined) {this.x = properties.x; this.predefinedPosition = true;}
if (properties.y !== undefined) {this.y = properties.y; this.predefinedPosition = true;}
if (properties.value !== undefined) {this.value = properties.value;} if (properties.value !== undefined) {this.value = properties.value;}
if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;} if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
@ -180,16 +177,16 @@ Node.prototype.setProperties = function(properties, constants) {
} }
// copy group properties // copy group properties
if (typeof this.options.group === 'number' || (typeof this.options.group === 'string' && this.options.group != '')) {
var groupObj = this.grouplist.get(this.options.group);
if (typeof properties.group === 'number' || (typeof properties.group === 'string' && properties.group != '')) {
var groupObj = this.grouplist.get(properties.group);
util.deepExtend(this.options, groupObj); util.deepExtend(this.options, groupObj);
// the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case.
this.options.color = util.parseColor(this.options.color); this.options.color = util.parseColor(this.options.color);
} }
// individual shape properties // individual shape properties
if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;} if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;}
if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);} if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);}
if (this.options.image !== undefined && this.options.image!= "") { if (this.options.image !== undefined && this.options.image!= "") {
if (this.imagelist) { if (this.imagelist) {
this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage); this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage);
@ -238,6 +235,7 @@ Node.prototype.setProperties = function(properties, constants) {
case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
case 'icon': this.draw = this._drawIcon; this.resize = this._resizeIcon; break;
default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
} }
// reset the size of the node, this can be changed // reset the size of the node, this can be changed
@ -482,16 +480,17 @@ Node.prototype.getDistance = function(x, y) {
* @param {Number} min * @param {Number} min
* @param {Number} max * @param {Number} max
*/ */
Node.prototype.setValueRange = function(min, max) {
Node.prototype.setValueRange = function(min, max, total) {
if (!this.radiusFixed && this.value !== undefined) { if (!this.radiusFixed && this.value !== undefined) {
if (max == min) {
this.options.radius= (this.options.radiusMin + this.options.radiusMax) / 2;
}
else {
var scale = (this.options.radiusMax - this.options.radiusMin) / (max - min);
this.options.radius= (this.value - min) * scale + this.options.radiusMin;
var scale = this.options.customScalingFunction(min, max, total, this.value);
var radiusDiff = this.options.radiusMax - this.options.radiusMin;
if (this.options.scaleFontWithValue == true) {
var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin;
this.options.fontSize = this.options.fontSizeMin + scale * fontDiff;
} }
this.options.radius = this.options.radiusMin + scale * radiusDiff;
} }
this.baseRadiusValue = this.options.radius; this.baseRadiusValue = this.options.radius;
}; };
@ -1013,14 +1012,91 @@ Node.prototype._drawText = function (ctx) {
this.boundingBox.bottom = this.top + this.height; this.boundingBox.bottom = this.top + this.height;
}; };
Node.prototype._resizeIcon = function (ctx) {
if (!this.width) {
var margin = 5;
var iconSize =
{
width: Number(this.options.iconSize),
height: Number(this.options.iconSize)
};
this.width = iconSize.width + 2 * margin;
this.height = iconSize.height + 2 * margin;
// scaling used for clustering
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - (iconSize.width + 2 * margin);
}
};
Node.prototype._drawIcon = function (ctx) {
this._resizeIcon(ctx);
this.options.iconSize = this.options.iconSize || 50;
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
this._icon(ctx);
this.boundingBox.top = this.y - this.options.iconSize/2;
this.boundingBox.left = this.x - this.options.iconSize/2;
this.boundingBox.right = this.x + this.options.iconSize/2;
this.boundingBox.bottom = this.y + this.options.iconSize/2;
if (this.label) {
var iconTextSpacing = 5;
this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, 'top', true);
this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
}
};
Node.prototype._icon = function (ctx) {
var relativeIconSize = Number(this.options.iconSize) * this.networkScale;
if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) {
var iconSize = Number(this.options.iconSize);
ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace;
// draw icon
ctx.fillStyle = this.options.iconColor || "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(this.options.icon, this.x, this.y);
}
};
Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
if (text && Number(this.options.fontSize) * this.networkScale > this.fontDrawThreshold) {
ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
var relativeFontSize = Number(this.options.fontSize) * this.networkScale;
if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) {
var fontSize = Number(this.options.fontSize);
// this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
if (relativeFontSize >= this.options.fontSizeMaxVisible) {
fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
}
// fade in when relative scale is between threshold and threshold - 1
var fontColor = this.options.fontColor || "#000000";
var strokecolor = this.options.fontStrokeColor;
if (relativeFontSize <= this.options.fontDrawThreshold) {
var opacity = Math.max(0,Math.min(1,1 - (this.options.fontDrawThreshold - relativeFontSize)));
fontColor = util.overrideOpacity(fontColor, opacity);
strokecolor = util.overrideOpacity(strokecolor, opacity);
}
ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;
var lines = text.split('\n'); var lines = text.split('\n');
var lineCount = lines.length; var lineCount = lines.length;
var fontSize = Number(this.options.fontSize);
var yLine = y + (1 - lineCount) / 2 * fontSize; var yLine = y + (1 - lineCount) / 2 * fontSize;
if (labelUnderNode == true) { if (labelUnderNode == true) {
yLine = y + (1 - lineCount) / (2 * fontSize); yLine = y + (1 - lineCount) / (2 * fontSize);
@ -1032,7 +1108,7 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNo
var lineWidth = ctx.measureText(lines[i]).width; var lineWidth = ctx.measureText(lines[i]).width;
width = lineWidth > width ? lineWidth : width; width = lineWidth > width ? lineWidth : width;
} }
var height = this.options.fontSize * lineCount;
var height = fontSize * lineCount;
var left = x - width / 2; var left = x - width / 2;
var top = y - height / 2; var top = y - height / 2;
if (baseline == "hanging") { if (baseline == "hanging") {
@ -1049,12 +1125,12 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNo
} }
// draw text // draw text
ctx.fillStyle = this.options.fontColor || "black";
ctx.fillStyle = fontColor;
ctx.textAlign = align || "center"; ctx.textAlign = align || "center";
ctx.textBaseline = baseline || "middle"; ctx.textBaseline = baseline || "middle";
if (this.options.fontStrokeWidth > 0){ if (this.options.fontStrokeWidth > 0){
ctx.lineWidth = this.options.fontStrokeWidth; ctx.lineWidth = this.options.fontStrokeWidth;
ctx.strokeStyle = this.options.fontStrokeColor;
ctx.strokeStyle = strokecolor;
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
} }
for (var i = 0; i < lineCount; i++) { for (var i = 0; i < lineCount; i++) {
@ -1070,10 +1146,14 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNo
Node.prototype.getTextSize = function(ctx) { Node.prototype.getTextSize = function(ctx) {
if (this.label !== undefined) { if (this.label !== undefined) {
ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
var fontSize = Number(this.options.fontSize);
if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) {
fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
}
ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;
var lines = this.label.split('\n'), var lines = this.label.split('\n'),
height = (Number(this.options.fontSize) + 4) * lines.length,
height = (fontSize + 4) * lines.length,
width = 0; width = 0;
for (var i = 0, iMax = lines.length; i < iMax; i++) { for (var i = 0, iMax = lines.length; i < iMax; i++) {

+ 11
- 16
lib/network/Popup.js View File

@ -40,8 +40,9 @@ function Popup(container, x, y, text, style) {
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
this.padding = 5; this.padding = 5;
this.hidden = false;
if (x !== undefined && y !== undefined ) {
if (x !== undefined && y !== undefined) {
this.setPosition(x, y); this.setPosition(x, y);
} }
if (text !== undefined) { if (text !== undefined) {
@ -49,21 +50,13 @@ function Popup(container, x, y, text, style) {
} }
// create the frame // create the frame
this.frame = document.createElement("div");
var styleAttr = this.frame.style;
styleAttr.position = "absolute";
styleAttr.visibility = "hidden";
styleAttr.border = "1px solid " + style.color.border;
styleAttr.color = style.fontColor;
styleAttr.fontSize = style.fontSize + "px";
styleAttr.fontFamily = style.fontFace;
styleAttr.padding = this.padding + "px";
styleAttr.backgroundColor = style.color.background;
styleAttr.borderRadius = "3px";
styleAttr.MozBorderRadius = "3px";
styleAttr.WebkitBorderRadius = "3px";
styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
styleAttr.whiteSpace = "nowrap";
this.frame = document.createElement('div');
this.frame.className = 'network-tooltip';
this.frame.style.color = style.fontColor;
this.frame.style.backgroundColor = style.color.background;
this.frame.style.borderColor = style.color.border;
this.frame.style.fontSize = style.fontSize + 'px';
this.frame.style.fontFamily = style.fontFace;
this.container.appendChild(this.frame); this.container.appendChild(this.frame);
} }
@ -124,6 +117,7 @@ Popup.prototype.show = function (show) {
this.frame.style.left = left + "px"; this.frame.style.left = left + "px";
this.frame.style.top = top + "px"; this.frame.style.top = top + "px";
this.frame.style.visibility = "visible"; this.frame.style.visibility = "visible";
this.hidden = false;
} }
else { else {
this.hide(); this.hide();
@ -134,6 +128,7 @@ Popup.prototype.show = function (show) {
* Hide the popup window * Hide the popup window
*/ */
Popup.prototype.hide = function () { Popup.prototype.hide = function () {
this.hidden = true;
this.frame.style.visibility = "hidden"; this.frame.style.visibility = "hidden";
}; };

+ 18
- 17
lib/network/css/network-manipulation.css View File

@ -22,9 +22,8 @@ div.network-manipulationDiv {
div.network-manipulation-editMode { div.network-manipulation-editMode {
position:absolute; position:absolute;
left: 0; left: 0;
top: 0;
top: 15px;
height: 30px; height: 30px;
margin-top:20px;
} }
div.network-manipulation-closeDiv { div.network-manipulation-closeDiv {
@ -50,7 +49,9 @@ div.network-manipulation-closeDiv:hover {
opacity: 0.6; opacity: 0.6;
} }
span.network-manipulationUI {
div.network-manipulationUI {
position:relative;
top:-7px;
font-family: verdana; font-family: verdana;
font-size: 12px; font-size: 12px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
@ -59,7 +60,7 @@ span.network-manipulationUI {
background-position: 0px 0px; background-position: 0px 0px;
background-repeat:no-repeat; background-repeat:no-repeat;
height:24px; height:24px;
margin: -14px 0px 0px 10px;
margin: 0px 0px 0px 10px;
vertical-align:middle; vertical-align:middle;
cursor: pointer; cursor: pointer;
padding: 0px 8px 0px 8px; padding: 0px 8px 0px 8px;
@ -71,57 +72,57 @@ span.network-manipulationUI {
user-select: none; user-select: none;
} }
span.network-manipulationUI:hover {
div.network-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
} }
span.network-manipulationUI:active {
div.network-manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
} }
span.network-manipulationUI.back {
div.network-manipulationUI.back {
background-image: url("img/network/backIcon.png"); background-image: url("img/network/backIcon.png");
} }
span.network-manipulationUI.none:hover {
div.network-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default; cursor: default;
} }
span.network-manipulationUI.none:active {
div.network-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
} }
span.network-manipulationUI.none {
div.network-manipulationUI.none {
padding: 0; padding: 0;
} }
span.network-manipulationUI.notification{
div.network-manipulationUI.notification{
margin: 2px; margin: 2px;
font-weight: bold; font-weight: bold;
} }
span.network-manipulationUI.add {
div.network-manipulationUI.add {
background-image: url("img/network/addNodeIcon.png"); background-image: url("img/network/addNodeIcon.png");
} }
span.network-manipulationUI.edit {
div.network-manipulationUI.edit {
background-image: url("img/network/editIcon.png"); background-image: url("img/network/editIcon.png");
} }
span.network-manipulationUI.edit.editmode {
div.network-manipulationUI.edit.editmode {
background-color: #fcfcfc; background-color: #fcfcfc;
border-style:solid; border-style:solid;
border-width:1px; border-width:1px;
border-color: #cccccc; border-color: #cccccc;
} }
span.network-manipulationUI.connect {
div.network-manipulationUI.connect {
background-image: url("img/network/connectIcon.png"); background-image: url("img/network/connectIcon.png");
} }
span.network-manipulationUI.delete {
div.network-manipulationUI.delete {
background-image: url("img/network/deleteIcon.png"); background-image: url("img/network/deleteIcon.png");
} }
/* top right bottom left */ /* top right bottom left */
span.network-manipulationLabel {
div.network-manipulationLabel {
margin: 0px 0px 0px 23px; margin: 0px 0px 0px 23px;
line-height: 25px; line-height: 25px;
} }

+ 13
- 0
lib/network/css/network-tooltip.css View File

@ -0,0 +1,13 @@
div.network-tooltip {
position: absolute;
visibility: hidden;
padding: 5px;
white-space: nowrap;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
border: 1px solid;
box-shadow: 3px 3px 10px rgba(128, 128, 128, 0.5);
}

+ 85
- 93
lib/network/mixins/ClusterMixin.js View File

@ -17,7 +17,7 @@ exports.startWithClustering = function() {
// this is called here because if clusterin is disabled, the start and stabilize are called in // this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function. // the setData function.
if (this.stabilize) {
if (this.constants.stabilize == true) {
this._stabilize(); this._stabilize();
} }
this.start(); this.start();
@ -37,14 +37,14 @@ exports.clusterToFit = function(maxNumberOfNodes, reposition) {
// we first cluster the hubs, then we pull in the outliers, repeat // we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 3 == 0) {
if (level % 3 == 0.0) {
this.forceAggregateHubs(true); this.forceAggregateHubs(true);
this.normalizeClusterLevels(); this.normalizeClusterLevels();
} }
else { else {
this.increaseClusterLevel(); // this also includes a cluster normalization this.increaseClusterLevel(); // this also includes a cluster normalization
} }
this.forceAggregateHubs(true);
numberOfNodes = this.nodeIndices.length; numberOfNodes = this.nodeIndices.length;
level += 1; level += 1;
} }
@ -57,7 +57,7 @@ exports.clusterToFit = function(maxNumberOfNodes, reposition) {
}; };
/** /**
* This function can be called to open up a specific cluster. It is only called by
* This function can be called to open up a specific cluster.
* It will unpack the cluster back one level. * It will unpack the cluster back one level.
* *
* @param node | Node object: cluster to open. * @param node | Node object: cluster to open.
@ -80,9 +80,8 @@ exports.openCluster = function(node) {
else { else {
this._expandClusterNode(node,false,true); this._expandClusterNode(node,false,true);
// update the index list, dynamic edges and labels
// update the index list and labels
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateCalculationNodes(); this._updateCalculationNodes();
this.updateLabels(); this.updateLabels();
} }
@ -98,7 +97,7 @@ exports.openCluster = function(node) {
* This calls the updateClustes with default arguments * This calls the updateClustes with default arguments
*/ */
exports.updateClustersDefault = function() { exports.updateClustersDefault = function() {
if (this.constants.clustering.enabled == true) {
if (this.constants.clustering.enabled == true && this.constants.clustering.clusterByZoom == true) {
this.updateClusters(0,false,false); this.updateClusters(0,false,false);
} }
}; };
@ -140,18 +139,21 @@ exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
var isMovingBeforeClustering = this.moving; var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length; var amountOfNodes = this.nodeIndices.length;
var detectedZoomingIn = (this.previousScale < this.scale && zoomDirection == 0);
var detectedZoomingOut = (this.previousScale > this.scale && zoomDirection == 0);
// on zoom out collapse the sector if the scale is at the level the sector was made // on zoom out collapse the sector if the scale is at the level the sector was made
if (this.previousScale > this.scale && zoomDirection == 0) {
if (detectedZoomingOut == true) {
this._collapseSector(); this._collapseSector();
} }
// check if we zoom in or out // check if we zoom in or out
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out
// forming clusters when forced pulls outliers in. When not forced, the edge length of the // forming clusters when forced pulls outliers in. When not forced, the edge length of the
// outer nodes determines if it is being clustered // outer nodes determines if it is being clustered
this._formClusters(force); this._formClusters(force);
} }
else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
else if (detectedZoomingIn == true || zoomDirection == 1) { // zoom in
if (force == true) { if (force == true) {
// _openClusters checks for each node if the formationScale of the cluster is smaller than // _openClusters checks for each node if the formationScale of the cluster is smaller than
// the current scale and if so, declusters. When forced, all clusters are reduced by one step // the current scale and if so, declusters. When forced, all clusters are reduced by one step
@ -159,27 +161,27 @@ exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
} }
else { else {
// if a cluster takes up a set percentage of the active window // if a cluster takes up a set percentage of the active window
this._openClustersBySize();
//this._openClustersBySize();
this._openClusters(recursive, false);
} }
} }
this._updateNodeIndexList(); this._updateNodeIndexList();
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
if (this.nodeIndices.length == amountOfNodes && (detectedZoomingOut == true || zoomDirection == -1)) {
this._aggregateHubs(force); this._aggregateHubs(force);
this._updateNodeIndexList(); this._updateNodeIndexList();
} }
// we now reduce chains. // we now reduce chains.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out
this.handleChains(); this.handleChains();
this._updateNodeIndexList(); this._updateNodeIndexList();
} }
this.previousScale = this.scale; this.previousScale = this.scale;
// rest of the update the index list, dynamic edges and labels
this._updateDynamicEdges();
// update labels
this.updateLabels(); this.updateLabels();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
@ -224,7 +226,7 @@ exports._aggregateHubs = function(force) {
/** /**
* This function is fired by keypress. It forces hubs to form.
* This function forces hubs to form.
* *
*/ */
exports.forceAggregateHubs = function(doNotStart) { exports.forceAggregateHubs = function(doNotStart) {
@ -235,9 +237,10 @@ exports.forceAggregateHubs = function(doNotStart) {
// update the index list, dynamic edges and labels // update the index list, dynamic edges and labels
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges();
this.updateLabels(); this.updateLabels();
this._updateCalculationNodes();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) { if (this.nodeIndices.length != amountOfNodes) {
this.clusterSession += 1; this.clusterSession += 1;
@ -257,13 +260,15 @@ exports.forceAggregateHubs = function(doNotStart) {
* @private * @private
*/ */
exports._openClustersBySize = function() { exports._openClustersBySize = function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.inView() == true) {
if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
this.openCluster(node);
if (this.constants.clustering.clusterByZoom == true) {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.inView() == true) {
if ((node.width * this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(node.height * this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
this.openCluster(node);
}
} }
} }
} }
@ -299,12 +304,12 @@ exports._openClusters = function(recursive,force) {
exports._expandClusterNode = function(parentNode, recursive, force, openAll) { exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
// first check if node is a cluster // first check if node is a cluster
if (parentNode.clusterSize > 1) { if (parentNode.clusterSize > 1) {
// this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
openAll = true;
if (openAll === undefined) {
openAll = false;
} }
recursive = openAll ? true : recursive;
// this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
recursive = openAll || recursive;
// if the last child has been added on a smaller scale than current scale decluster // if the last child has been added on a smaller scale than current scale decluster
if (parentNode.formationScale < this.scale || force == true) { if (parentNode.formationScale < this.scale || force == true) {
// we will check if any of the contained child nodes should be removed from the cluster // we will check if any of the contained child nodes should be removed from the cluster
@ -347,7 +352,7 @@ exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
* @private * @private
*/ */
exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
var childNode = parentNode.containedNodes[containedNodeId];
var childNode = parentNode.containedNodes[containedNodeId]
// if child node has been added on smaller scale than current, kick out // if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) { if (childNode.formationScale < this.scale || force == true) {
@ -370,7 +375,6 @@ exports._expelChildFromParent = function(parentNode, containedNodeId, recursive,
parentNode.options.mass -= childNode.options.mass; parentNode.options.mass -= childNode.options.mass;
parentNode.clusterSize -= childNode.clusterSize; parentNode.clusterSize -= childNode.clusterSize;
parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1)); parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1));
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system // place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
@ -438,7 +442,9 @@ exports._repositionBezierNodes = function(node) {
*/ */
exports._formClusters = function(force) { exports._formClusters = function(force) {
if (force == false) { if (force == false) {
this._formClustersByZoom();
if (this.constants.clustering.clusterByZoom == true) {
this._formClustersByZoom();
}
} }
else { else {
this._forceClustersByZoom(); this._forceClustersByZoom();
@ -452,8 +458,8 @@ exports._formClusters = function(force) {
* @private * @private
*/ */
exports._formClustersByZoom = function() { exports._formClustersByZoom = function() {
var dx,dy,length,
minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
var dx,dy,length;
var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
// check if any edges are shorter than minLength and start the clustering // check if any edges are shorter than minLength and start the clustering
// the clustering favours the node with the larger mass // the clustering favours the node with the larger mass
@ -476,10 +482,10 @@ exports._formClustersByZoom = function() {
childNode = edge.from; childNode = edge.from;
} }
if (childNode.dynamicEdgesLength == 1) {
if (childNode.dynamicEdges.length == 1) {
this._addToCluster(parentNode,childNode,false); this._addToCluster(parentNode,childNode,false);
} }
else if (parentNode.dynamicEdgesLength == 1) {
else if (parentNode.dynamicEdges.length == 1) {
this._addToCluster(childNode,parentNode,false); this._addToCluster(childNode,parentNode,false);
} }
} }
@ -502,10 +508,9 @@ exports._forceClustersByZoom = function() {
var childNode = this.nodes[nodeId]; var childNode = this.nodes[nodeId];
// the edges can be swallowed by another decrease // the edges can be swallowed by another decrease
if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
if (childNode.dynamicEdges.length == 1) {
var edge = childNode.dynamicEdges[0]; var edge = childNode.dynamicEdges[0];
var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
// group to the largest node // group to the largest node
if (childNode.id != parentNode.id) { if (childNode.id != parentNode.id) {
if (parentNode.options.mass > childNode.options.mass) { if (parentNode.options.mass > childNode.options.mass) {
@ -585,9 +590,13 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
if (absorptionSizeOffset === undefined) { if (absorptionSizeOffset === undefined) {
absorptionSizeOffset = 0; absorptionSizeOffset = 0;
} }
//this.hubThreshold = 43
//if (hubNode.dynamicEdgesLength < 0) {
// console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual)
//}
// we decide if the node is a hub // we decide if the node is a hub
if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
(hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
if ((hubNode.dynamicEdges.length >= this.hubThreshold && onlyEqual == false) ||
(hubNode.dynamicEdges.length == this.hubThreshold && onlyEqual == true)) {
// initialize variables // initialize variables
var dx,dy,length; var dx,dy,length;
var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
@ -600,7 +609,7 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
edgesIdarray.push(hubNode.dynamicEdges[j].id); edgesIdarray.push(hubNode.dynamicEdges[j].id);
} }
// if the hub clustering is not forces, we check if one of the edges connected
// if the hub clustering is not forced, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
if (force == false) { if (force == false) {
allowCluster = false; allowCluster = false;
@ -625,19 +634,31 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
// start the clustering if allowed // start the clustering if allowed
if ((!force && allowCluster) || force) { if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
var children = [];
var childrenIds = {};
// we loop over all edges INITIALLY connected to this hub to get a list of the childNodes
for (j = 0; j < amountOfInitialEdges; j++) { for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIdarray[j]]; edge = this.edges[edgesIdarray[j]];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// we do not want hubs to merge with other hubs nor do we want to cluster itself.
if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
(childNode.id != hubNode.id)) {
this._addToCluster(hubNode,childNode,force);
}
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
if (childrenIds[childNode.id] === undefined) {
childrenIds[childNode.id] = true;
children.push(childNode);
}
}
for (j = 0; j < children.length; j++) {
var childNode = children[j];
// we do not want hubs to merge with other hubs nor do we want to cluster itself.
if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
(childNode.id != hubNode.id)) {
this._addToCluster(hubNode,childNode,force);
}
else {
//console.log("WILL NOT MERGE:",childNode.dynamicEdges.length , (this.hubThreshold + absorptionSizeOffset))
} }
} }
} }
} }
}; };
@ -655,14 +676,16 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
exports._addToCluster = function(parentNode, childNode, force) { exports._addToCluster = function(parentNode, childNode, force) {
// join child node in the parent node // join child node in the parent node
parentNode.containedNodes[childNode.id] = childNode; parentNode.containedNodes[childNode.id] = childNode;
//console.log(parentNode.id, childNode.id)
// manage all the edges connected to the child and parent nodes // manage all the edges connected to the child and parent nodes
for (var i = 0; i < childNode.dynamicEdges.length; i++) { for (var i = 0; i < childNode.dynamicEdges.length; i++) {
var edge = childNode.dynamicEdges[i]; var edge = childNode.dynamicEdges[i];
if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
//console.log("COLLECT",parentNode.id, childNode.id, edge.toId, edge.fromId)
this._addToContainedEdges(parentNode,childNode,edge); this._addToContainedEdges(parentNode,childNode,edge);
} }
else { else {
//console.log("REWIRE",parentNode.id, childNode.id, edge.toId, edge.fromId)
this._connectEdgeToCluster(parentNode,childNode,edge); this._connectEdgeToCluster(parentNode,childNode,edge);
} }
} }
@ -690,7 +713,6 @@ exports._addToCluster = function(parentNode, childNode, force) {
// forced clusters only open from screen size and double tap // forced clusters only open from screen size and double tap
if (force == true) { if (force == true) {
// parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
parentNode.formationScale = 0; parentNode.formationScale = 0;
} }
else { else {
@ -714,36 +736,6 @@ exports._addToCluster = function(parentNode, childNode, force) {
}; };
/**
* This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
exports._updateDynamicEdges = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.dynamicEdgesLength = node.dynamicEdges.length;
// this corrects for multiple edges pointing at the same other node
var correction = 0;
if (node.dynamicEdgesLength > 1) {
for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
var edgeToId = node.dynamicEdges[j].toId;
var edgeFromId = node.dynamicEdges[j].fromId;
for (var k = j+1; k < node.dynamicEdgesLength; k++) {
if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
(node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
correction += 1;
}
}
}
}
node.dynamicEdgesLength -= correction;
}
};
/** /**
* This adds an edge from the childNode to the contained edges of the parent node * This adds an edge from the childNode to the contained edges of the parent node
* *
@ -754,7 +746,7 @@ exports._updateDynamicEdges = function() {
*/ */
exports._addToContainedEdges = function(parentNode, childNode, edge) { exports._addToContainedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode // create an array object if it does not yet exist for this childNode
if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
if (parentNode.containedEdges[childNode.id] === undefined) {
parentNode.containedEdges[childNode.id] = [] parentNode.containedEdges[childNode.id] = []
} }
// add this edge to the list // add this edge to the list
@ -793,7 +785,6 @@ exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
edge.toId = parentNode.id; edge.toId = parentNode.id;
} }
else { // edge connected to other node with the "from" side else { // edge connected to other node with the "from" side
edge.originalFromId.push(childNode.id); edge.originalFromId.push(childNode.id);
edge.from = parentNode; edge.from = parentNode;
edge.fromId = parentNode.id; edge.fromId = parentNode.id;
@ -894,12 +885,14 @@ exports._connectEdgeBackToChild = function(parentNode, childNode) {
* @private * @private
*/ */
exports._validateEdges = function(parentNode) { exports._validateEdges = function(parentNode) {
var dynamicEdges = []
for (var i = 0; i < parentNode.dynamicEdges.length; i++) { for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i]; var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
parentNode.dynamicEdges.splice(i,1);
if (parentNode.id == edge.toId || parentNode.id == edge.fromId) {
dynamicEdges.push(edge);
} }
} }
parentNode.dynamicEdges = dynamicEdges;
}; };
@ -967,7 +960,7 @@ exports.updateLabels = function() {
// for (nodeId in this.nodes) { // for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) { // if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId]; // node = this.nodes[nodeId];
// node.label = String(node.level);
// node.label = String(node.clusterSize + ":" + node.dynamicEdges.length);
// } // }
// } // }
@ -1007,7 +1000,6 @@ exports.normalizeClusterLevels = function() {
} }
} }
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) { if (this.nodeIndices.length != amountOfNodes) {
this.clusterSession += 1; this.clusterSession += 1;
@ -1068,11 +1060,11 @@ exports._getHubSize = function() {
for (var i = 0; i < this.nodeIndices.length; i++) { for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]]; var node = this.nodes[this.nodeIndices[i]];
if (node.dynamicEdgesLength > largestHub) {
largestHub = node.dynamicEdgesLength;
if (node.dynamicEdges.length > largestHub) {
largestHub = node.dynamicEdges.length;
} }
average += node.dynamicEdgesLength;
averageSquared += Math.pow(node.dynamicEdgesLength,2);
average += node.dynamicEdges.length;
averageSquared += Math.pow(node.dynamicEdges.length,2);
hubCounter += 1; hubCounter += 1;
} }
average = average / hubCounter; average = average / hubCounter;
@ -1106,7 +1098,7 @@ exports._reduceAmountOfChains = function(fraction) {
var reduceAmount = Math.floor(this.nodeIndices.length * fraction); var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
for (var nodeId in this.nodes) { for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) { if (this.nodes.hasOwnProperty(nodeId)) {
if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
if (this.nodes[nodeId].dynamicEdges.length == 2) {
if (reduceAmount > 0) { if (reduceAmount > 0) {
this._formClusterFromHub(this.nodes[nodeId],true,true,1); this._formClusterFromHub(this.nodes[nodeId],true,true,1);
reduceAmount -= 1; reduceAmount -= 1;
@ -1127,7 +1119,7 @@ exports._getChainFraction = function() {
var total = 0; var total = 0;
for (var nodeId in this.nodes) { for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) { if (this.nodes.hasOwnProperty(nodeId)) {
if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
if (this.nodes[nodeId].dynamicEdges.length == 2) {
chains += 1; chains += 1;
} }
total += 1; total += 1;

+ 1
- 1
lib/network/mixins/HierarchicalLayoutMixin.js View File

@ -42,7 +42,7 @@ exports._setupHierarchicalLayout = function() {
// if the user defined some levels but not all, alert and run without hierarchical layout // if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) { if (undefinedLevel == true && definedLevel == true) {
throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent(undefined,true,this.constants.clustering.enabled);
this.zoomExtent({duration:0},true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) { if (!this.constants.clustering.enabled) {
this.start(); this.start();
} }

+ 75
- 49
lib/network/mixins/ManipulationMixin.js View File

@ -1,6 +1,7 @@
var util = require('../../util'); var util = require('../../util');
var Node = require('../Node'); var Node = require('../Node');
var Edge = require('../Edge'); var Edge = require('../Edge');
var Hammer = require('../../module/hammer');
/** /**
* clears the toolbar div element of children * clears the toolbar div element of children
@ -11,11 +12,24 @@ exports._clearManipulatorBar = function() {
this._recursiveDOMDelete(this.manipulationDiv); this._recursiveDOMDelete(this.manipulationDiv);
this.manipulationDOM = {}; this.manipulationDOM = {};
this._cleanManipulatorHammers();
this._manipulationReleaseOverload = function () {}; this._manipulationReleaseOverload = function () {};
delete this.sectors['support']['nodes']['targetNode']; delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode']; delete this.sectors['support']['nodes']['targetViaNode'];
this.controlNodesActive = false; this.controlNodesActive = false;
this.freezeSimulation = false;
this.freezeSimulation(false);
};
exports._cleanManipulatorHammers = function() {
// clean hammer bindings
if (this.manipulationHammers.length != 0) {
for (var i = 0; i < this.manipulationHammers.length; i++) {
this.manipulationHammers[i].dispose();
}
this.manipulationHammers = [];
}
}; };
/** /**
@ -48,13 +62,12 @@ exports._toggleEditMode = function() {
toolbar.style.display="block"; toolbar.style.display="block";
closeDiv.style.display="block"; closeDiv.style.display="block";
editModeDiv.style.display="none"; editModeDiv.style.display="none";
closeDiv.onclick = this._toggleEditMode.bind(this);
this._bindHammerToDiv(closeDiv,'_toggleEditMode');
} }
else { else {
toolbar.style.display="none"; toolbar.style.display="none";
closeDiv.style.display="none"; closeDiv.style.display="none";
editModeDiv.style.display="block"; editModeDiv.style.display="block";
closeDiv.onclick = null;
} }
this._createManipulatorBar() this._createManipulatorBar()
}; };
@ -70,6 +83,8 @@ exports._createManipulatorBar = function() {
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
} }
this._cleanManipulatorHammers();
var locale = this.constants.locales[this.constants.locale]; var locale = this.constants.locales[this.constants.locale];
if (this.edgeBeingEdited !== undefined) { if (this.edgeBeingEdited !== undefined) {
@ -84,7 +99,7 @@ exports._createManipulatorBar = function() {
this._restoreOverloadedFunctions(); this._restoreOverloadedFunctions();
// resume calculation // resume calculation
this.freezeSimulation = false;
this.freezeSimulation(false);
// reset global variables // reset global variables
this.blockConnectingEdgeSelection = false; this.blockConnectingEdgeSelection = false;
@ -96,9 +111,10 @@ exports._createManipulatorBar = function() {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
} }
this.manipulationDOM['addNodeSpan'] = document.createElement('span');
this.manipulationDOM['addNodeSpan'] = document.createElement('div');
this.manipulationDOM['addNodeSpan'].className = 'network-manipulationUI add'; this.manipulationDOM['addNodeSpan'].className = 'network-manipulationUI add';
this.manipulationDOM['addNodeLabelSpan'] = document.createElement('span');
this.manipulationDOM['addNodeLabelSpan'] = document.createElement('div');
this.manipulationDOM['addNodeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['addNodeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['addNodeLabelSpan'].innerHTML = locale['addNode']; this.manipulationDOM['addNodeLabelSpan'].innerHTML = locale['addNode'];
this.manipulationDOM['addNodeSpan'].appendChild(this.manipulationDOM['addNodeLabelSpan']); this.manipulationDOM['addNodeSpan'].appendChild(this.manipulationDOM['addNodeLabelSpan']);
@ -106,9 +122,9 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
this.manipulationDOM['addEdgeSpan'] = document.createElement('span');
this.manipulationDOM['addEdgeSpan'] = document.createElement('div');
this.manipulationDOM['addEdgeSpan'].className = 'network-manipulationUI connect'; this.manipulationDOM['addEdgeSpan'].className = 'network-manipulationUI connect';
this.manipulationDOM['addEdgeLabelSpan'] = document.createElement('span');
this.manipulationDOM['addEdgeLabelSpan'] = document.createElement('div');
this.manipulationDOM['addEdgeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['addEdgeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['addEdgeLabelSpan'].innerHTML = locale['addEdge']; this.manipulationDOM['addEdgeLabelSpan'].innerHTML = locale['addEdge'];
this.manipulationDOM['addEdgeSpan'].appendChild(this.manipulationDOM['addEdgeLabelSpan']); this.manipulationDOM['addEdgeSpan'].appendChild(this.manipulationDOM['addEdgeLabelSpan']);
@ -121,9 +137,9 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv2'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv2'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine';
this.manipulationDOM['editNodeSpan'] = document.createElement('span');
this.manipulationDOM['editNodeSpan'] = document.createElement('div');
this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit'; this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit';
this.manipulationDOM['editNodeLabelSpan'] = document.createElement('span');
this.manipulationDOM['editNodeLabelSpan'] = document.createElement('div');
this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode']; this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode'];
this.manipulationDOM['editNodeSpan'].appendChild(this.manipulationDOM['editNodeLabelSpan']); this.manipulationDOM['editNodeSpan'].appendChild(this.manipulationDOM['editNodeLabelSpan']);
@ -135,9 +151,9 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv3'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv3'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine';
this.manipulationDOM['editEdgeSpan'] = document.createElement('span');
this.manipulationDOM['editEdgeSpan'] = document.createElement('div');
this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit'; this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit';
this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('span');
this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('div');
this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge']; this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge'];
this.manipulationDOM['editEdgeSpan'].appendChild(this.manipulationDOM['editEdgeLabelSpan']); this.manipulationDOM['editEdgeSpan'].appendChild(this.manipulationDOM['editEdgeLabelSpan']);
@ -149,9 +165,9 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv4'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv4'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv4'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv4'].className = 'network-seperatorLine';
this.manipulationDOM['deleteSpan'] = document.createElement('span');
this.manipulationDOM['deleteSpan'] = document.createElement('div');
this.manipulationDOM['deleteSpan'].className = 'network-manipulationUI delete'; this.manipulationDOM['deleteSpan'].className = 'network-manipulationUI delete';
this.manipulationDOM['deleteLabelSpan'] = document.createElement('span');
this.manipulationDOM['deleteLabelSpan'] = document.createElement('div');
this.manipulationDOM['deleteLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['deleteLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['deleteLabelSpan'].innerHTML = locale['del']; this.manipulationDOM['deleteLabelSpan'].innerHTML = locale['del'];
this.manipulationDOM['deleteSpan'].appendChild(this.manipulationDOM['deleteLabelSpan']); this.manipulationDOM['deleteSpan'].appendChild(this.manipulationDOM['deleteLabelSpan']);
@ -160,20 +176,20 @@ exports._createManipulatorBar = function() {
this.manipulationDiv.appendChild(this.manipulationDOM['deleteSpan']); this.manipulationDiv.appendChild(this.manipulationDOM['deleteSpan']);
} }
// bind the icons // bind the icons
this.manipulationDOM['addNodeSpan'].onclick = this._createAddNodeToolbar.bind(this);
this.manipulationDOM['addEdgeSpan'].onclick = this._createAddEdgeToolbar.bind(this);
this._bindHammerToDiv(this.manipulationDOM['addNodeSpan'],'_createAddNodeToolbar');
this._bindHammerToDiv(this.manipulationDOM['addEdgeSpan'],'_createAddEdgeToolbar');
this._bindHammerToDiv(this.closeDiv,'_toggleEditMode');
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDOM['editNodeSpan'].onclick = this._editNode.bind(this);
this._bindHammerToDiv(this.manipulationDOM['editNodeSpan'],'_editNode');
} }
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
this.manipulationDOM['editEdgeSpan'].onclick = this._createEditEdgeToolbar.bind(this);
this._bindHammerToDiv(this.manipulationDOM['editEdgeSpan'],'_createEditEdgeToolbar');
} }
if (this._selectionIsEmpty() == false) { if (this._selectionIsEmpty() == false) {
this.manipulationDOM['deleteSpan'].onclick = this._deleteSelected.bind(this);
this._bindHammerToDiv(this.manipulationDOM['deleteSpan'],'_deleteSelected');
} }
this.closeDiv.onclick = this._toggleEditMode.bind(this);
var me = this; var me = this;
this.boundFunction = me._createManipulatorBar; this.boundFunction = me._createManipulatorBar;
@ -184,20 +200,26 @@ exports._createManipulatorBar = function() {
this.editModeDiv.removeChild(this.editModeDiv.firstChild); this.editModeDiv.removeChild(this.editModeDiv.firstChild);
} }
this.manipulationDOM['editModeSpan'] = document.createElement('span');
this.manipulationDOM['editModeSpan'] = document.createElement('div');
this.manipulationDOM['editModeSpan'].className = 'network-manipulationUI edit editmode'; this.manipulationDOM['editModeSpan'].className = 'network-manipulationUI edit editmode';
this.manipulationDOM['editModeLabelSpan'] = document.createElement('span');
this.manipulationDOM['editModeLabelSpan'] = document.createElement('div');
this.manipulationDOM['editModeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['editModeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['editModeLabelSpan'].innerHTML = locale['edit']; this.manipulationDOM['editModeLabelSpan'].innerHTML = locale['edit'];
this.manipulationDOM['editModeSpan'].appendChild(this.manipulationDOM['editModeLabelSpan']); this.manipulationDOM['editModeSpan'].appendChild(this.manipulationDOM['editModeLabelSpan']);
this.editModeDiv.appendChild(this.manipulationDOM['editModeSpan']); this.editModeDiv.appendChild(this.manipulationDOM['editModeSpan']);
this.manipulationDOM['editModeSpan'].onclick = this._toggleEditMode.bind(this);
this._bindHammerToDiv(this.manipulationDOM['editModeSpan'],'_toggleEditMode');
} }
}; };
exports._bindHammerToDiv = function(domElement, funct) {
var hammer = Hammer(domElement, {prevent_default: true});
hammer.on('touch', this[funct].bind(this));
this.manipulationHammers.push(hammer);
}
/** /**
* Create the toolbar for adding Nodes * Create the toolbar for adding Nodes
@ -214,9 +236,9 @@ exports._createAddNodeToolbar = function() {
var locale = this.constants.locales[this.constants.locale]; var locale = this.constants.locales[this.constants.locale];
this.manipulationDOM = {}; this.manipulationDOM = {};
this.manipulationDOM['backSpan'] = document.createElement('span');
this.manipulationDOM['backSpan'] = document.createElement('div');
this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
this.manipulationDOM['backLabelSpan'] = document.createElement('span');
this.manipulationDOM['backLabelSpan'] = document.createElement('div');
this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
@ -224,9 +246,9 @@ exports._createAddNodeToolbar = function() {
this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
this.manipulationDOM['descriptionSpan'] = document.createElement('span');
this.manipulationDOM['descriptionSpan'] = document.createElement('div');
this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div');
this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['addDescription']; this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['addDescription'];
this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
@ -236,7 +258,7 @@ exports._createAddNodeToolbar = function() {
this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
// bind the icon // bind the icon
this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar');
// we use the boundFunction so we can reference it when we unbind it from the "select" event. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
var me = this; var me = this;
@ -254,7 +276,7 @@ exports._createAddEdgeToolbar = function() {
// clear the toolbar // clear the toolbar
this._clearManipulatorBar(); this._clearManipulatorBar();
this._unselectAll(true); this._unselectAll(true);
this.freezeSimulation = true;
this.freezeSimulation(true);
if (this.boundFunction) { if (this.boundFunction) {
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
@ -267,9 +289,9 @@ exports._createAddEdgeToolbar = function() {
this.blockConnectingEdgeSelection = true; this.blockConnectingEdgeSelection = true;
this.manipulationDOM = {}; this.manipulationDOM = {};
this.manipulationDOM['backSpan'] = document.createElement('span');
this.manipulationDOM['backSpan'] = document.createElement('div');
this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
this.manipulationDOM['backLabelSpan'] = document.createElement('span');
this.manipulationDOM['backLabelSpan'] = document.createElement('div');
this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
@ -277,9 +299,9 @@ exports._createAddEdgeToolbar = function() {
this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
this.manipulationDOM['descriptionSpan'] = document.createElement('span');
this.manipulationDOM['descriptionSpan'] = document.createElement('div');
this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div');
this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['edgeDescription']; this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['edgeDescription'];
this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
@ -289,7 +311,7 @@ exports._createAddEdgeToolbar = function() {
this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
// bind the icon // bind the icon
this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar');
// we use the boundFunction so we can reference it when we unbind it from the "select" event. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
var me = this; var me = this;
@ -301,10 +323,12 @@ exports._createAddEdgeToolbar = function() {
this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload; this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
this.cachedFunctions["_handleDragStart"] = this._handleDragStart; this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd; this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd;
this.cachedFunctions["_handleOnHold"] = this._handleOnHold;
this._handleTouch = this._handleConnect; this._handleTouch = this._handleConnect;
this._manipulationReleaseOverload = function () {}; this._manipulationReleaseOverload = function () {};
this._handleOnHold = function () {};
this._handleDragStart = function () {}; this._handleDragStart = function () {};
this._handleDragEnd = this._finishConnect;
this._handleDragEnd = this._finishConnect;
// redraw to show the unselect // redraw to show the unselect
this._redraw(); this._redraw();
@ -330,9 +354,9 @@ exports._createEditEdgeToolbar = function() {
var locale = this.constants.locales[this.constants.locale]; var locale = this.constants.locales[this.constants.locale];
this.manipulationDOM = {}; this.manipulationDOM = {};
this.manipulationDOM['backSpan'] = document.createElement('span');
this.manipulationDOM['backSpan'] = document.createElement('div');
this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
this.manipulationDOM['backLabelSpan'] = document.createElement('span');
this.manipulationDOM['backLabelSpan'] = document.createElement('div');
this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
@ -340,9 +364,9 @@ exports._createEditEdgeToolbar = function() {
this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
this.manipulationDOM['descriptionSpan'] = document.createElement('span');
this.manipulationDOM['descriptionSpan'] = document.createElement('div');
this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div');
this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['editEdgeDescription']; this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['editEdgeDescription'];
this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
@ -352,7 +376,7 @@ exports._createEditEdgeToolbar = function() {
this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
// bind the icon // bind the icon
this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar');
// temporarily overload functions // temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch; this.cachedFunctions["_handleTouch"] = this._handleTouch;
@ -383,7 +407,7 @@ exports._selectControlNode = function(pointer) {
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
if (this.selectedControlNode !== null) { if (this.selectedControlNode !== null) {
this.selectedControlNode.select(); this.selectedControlNode.select();
this.freezeSimulation = true;
this.freezeSimulation(true);
} }
this._redraw(); this._redraw();
}; };
@ -396,7 +420,7 @@ exports._selectControlNode = function(pointer) {
* @private * @private
*/ */
exports._controlNodeDrag = function(event) { exports._controlNodeDrag = function(event) {
var pointer = this._getPointer(event.center);
var pointer = this._getPointer(event.gesture.center);
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
@ -427,7 +451,7 @@ exports._releaseControlNode = function(pointer) {
else { else {
this.edgeBeingEdited._restoreControlNodes(); this.edgeBeingEdited._restoreControlNodes();
} }
this.freezeSimulation = false;
this.freezeSimulation(false);
this._redraw(); this._redraw();
}; };
@ -469,11 +493,13 @@ exports._handleConnect = function(pointer) {
connectionEdge.to = targetNode; connectionEdge.to = targetNode;
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
var me = this;
this._handleOnDrag = function(event) { this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.center);
var connectionEdge = this.edges['connectionEdge'];
connectionEdge.to.x = this._XconvertDOMtoCanvas(pointer.x);
connectionEdge.to.y = this._YconvertDOMtoCanvas(pointer.y);
var pointer = this._getPointer(event.gesture.center);
var connectionEdge = me.edges['connectionEdge'];
connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x);
connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y);
me._redraw();
}; };
this.moving = true; this.moving = true;
@ -485,7 +511,7 @@ exports._handleConnect = function(pointer) {
exports._finishConnect = function(event) { exports._finishConnect = function(event) {
if (this._getSelectedNodeCount() == 1) { if (this._getSelectedNodeCount() == 1) {
var pointer = this._getPointer(event.center);
var pointer = this._getPointer(event.gesture.center);
// restore the drag function // restore the drag function
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
delete this.cachedFunctions["_handleOnDrag"]; delete this.cachedFunctions["_handleOnDrag"];

+ 15
- 19
lib/network/mixins/NavigationMixin.js View File

@ -1,21 +1,20 @@
var util = require('../../util'); var util = require('../../util');
var hammerUtil = require('../../hammerUtil');
var Hammer = require('../../module/hammer'); var Hammer = require('../../module/hammer');
exports._cleanNavigation = function() { exports._cleanNavigation = function() {
// clean hammer bindings // clean hammer bindings
if (this.navigationHammers.existing.length != 0) {
for (var i = 0; i < this.navigationHammers.existing.length; i++) {
this.navigationHammers.existing[i].destroy();
if (this.navigationHammers.length != 0) {
for (var i = 0; i < this.navigationHammers.length; i++) {
this.navigationHammers[i].dispose();
} }
this.navigationHammers.existing = [];
this.navigationHammers = [];
} }
this._navigationReleaseOverload = function () {}; this._navigationReleaseOverload = function () {};
// clean up previous navigation items // clean up previous navigation items
if (this.navigationDivs && this.navigationDivs['wrapper'] && this.navigationDivs['wrapper'].parentNode) {
this.navigationDivs['wrapper'].parentNode.removeChild(this.navigationDivs['wrapper']);
if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) {
this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']);
} }
}; };
@ -30,28 +29,25 @@ exports._cleanNavigation = function() {
exports._loadNavigationElements = function() { exports._loadNavigationElements = function() {
this._cleanNavigation(); this._cleanNavigation();
this.navigationDivs = {};
this.navigationDOM = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent']; var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent'];
this.navigationDivs['wrapper'] = document.createElement('div');
this.frame.appendChild(this.navigationDivs['wrapper']);
this.navigationDOM['wrapper'] = document.createElement('div');
this.frame.appendChild(this.navigationDOM['wrapper']);
for (var i = 0; i < navigationDivs.length; i++) { for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDivs[navigationDivs[i]] = document.createElement('div');
this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDOM[navigationDivs[i]] = document.createElement('div');
this.navigationDOM[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i];
this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]);
var hammer = new Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true});
hammerUtil.onTouch(hammer, this[navigationDivActions[i]].bind(this));
hammerUtil.onRelease(hammer, this._onRelease.bind(this));
this.navigationHammers._new.push(hammer);
var hammer = Hammer(this.navigationDOM[navigationDivs[i]], {prevent_default: true});
hammer.on('touch', this[navigationDivActions[i]].bind(this));
this.navigationHammers.push(hammer);
} }
this._navigationReleaseOverload = this._stopMovement; this._navigationReleaseOverload = this._stopMovement;
this.navigationHammers.existing = this.navigationHammers._new;
}; };

+ 2
- 2
lib/network/mixins/SelectionMixin.js View File

@ -512,7 +512,7 @@ exports._handleTap = function(pointer) {
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
} }
this.emit("click", properties); this.emit("click", properties);
this._redraw();
this._requestRedraw();
}; };
@ -556,7 +556,7 @@ exports._handleOnHold = function(pointer) {
this._selectObject(edge,true); this._selectObject(edge,true);
} }
} }
this._redraw();
this._requestRedraw();
}; };

+ 6
- 3
lib/network/mixins/physics/PhysicsMixin.js View File

@ -325,6 +325,9 @@ exports._loadPhysicsConfiguration = function () {
this.backupConstants = {}; this.backupConstants = {};
util.deepExtend(this.backupConstants,this.constants); util.deepExtend(this.backupConstants,this.constants);
var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10);
var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10)
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
this.physicsConfiguration = document.createElement('div'); this.physicsConfiguration = document.createElement('div');
this.physicsConfiguration.className = "PhysicsConfiguration"; this.physicsConfiguration.className = "PhysicsConfiguration";
@ -339,16 +342,16 @@ exports._loadPhysicsConfiguration = function () {
'<table id="graph_BH_table" style="display:none">' + '<table id="graph_BH_table" style="display:none">' +
'<tr><td><b>Barnes Hut</b></td></tr>' + '<tr><td><b>Barnes Hut</b></td></tr>' +
'<tr>' + '<tr>' +
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" 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>' +
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="'+maxGravitational+'" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-'+maxGravitational+'</td><td><input value="' + (this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
'</tr>' + '</tr>' +
'<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>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="6" 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>' +
'<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>' + '<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>' +
'<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>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="'+maxSpring+'" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.0001" style="width:300px" id="graph_BH_sc"></td><td>'+maxSpring+'</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
'</tr>' + '</tr>' +
'<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>' + '<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>' +

+ 121
- 11
lib/timeline/Core.js View File

@ -7,6 +7,7 @@ var Range = require('./Range');
var ItemSet = require('./component/ItemSet'); var ItemSet = require('./component/ItemSet');
var Activator = require('../shared/Activator'); var Activator = require('../shared/Activator');
var DateUtil = require('./DateUtil'); var DateUtil = require('./DateUtil');
var CustomTime = require('./component/CustomTime');
/** /**
* Create a timeline visualization * Create a timeline visualization
@ -97,13 +98,13 @@ Core.prototype._create = function (container) {
if (!me._redrawTimer) { if (!me._redrawTimer) {
me._redrawTimer = setTimeout(function () { me._redrawTimer = setTimeout(function () {
me._redrawTimer = null; me._redrawTimer = null;
me.redraw();
me._redraw();
}, 0) }, 0)
} }
} }
else { else {
// redraw immediately // redraw immediately
me.redraw();
me._redraw();
} }
}); });
@ -238,7 +239,7 @@ Core.prototype.setOptions = function (options) {
} }
// redraw everything // redraw everything
this.redraw();
this._redraw();
}; };
/** /**
@ -295,25 +296,123 @@ Core.prototype.destroy = function () {
/** /**
* Set a custom time bar * Set a custom time bar
* @param {Date} time * @param {Date} time
* @param {int} id
*/ */
Core.prototype.setCustomTime = function (time) {
Core.prototype.setCustomTime = function (time, id) {
if (!this.customTime) { if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled'); throw new Error('Cannot get custom time: Custom time bar is not enabled');
} }
this.customTime.setCustomTime(time);
var barId = id || 0;
this.components.forEach(function (element, index, components) {
if (element instanceof CustomTime && element.options.id === barId) {
element.setCustomTime(time);
}
});
}; };
/** /**
* Retrieve the current custom time. * Retrieve the current custom time.
* @return {Date} customTime * @return {Date} customTime
* @param {int} id
*/ */
Core.prototype.getCustomTime = function() {
Core.prototype.getCustomTime = function(id) {
if (!this.customTime) { if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled'); throw new Error('Cannot get custom time: Custom time bar is not enabled');
} }
return this.customTime.getCustomTime();
var barId = id || 0,
customTime = this.customTime.getCustomTime();
this.components.forEach(function (element, index, components) {
if (element instanceof CustomTime && element.options.id === barId) {
customTime = element.getCustomTime();
}
});
return customTime;
};
/**
* Add custom vertical bar
* @param {Date | String | Number} time A Date, unix timestamp, or
* ISO date string. Time point where the new bar should be placed
* @param {Number | String} ID of the new bar
* @return {Number | String} ID of the new bar
*/
Core.prototype.addCustomTime = function (time, id) {
if (!this.currentTime) {
throw new Error('Option showCurrentTime must be true');
}
if (time === undefined) {
throw new Error('Time parameter for the custom bar must be provided');
}
var ts = util.convert(time, 'Date').valueOf(),
numIds, customTime, customBarId;
// All bar IDs are kept in 1 array, mixed types
// Bar with ID 0 is the default bar.
if (!this.customBarIds || this.customBarIds.constructor !== Array) {
this.customBarIds = [0];
}
// If the ID is not provided, generate one, otherwise just use it
if (id === undefined) {
numIds = this.customBarIds.filter(function (element) {
return util.isNumber(element);
});
customBarId = numIds.length > 0 ? Math.max.apply(null, numIds) + 1 : 1;
} else {
// Check for duplicates
this.customBarIds.forEach(function (element) {
if (element === id) {
throw new Error('Custom time ID already exists');
}
});
customBarId = id;
}
this.customBarIds.push(customBarId);
customTime = new CustomTime(this.body, {
showCustomTime : true,
time : ts,
id : customBarId
});
this.components.push(customTime);
this.redraw();
return customBarId;
};
/**
* Remove previously added custom bar
* @param {int} id ID of the custom bar to be removed
* @return {boolean} True if the bar exists and is removed, false otherwise
*/
Core.prototype.removeCustomTime = function (id) {
var me = this;
this.components.forEach(function (bar, index, components) {
if (bar instanceof CustomTime && bar.options.id === id) {
// Only the lines added by the user will be removed
if (bar.options.id !== 0) {
me.customBarIds.splice(me.customBarIds.indexOf(id), 1);
components.splice(index, 1);
bar.destroy();
}
}
});
}; };
@ -412,6 +511,7 @@ Core.prototype._getDataRange = function() {
* start or only end. Syntax: * start or only end. Syntax:
* *
* TimeLine.setWindow(start, end) * TimeLine.setWindow(start, end)
* TimeLine.setWindow(start, end, options)
* TimeLine.setWindow(range) * TimeLine.setWindow(range)
* *
* Where start and end can be a Date, number, or string, and range is an * Where start and end can be a Date, number, or string, and range is an
@ -427,12 +527,14 @@ Core.prototype._getDataRange = function() {
* for the animation. Default duration is 500 ms. * for the animation. Default duration is 500 ms.
*/ */
Core.prototype.setWindow = function(start, end, options) { Core.prototype.setWindow = function(start, end, options) {
var animate = (options && options.animate !== undefined) ? options.animate : true;
var animate;
if (arguments.length == 1) { if (arguments.length == 1) {
var range = arguments[0]; var range = arguments[0];
animate = (range.animate !== undefined) ? range.animate : true;
this.range.setRange(range.start, range.end, animate); this.range.setRange(range.start, range.end, animate);
} }
else { else {
animate = (options && options.animate !== undefined) ? options.animate : true;
this.range.setRange(start, end, animate); this.range.setRange(start, end, animate);
} }
}; };
@ -471,10 +573,18 @@ Core.prototype.getWindow = function() {
}; };
/** /**
* Force a redraw of the Core. Can be useful to manually redraw when
* option autoResize=false
* Force a redraw. Can be overridden by implementations of Core
*/ */
Core.prototype.redraw = function() { Core.prototype.redraw = function() {
this._redraw();
};
/**
* Redraw for internal use. Redraws all components. See also the public
* method redraw.
* @protected
*/
Core.prototype._redraw = function() {
var resized = false; var resized = false;
var options = this.options; var options = this.options;
var props = this.props; var props = this.props;
@ -625,7 +735,7 @@ Core.prototype.redraw = function() {
var MAX_REDRAWS = 3; // maximum number of consecutive redraws var MAX_REDRAWS = 3; // maximum number of consecutive redraws
if (this.redrawCount < MAX_REDRAWS) { if (this.redrawCount < MAX_REDRAWS) {
this.redrawCount++; this.redrawCount++;
this.redraw();
this._redraw();
} }
else { else {
console.log('WARNING: infinite loop in redraw?'); console.log('WARNING: infinite loop in redraw?');

+ 0
- 12
lib/timeline/DataStep.js View File

@ -251,18 +251,6 @@ DataStep.prototype.getCurrent = function(decimals) {
return toPrecision; return toPrecision;
}; };
/**
* Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
DataStep.prototype.snap = function(date) {
};
/** /**
* Check if the current value is a major value (for example when the step * Check if the current value is a major value (for example when the step
* is DAY, a major value is each first day of the MONTH) * is DAY, a major value is each first day of the MONTH)

+ 2
- 3
lib/timeline/Graph2d.js View File

@ -57,7 +57,6 @@ function Graph2d (container, items, groups, options) {
}, },
hiddenDates: [], hiddenDates: [],
util: { util: {
snap: null, // will be specified after TimeAxis is created
toScreen: me._toScreen.bind(me), toScreen: me._toScreen.bind(me),
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
toTime: me._toTime.bind(me), toTime: me._toTime.bind(me),
@ -73,7 +72,7 @@ function Graph2d (container, items, groups, options) {
// time axis // time axis
this.timeAxis = new TimeAxis(this.body); this.timeAxis = new TimeAxis(this.body);
this.components.push(this.timeAxis); this.components.push(this.timeAxis);
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
//this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar // current time bar
this.currentTime = new CurrentTime(this.body); this.currentTime = new CurrentTime(this.body);
@ -106,7 +105,7 @@ function Graph2d (container, items, groups, options) {
this.setItems(items); this.setItems(items);
} }
else { else {
this.redraw();
this._redraw();
} }
} }

+ 5
- 4
lib/timeline/Range.js View File

@ -156,7 +156,7 @@ Range.prototype.setRange = function(start, end, animate, byUser) {
me.animateTimer = setTimeout(next, 20); me.animateTimer = setTimeout(next, 20);
} }
} }
}
};
return next(); return next();
} }
@ -250,7 +250,7 @@ Range.prototype._applyRange = function(start, end) {
zoomMin = 0; zoomMin = 0;
} }
if ((newEnd - newStart) < zoomMin) { if ((newEnd - newStart) < zoomMin) {
if ((this.end - this.start) === zoomMin) {
if ((this.end - this.start) === zoomMin && newStart > this.start && newEnd < this.end) {
// ignore this action, we are already zoomed to the minimum // ignore this action, we are already zoomed to the minimum
newStart = this.start; newStart = this.start;
newEnd = this.end; newEnd = this.end;
@ -270,8 +270,9 @@ Range.prototype._applyRange = function(start, end) {
if (zoomMax < 0) { if (zoomMax < 0) {
zoomMax = 0; zoomMax = 0;
} }
if ((newEnd - newStart) > zoomMax) { if ((newEnd - newStart) > zoomMax) {
if ((this.end - this.start) === zoomMax) {
if ((this.end - this.start) === zoomMax && newStart < this.start && newEnd > this.end) {
// ignore this action, we are already zoomed to the maximum // ignore this action, we are already zoomed to the maximum
newStart = this.start; newStart = this.start;
newEnd = this.end; newEnd = this.end;
@ -287,7 +288,7 @@ Range.prototype._applyRange = function(start, end) {
var changed = (this.start != newStart || this.end != newEnd); var changed = (this.start != newStart || this.end != newEnd);
// if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not neccesarily of type Range)
// if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range)
if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) && if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&
!((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) { !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {
this.body.emitter.emit('checkRangedItems'); this.body.emitter.emit('checkRangedItems');

+ 34
- 31
lib/timeline/TimeStep.js View File

@ -240,23 +240,22 @@ TimeStep.prototype.getCurrent = function() {
/** /**
* Set a custom scale. Autoscaling will be disabled. * Set a custom scale. Autoscaling will be disabled.
* For example setScale(SCALE.MINUTES, 5) will result
* For example setScale('minute', 5) will result
* in minor steps of 5 minutes, and major steps of an hour. * in minor steps of 5 minutes, and major steps of an hour.
* *
* @param {string} newScale
* A scale. Choose from 'millisecond, 'second,
* 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
* @param {Number} newStep A step size, by default 1. Choose for
* example 1, 2, 5, or 10.
* @param {{scale: string, step: number}} params
* An object containing two properties:
* - A string 'scale'. Choose from 'millisecond', 'second',
* 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
* - A number 'step'. A step size, by default 1.
* Choose for example 1, 2, 5, or 10.
*/ */
TimeStep.prototype.setScale = function(newScale, newStep) {
this.scale = newScale;
if (newStep > 0) {
this.step = newStep;
TimeStep.prototype.setScale = function(params) {
if (params && typeof params.scale == 'string') {
this.scale = params.scale;
this.step = params.step > 0 ? params.step : 1;
this.autoScale = false;
} }
this.autoScale = false;
}; };
/** /**
@ -322,15 +321,19 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) {
/** /**
* Snap a date to a rounded value. * Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step. * The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* Static function
* @param {Date} date the date to be snapped.
* @param {string} scale Current scale, can be 'millisecond', 'second',
* 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
* @param {number} step Current step (1, 2, 4, 5, ...
* @return {Date} snappedDate * @return {Date} snappedDate
*/ */
TimeStep.prototype.snap = function(date) {
TimeStep.snap = function(date, scale, step) {
var clone = new Date(date.valueOf()); var clone = new Date(date.valueOf());
if (this.scale == 'year') {
if (scale == 'year') {
var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
clone.setFullYear(Math.round(year / this.step) * this.step);
clone.setFullYear(Math.round(year / step) * step);
clone.setMonth(0); clone.setMonth(0);
clone.setDate(0); clone.setDate(0);
clone.setHours(0); clone.setHours(0);
@ -338,7 +341,7 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'month') {
else if (scale == 'month') {
if (clone.getDate() > 15) { if (clone.getDate() > 15) {
clone.setDate(1); clone.setDate(1);
clone.setMonth(clone.getMonth() + 1); clone.setMonth(clone.getMonth() + 1);
@ -353,9 +356,9 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'day') {
else if (scale == 'day') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 5: case 5:
case 2: case 2:
clone.setHours(Math.round(clone.getHours() / 24) * 24); break; clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
@ -366,9 +369,9 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'weekday') {
else if (scale == 'weekday') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 5: case 5:
case 2: case 2:
clone.setHours(Math.round(clone.getHours() / 12) * 12); break; clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
@ -379,8 +382,8 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'hour') {
switch (this.step) {
else if (scale == 'hour') {
switch (step) {
case 4: case 4:
clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
default: default:
@ -388,9 +391,9 @@ TimeStep.prototype.snap = function(date) {
} }
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} else if (this.scale == 'minute') {
} else if (scale == 'minute') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 15: case 15:
case 10: case 10:
clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
@ -403,9 +406,9 @@ TimeStep.prototype.snap = function(date) {
} }
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'second') {
else if (scale == 'second') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 15: case 15:
case 10: case 10:
clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
@ -417,9 +420,9 @@ TimeStep.prototype.snap = function(date) {
clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
} }
} }
else if (this.scale == 'millisecond') {
var step = this.step > 5 ? this.step / 2 : 1;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
else if (scale == 'millisecond') {
var _step = step > 5 ? step / 2 : 1;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step);
} }
return clone; return clone;

+ 21
- 6
lib/timeline/Timeline.js View File

@ -13,8 +13,8 @@ var ItemSet = require('./component/ItemSet');
/** /**
* Create a timeline visualization * Create a timeline visualization
* @param {HTMLElement} container * @param {HTMLElement} container
* @param {vis.DataSet | Array | google.visualization.DataTable} [items]
* @param {vis.DataSet | Array | google.visualization.DataTable} [groups]
* @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [items]
* @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [groups]
* @param {Object} [options] See Timeline.setOptions for the available options. * @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor * @constructor
* @extends Core * @extends Core
@ -25,7 +25,7 @@ function Timeline (container, items, groups, options) {
} }
// if the third element is options, the forth is groups (optionally); // if the third element is options, the forth is groups (optionally);
if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
var forthArgument = options; var forthArgument = options;
options = groups; options = groups;
groups = forthArgument; groups = forthArgument;
@ -62,7 +62,13 @@ function Timeline (container, items, groups, options) {
}, },
hiddenDates: [], hiddenDates: [],
util: { util: {
snap: null, // will be specified after TimeAxis is created
getScale: function () {
return me.timeAxis.step.scale;
},
getStep: function () {
return me.timeAxis.step.step;
},
toScreen: me._toScreen.bind(me), toScreen: me._toScreen.bind(me),
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
toTime: me._toTime.bind(me), toTime: me._toTime.bind(me),
@ -78,7 +84,6 @@ function Timeline (container, items, groups, options) {
// time axis // time axis
this.timeAxis = new TimeAxis(this.body); this.timeAxis = new TimeAxis(this.body);
this.components.push(this.timeAxis); this.components.push(this.timeAxis);
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar // current time bar
this.currentTime = new CurrentTime(this.body); this.currentTime = new CurrentTime(this.body);
@ -111,13 +116,23 @@ function Timeline (container, items, groups, options) {
this.setItems(items); this.setItems(items);
} }
else { else {
this.redraw();
this._redraw();
} }
} }
// Extend the functionality from Core // Extend the functionality from Core
Timeline.prototype = new Core(); Timeline.prototype = new Core();
/**
* Force a redraw. The size of all items will be recalculated.
* Can be useful to manually redraw when option autoResize=false and the window
* has been resized, or when the items CSS has been changed.
*/
Timeline.prototype.redraw = function() {
this.itemSet && this.itemSet.markDirty({refreshItems: true});
this._redraw();
};
/** /**
* Set items * Set items
* @param {vis.DataSet | Array | google.visualization.DataTable | null} items * @param {vis.DataSet | Array | google.visualization.DataTable | null} items

+ 16
- 3
lib/timeline/component/CustomTime.js View File

@ -20,11 +20,17 @@ function CustomTime (body, options) {
this.defaultOptions = { this.defaultOptions = {
showCustomTime: false, showCustomTime: false,
locales: locales, locales: locales,
locale: 'en'
locale: 'en',
id: 0
}; };
this.options = util.extend({}, this.defaultOptions); this.options = util.extend({}, this.defaultOptions);
this.customTime = new Date();
if (options && options.time) {
this.customTime = options.time;
} else {
this.customTime = new Date();
}
this.eventParams = {}; // stores state parameters while dragging the bar this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM // create the DOM
@ -43,7 +49,12 @@ CustomTime.prototype = new Component();
CustomTime.prototype.setOptions = function(options) { CustomTime.prototype.setOptions = function(options) {
if (options) { if (options) {
// copy all options that we know // copy all options that we know
util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options);
util.selectiveExtend(['showCustomTime', 'locale', 'locales', 'id'], this.options, options);
// Triggered by addCustomTimeBar, redraw to add new bar
if (this.options.id) {
this.redraw();
}
} }
}; };
@ -169,6 +180,7 @@ CustomTime.prototype._onDrag = function (event) {
// fire a timechange event // fire a timechange event
this.body.emitter.emit('timechange', { this.body.emitter.emit('timechange', {
id: this.options.id,
time: new Date(this.customTime.valueOf()) time: new Date(this.customTime.valueOf())
}); });
@ -186,6 +198,7 @@ CustomTime.prototype._onDragEnd = function (event) {
// fire a timechanged event // fire a timechanged event
this.body.emitter.emit('timechanged', { this.body.emitter.emit('timechanged', {
id: this.options.id,
time: new Date(this.customTime.valueOf()) time: new Date(this.customTime.valueOf())
}); });

+ 0
- 10
lib/timeline/component/DataAxis.js View File

@ -626,14 +626,4 @@ DataAxis.prototype._calculateCharSize = function () {
} }
}; };
/**
* Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
DataAxis.prototype.snap = function(date) {
return this.step.snap(date);
};
module.exports = DataAxis; module.exports = DataAxis;

+ 79
- 21
lib/timeline/component/ItemSet.js View File

@ -2,6 +2,7 @@ var Hammer = require('../../module/hammer');
var util = require('../../util'); var util = require('../../util');
var DataSet = require('../../DataSet'); var DataSet = require('../../DataSet');
var DataView = require('../../DataView'); var DataView = require('../../DataView');
var TimeStep = require('../TimeStep');
var Component = require('./Component'); var Component = require('./Component');
var Group = require('./Group'); var Group = require('./Group');
var BackgroundGroup = require('./BackgroundGroup'); var BackgroundGroup = require('./BackgroundGroup');
@ -41,6 +42,8 @@ function ItemSet(body, options) {
remove: false remove: false
}, },
snap: TimeStep.snap,
onAdd: function (item, callback) { onAdd: function (item, callback) {
callback(item); callback(item);
}, },
@ -273,7 +276,7 @@ ItemSet.prototype._create = function(){
ItemSet.prototype.setOptions = function(options) { ItemSet.prototype.setOptions = function(options) {
if (options) { if (options) {
// copy all options that we know // copy all options that we know
var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide'];
var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap'];
util.selectiveExtend(fields, this.options, options); util.selectiveExtend(fields, this.options, options);
if ('margin' in options) { if ('margin' in options) {
@ -326,11 +329,20 @@ ItemSet.prototype.setOptions = function(options) {
}; };
/** /**
* Mark the ItemSet dirty so it will refresh everything with next redraw
* Mark the ItemSet dirty so it will refresh everything with next redraw.
* Optionally, all items can be marked as dirty and be refreshed.
* @param {{refreshItems: boolean}} [options]
*/ */
ItemSet.prototype.markDirty = function() {
ItemSet.prototype.markDirty = function(options) {
this.groupIds = []; this.groupIds = [];
this.stackDirty = true; this.stackDirty = true;
if (options && options.refreshItems) {
util.forEach(this.items, function (item) {
item.dirty = true;
if (item.displayed) item.redraw();
});
}
}; };
/** /**
@ -1136,8 +1148,15 @@ ItemSet.prototype._onDragStart = function (event) {
}; };
if (me.options.editable.updateTime) { if (me.options.editable.updateTime) {
if ('start' in item.data) props.start = item.data.start.valueOf();
if ('end' in item.data) props.end = item.data.end.valueOf();
if ('start' in item.data) {
props.start = item.data.start.valueOf();
if ('end' in item.data) {
// we store a duration here in order not to change the width
// of the item when moving it.
props.duration = item.data.end.valueOf() - props.start;
}
}
} }
if (me.options.editable.updateGroup) { if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group; if ('group' in item.data) props.group = item.data.group;
@ -1162,8 +1181,10 @@ ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.itemProps) { if (this.touchParams.itemProps) {
var me = this; var me = this;
var snap = this.body.util.snap || null;
var snap = this.options.snap || null;
var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width;
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
// move // move
this.touchParams.itemProps.forEach(function (props) { this.touchParams.itemProps.forEach(function (props) {
@ -1174,17 +1195,20 @@ ItemSet.prototype._onDrag = function (event) {
if ('start' in props) { if ('start' in props) {
var start = new Date(props.start + offset); var start = new Date(props.start + offset);
newProps.start = snap ? snap(start) : start;
newProps.start = snap ? snap(start, scale, step) : start;
} }
if ('end' in props) { if ('end' in props) {
var end = new Date(props.end + offset); var end = new Date(props.end + offset);
newProps.end = snap ? snap(end) : end;
newProps.end = snap ? snap(end, scale, step) : end;
}
else if ('duration' in props) {
newProps.end = new Date(newProps.start.valueOf() + props.duration);
} }
if ('group' in props) { if ('group' in props) {
// drag from one group to another // drag from one group to another
var group = ItemSet.groupFromTarget(event);
var group = me.groupFromTarget(event);
newProps.group = group && group.groupId; newProps.group = group && group.groupId;
} }
@ -1212,8 +1236,15 @@ ItemSet.prototype._onDrag = function (event) {
*/ */
ItemSet.prototype._updateItemProps = function(item, props) { ItemSet.prototype._updateItemProps = function(item, props) {
// TODO: copy all properties from props to item? (also new ones) // TODO: copy all properties from props to item? (also new ones)
if ('start' in props) item.data.start = props.start;
if ('end' in props) item.data.end = props.end;
if ('start' in props) {
item.data.start = props.start;
}
if ('end' in props) {
item.data.end = props.end;
}
else if ('duration' in props) {
item.data.end = new Date(props.start.valueOf() + props.duration);
}
if ('group' in props && item.data.group != props.group) { if ('group' in props && item.data.group != props.group) {
this._moveToGroup(item, props.group) this._moveToGroup(item, props.group)
} }
@ -1342,7 +1373,7 @@ ItemSet.prototype._onAddItem = function (event) {
if (!this.options.editable.add) return; if (!this.options.editable.add) return;
var me = this, var me = this,
snap = this.body.util.snap || null,
snap = this.options.snap || null,
item = ItemSet.itemFromTarget(event); item = ItemSet.itemFromTarget(event);
if (item) { if (item) {
@ -1361,20 +1392,23 @@ ItemSet.prototype._onAddItem = function (event) {
var xAbs = util.getAbsoluteLeft(this.dom.frame); var xAbs = util.getAbsoluteLeft(this.dom.frame);
var x = event.center.x - xAbs; var x = event.center.x - xAbs;
var start = this.body.util.toTime(x); var start = this.body.util.toTime(x);
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
var newItem = { var newItem = {
start: snap ? snap(start) : start,
start: snap ? snap(start, scale, step) : start,
content: 'new item' content: 'new item'
}; };
// when default type is a range, add a default end date to the new item // when default type is a range, add a default end date to the new item
if (this.options.type === 'range') { if (this.options.type === 'range') {
var end = this.body.util.toTime(x + this.props.width / 5); var end = this.body.util.toTime(x + this.props.width / 5);
newItem.end = snap ? snap(end) : end;
newItem.end = snap ? snap(end, scale, step) : end;
} }
newItem[this.itemsData._fieldId] = util.randomUUID(); newItem[this.itemsData._fieldId] = util.randomUUID();
var group = ItemSet.groupFromTarget(event);
var group = this.groupFromTarget(event);
if (group) { if (group) {
newItem.group = group.groupId; newItem.group = group.groupId;
} }
@ -1504,13 +1538,37 @@ ItemSet.itemFromTarget = function(event) {
* @param {Event} event * @param {Event} event
* @return {Group | null} group * @return {Group | null} group
*/ */
ItemSet.groupFromTarget = function(event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-group')) {
return target['timeline-group'];
ItemSet.prototype.groupFromTarget = function(event) {
// TODO: cleanup when the new solution is stable (also on mobile)
//var target = event.target;
//while (target) {
// if (target.hasOwnProperty('timeline-group')) {
// return target['timeline-group'];
// }
// target = target.parentNode;
//}
//
var clientY = event.gesture.center.clientY;
for (var i = 0; i < this.groupIds.length; i++) {
var groupId = this.groupIds[i];
var group = this.groups[groupId];
var foreground = group.dom.foreground;
var top = util.getAbsoluteTop(foreground);
if (clientY > top && clientY < top + foreground.offsetHeight) {
return group;
}
if (this.options.orientation === 'top') {
if (i === this.groupIds.length - 1 && clientY > top) {
return group;
}
}
else {
if (i === 0 && clientY < top + foreground.offset) {
return group;
}
} }
target = target.parentNode;
} }
return null; return null;

+ 9
- 1
lib/timeline/component/LineGraph.js View File

@ -981,9 +981,17 @@ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
} }
for (var i = 0; i < datapoints.length; i++) { for (var i = 0; i < datapoints.length; i++) {
var labelValue;
//if (datapoints[i].label) {
// labelValue = datapoints[i].label;
//}
//else {
// labelValue = null;
//}
labelValue = datapoints[i].label ? datapoints[i].label : null;
xValue = toScreen(datapoints[i].x) + this.props.width; xValue = toScreen(datapoints[i].x) + this.props.width;
yValue = Math.round(axis.convertValue(datapoints[i].y)); yValue = Math.round(axis.convertValue(datapoints[i].y));
extractedData.push({x: xValue, y: yValue});
extractedData.push({x: xValue, y: yValue, label:labelValue});
} }
group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));

+ 7
- 12
lib/timeline/component/TimeAxis.js View File

@ -38,7 +38,8 @@ function TimeAxis (body, options) {
// TODO: implement timeaxis orientations 'left' and 'right' // TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true, showMinorLabels: true,
showMajorLabels: true, showMajorLabels: true,
format: null
format: null,
timeAxis: null
}; };
this.options = util.extend({}, this.defaultOptions); this.options = util.extend({}, this.defaultOptions);
@ -68,7 +69,8 @@ TimeAxis.prototype.setOptions = function(options) {
'showMinorLabels', 'showMinorLabels',
'showMajorLabels', 'showMajorLabels',
'hiddenDates', 'hiddenDates',
'format'
'format',
'timeAxis'
], this.options, options); ], this.options, options);
// apply locale to moment.js // apply locale to moment.js
@ -190,6 +192,9 @@ TimeAxis.prototype._repaintLabels = function () {
if (this.options.format) { if (this.options.format) {
step.setFormat(this.options.format); step.setFormat(this.options.format);
} }
if (this.options.timeAxis) {
step.setScale(this.options.timeAxis);
}
this.step = step; this.step = step;
// Move all DOM elements to a "redundant" list, where they // Move all DOM elements to a "redundant" list, where they
@ -428,14 +433,4 @@ TimeAxis.prototype._calculateCharSize = function () {
this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
}; };
/**
* Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
TimeAxis.prototype.snap = function(date) {
return this.step.snap(date);
};
module.exports = TimeAxis; module.exports = TimeAxis;

+ 1
- 1
lib/timeline/component/graph2d_types/points.js View File

@ -35,7 +35,7 @@ Points.prototype.draw = function(dataset, group, framework, offset) {
Points.draw = function (dataset, group, framework, offset) { Points.draw = function (dataset, group, framework, offset) {
if (offset === undefined) {offset = 0;} if (offset === undefined) {offset = 0;}
for (var i = 0; i < dataset.length; i++) { for (var i = 0; i < dataset.length; i++) {
DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg);
DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label);
} }
}; };

+ 42
- 0
lib/util.js View File

@ -13,6 +13,26 @@ exports.isNumber = function(object) {
return (object instanceof Number || typeof object == 'number'); return (object instanceof Number || typeof object == 'number');
}; };
/**
* this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
*
* @param min
* @param max
* @param total
* @param value
* @returns {number}
*/
exports.giveRange = function(min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
}
/** /**
* Test whether given object is a string * Test whether given object is a string
* @param {*} object * @param {*} object
@ -757,6 +777,28 @@ exports.hexToRGB = function(hex) {
} : null; } : null;
}; };
/**
* This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string.
* @param color
* @param opacity
* @returns {*}
*/
exports.overrideOpacity = function(color,opacity) {
if (color.indexOf("rgb") != -1) {
var rgb = color.substr(color.indexOf("(")+1).replace(")","").split(",");
return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")"
}
else {
var rgb = exports.hexToRGB(color);
if (rgb == null) {
return color;
}
else {
return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")"
}
}
}
/** /**
* *
* @param red 0 -- 255 * @param red 0 -- 255

+ 1
- 3
misc/how_to_publish.md View File

@ -74,9 +74,7 @@ This generates the vis.js library in the folder `./dist`.
- Check if there are new or updated examples, and update the gallery screenshots - Check if there are new or updated examples, and update the gallery screenshots
accordingly. accordingly.
- Go to the `github-pages` branch and run the following script:
node updateversion.js
- Update the library version number in the index.html page.
- Commit the changes in the `gh-pages` branch. - Commit the changes in the `gh-pages` branch.

+ 9
- 9
package.json View File

@ -1,6 +1,6 @@
{ {
"name": "vis", "name": "vis",
"version": "3.9.0",
"version": "3.10.1-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.", "description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/", "homepage": "http://visjs.org/",
"repository": { "repository": {
@ -36,16 +36,16 @@
}, },
"devDependencies": { "devDependencies": {
"clean-css": "latest", "clean-css": "latest",
"gulp": "^3.8.5",
"gulp-concat": "^2.2.0",
"gulp-minify-css": "^0.3.6",
"gulp": "^3.8.11",
"gulp-concat": "^2.4.3",
"gulp-minify-css": "^0.4.5",
"gulp-rename": "^1.2.0", "gulp-rename": "^1.2.0",
"gulp-util": "^2.2.19",
"gulp-util": "^3.0.3",
"merge-stream": "^0.1.5", "merge-stream": "^0.1.5",
"mocha": "^1.20.1",
"mocha": "^2.1.0",
"rimraf": "^2.2.8", "rimraf": "^2.2.8",
"uglify-js": "^2.4.14",
"webpack": "^1.3.1-beta7",
"yargs": "^1.2.6"
"uglify-js": "^2.4.16",
"webpack": "^1.5.3",
"yargs": "^2.3.0"
} }
} }

+ 9
- 0
test/DataSet.test.js View File

@ -26,6 +26,7 @@ describe('DataSet', function () {
]); ]);
var items = data.get(); var items = data.get();
assert.equal(data.length, 4);
assert.equal(items.length, 4); assert.equal(items.length, 4);
items.forEach(function (item) { items.forEach(function (item) {
assert.ok(item.start instanceof Date); assert.ok(item.start instanceof Date);
@ -76,6 +77,7 @@ describe('DataSet', function () {
{id: 3}, {id: 3},
{id: 4} {id: 4}
]); ]);
assert.equal(data.length, 3);
// add an item // add an item
data.add({id: 5, content: 'Item 5', start: now.valueOf()}); data.add({id: 5, content: 'Item 5', start: now.valueOf()});
@ -87,12 +89,17 @@ describe('DataSet', function () {
{id: 4}, {id: 4},
{id: 5} {id: 5}
]); ]);
assert.equal(data.length, 4);
// update an item // update an item
data.update({id: 5, content: 'changed!'}); // update item (extend existing fields) data.update({id: 5, content: 'changed!'}); // update item (extend existing fields)
assert.equal(data.length, 4);
data.remove(3); // remove existing item data.remove(3); // remove existing item
assert.equal(data.length, 3);
data.add({id: 3, other: 'bla'}); // add new item data.add({id: 3, other: 'bla'}); // add new item
assert.equal(data.length, 4);
data.update({id: 6, content: 'created!', start: now.valueOf()}); // this item is not yet existing, create it data.update({id: 6, content: 'created!', start: now.valueOf()}); // this item is not yet existing, create it
assert.equal(data.length, 5);
assert.deepEqual(data.get().sort(sort), [ assert.deepEqual(data.get().sort(sort), [
{id: 1, content: 'Item 1', start: now}, {id: 1, content: 'Item 1', start: now},
{id: 3, other: 'bla'}, {id: 3, other: 'bla'},
@ -100,8 +107,10 @@ describe('DataSet', function () {
{id: 5, content: 'changed!', start: now}, {id: 5, content: 'changed!', start: now},
{id: 6, content: 'created!', start: now} {id: 6, content: 'created!', start: now}
]); ]);
assert.equal(data.length, 5);
data.clear(); data.clear();
assert.equal(data.length, 0);
assert.equal(data.get().length, 0); assert.equal(data.get().length, 0);

+ 81
- 0
test/DataView.test.js View File

@ -28,6 +28,7 @@ describe('DataView', function () {
{id: 2, content: 'Item 2', group: 2}, {id: 2, content: 'Item 2', group: 2},
{id: 3, content: 'Item 3', group: 2} {id: 3, content: 'Item 3', group: 2}
]); ]);
assert.equal(group2.length, 2);
// test filtering the view contents // test filtering the view contents
assert.deepEqual(group2.get({ assert.deepEqual(group2.get({
@ -51,19 +52,99 @@ describe('DataView', function () {
groups.update({id:2, content: 'Item 2 (changed)'}); groups.update({id:2, content: 'Item 2 (changed)'});
assert.equal(groupsTriggerCount, 1); assert.equal(groupsTriggerCount, 1);
assert.equal(group2TriggerCount, 1); assert.equal(group2TriggerCount, 1);
assert.equal(group2.length, 2);
groups.update({id:5, content: 'Item 5 (changed)'}); groups.update({id:5, content: 'Item 5 (changed)'});
assert.equal(groupsTriggerCount, 2); assert.equal(groupsTriggerCount, 2);
assert.equal(group2TriggerCount, 1); assert.equal(group2TriggerCount, 1);
assert.equal(group2.length, 2);
// detach the view from groups // detach the view from groups
group2.setData(null); group2.setData(null);
assert.equal(groupsTriggerCount, 2); assert.equal(groupsTriggerCount, 2);
assert.equal(group2TriggerCount, 2); assert.equal(group2TriggerCount, 2);
assert.equal(group2.length, 0);
groups.update({id:2, content: 'Item 2 (changed again)'}); groups.update({id:2, content: 'Item 2 (changed again)'});
assert.equal(groupsTriggerCount, 3); assert.equal(groupsTriggerCount, 3);
assert.equal(group2TriggerCount, 2); assert.equal(group2TriggerCount, 2);
// test updating of .length property
group2.setData(groups);
assert.equal(group2.length, 2);
// add a new item
groups.add({id: 6, content: 'Item 6', group: 2});
assert.equal(group2.length, 3);
// change an items group to 2
groups.update({id: 4, group: 2});
assert.equal(group2.length, 4);
// change an items group to 1
groups.update({id: 4, group: 1});
assert.equal(group2.length, 3);
// remove an item
groups.remove(2);
assert.equal(group2.length, 2);
// remove all items
groups.clear();
assert.equal(group2.length, 0);
}); });
it('should refresh a DataView with filter', function () {
var data = new DataSet([
{id:1, value:2},
{id:2, value:4},
{id:3, value:7}
]);
var threshold = 5;
// create a view. The view has a filter with a dynamic property `threshold`
var view = new DataView(data, {
filter: function (item) {
return item.value < threshold;
}
});
var added, updated, removed;
view.on('add', function (event, props) {added = added.concat(props.items)});
view.on('update', function (event, props) {updated = updated.concat(props.items)});
view.on('remove', function (event, props) {removed = removed.concat(props.items)});
assert.deepEqual(view.get(), [
{id:1, value:2},
{id:2, value:4}
]);
// change the threshold to 3
added = [];
updated = [];
removed = [];
threshold = 3;
view.refresh();
assert.deepEqual(view.get(), [{id:1, value:2}]);
assert.deepEqual(added, []);
assert.deepEqual(updated, []);
assert.deepEqual(removed, [2]);
// change threshold to 8
added = [];
updated = [];
removed = [];
threshold = 8;
view.refresh();
assert.deepEqual(view.get(), [
{id:1, value:2},
{id:2, value:4},
{id:3, value:7}
]);
assert.deepEqual(added, [2, 3]);
assert.deepEqual(updated, []);
assert.deepEqual(removed, []);
})
}); });

+ 5
- 0
test/timeline.html View File

@ -125,6 +125,11 @@
'hour': 'dddd D MMMM' 'hour': 'dddd D MMMM'
} }
}, },
}
// timeAxis: {
// scale: 'hour',
// step: 2
// }
//clickToUse: true, //clickToUse: true,
//min: moment('2013-01-01'), //min: moment('2013-01-01'),
//max: moment('2013-12-31'), //max: moment('2013-12-31'),

+ 6
- 0
test/timeline_groups.html View File

@ -173,6 +173,12 @@
groups.on('update', console.log.bind(console)); groups.on('update', console.log.bind(console));
groups.on('remove', console.log.bind(console)); groups.on('remove', console.log.bind(console));
function log (msg) {
var logs = document.getElementById('logs');
logs.innerHTML = msg + '<br>' + logs.innerHTML;
}
</script> </script>
<div id="logs"></div>
</body> </body>
</html> </html>

Loading…
Cancel
Save