Browse Source

Merge branch 'develop' into v4

Conflicts:
	HISTORY.md
	bower.json
	dist/vis.js
	dist/vis.min.css
	dist/vis.min.js
	package.json
flowchartTest
jos 9 years ago
parent
commit
261ea430ad
32 changed files with 760 additions and 330 deletions
  1. +25
    -0
      HISTORY.md
  2. +13
    -0
      dist/vis.css
  3. +1
    -1
      dist/vis.map
  4. +102
    -4
      docs/network.html
  5. +14
    -1
      docs/timeline.html
  6. +1
    -0
      examples/network/25_physics_configuration.html
  7. +10
    -27
      examples/network/29_neighbourhood_highlight.html
  8. +54
    -0
      examples/timeline/33_custom_snapping.html
  9. +1
    -0
      examples/timeline/index.html
  10. +2
    -1
      gulpfile.js
  11. +29
    -19
      lib/network/Edge.js
  12. +178
    -39
      lib/network/Network.js
  13. +42
    -23
      lib/network/Node.js
  14. +7
    -15
      lib/network/Popup.js
  15. +13
    -0
      lib/network/css/network-tooltip.css
  16. +61
    -91
      lib/network/mixins/ClusterMixin.js
  17. +1
    -1
      lib/network/mixins/HierarchicalLayoutMixin.js
  18. +5
    -5
      lib/network/mixins/ManipulationMixin.js
  19. +6
    -3
      lib/network/mixins/physics/PhysicsMixin.js
  20. +20
    -9
      lib/timeline/Core.js
  21. +0
    -12
      lib/timeline/DataStep.js
  22. +2
    -3
      lib/timeline/Graph2d.js
  23. +5
    -4
      lib/timeline/Range.js
  24. +22
    -18
      lib/timeline/TimeStep.js
  25. +18
    -3
      lib/timeline/Timeline.js
  26. +0
    -10
      lib/timeline/component/DataAxis.js
  27. +71
    -20
      lib/timeline/component/ItemSet.js
  28. +0
    -10
      lib/timeline/component/TimeAxis.js
  29. +42
    -0
      lib/util.js
  30. +1
    -3
      misc/how_to_publish.md
  31. +8
    -8
      package.json
  32. +6
    -0
      test/timeline_groups.html

+ 25
- 0
HISTORY.md View File

@ -22,11 +22,36 @@ http://visjs.org
## not yet released, version 3.9.2-SNAPSHOT ## not yet released, version 3.9.2-SNAPSHOT
## 2015-02-11, version 3.10.0
### Network ### Network
- Added option bindToWindow (default true) to choose whether the keyboard binds are global or to the network div. - 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. - 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 ### DataSet/DataView

+ 13
- 0
dist/vis.css View File

@ -789,4 +789,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);
} }

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


+ 102
- 4
docs/network.html View File

@ -849,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>
@ -861,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>
@ -927,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>
@ -1197,7 +1261,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>
@ -1727,7 +1796,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:
@ -1870,6 +1940,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>
@ -2209,11 +2285,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>
@ -2240,6 +2323,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>
@ -2329,6 +2419,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>
@ -2348,7 +2444,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>
@ -2370,6 +2466,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,
@ -2378,6 +2475,7 @@ var options = {
</td> </td>
</tr> </tr>
</table> </table>
<h2 id="Events">Events</h2> <h2 id="Events">Events</h2>

+ 14
- 1
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>
@ -941,7 +952,9 @@ 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> </td>
</tr> </tr>

+ 1
- 0
examples/network/25_physics_configuration.html View File

@ -78,6 +78,7 @@
}; };
var options = { var options = {
edges:{opacity:0.2},
stabilize: false, stabilize: false,
configurePhysics:true configurePhysics:true
}; };

+ 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>

+ 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>

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

@ -43,6 +43,7 @@
<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="requirejs/requirejs_example.html">requirejs_example.html</a></p> <p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>

+ 2
- 1
gulpfile.js View File

@ -100,7 +100,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)

+ 29
- 19
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'
]; ];
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;
} }
}; };
@ -230,19 +236,23 @@ Edge.prototype.isOverlappingWith = function(obj) {
Edge.prototype._getColor = function() { Edge.prototype._getColor = function() {
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
};
}
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;}

+ 178
- 39
lib/network/Network.js View File

@ -53,9 +53,19 @@ function Network (container, data, options) {
this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
var customScalingFunction = function (min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
};
// set constant values // set constant values
this.defaultOptions = { this.defaultOptions = {
nodes: { nodes: {
customScalingFunction: customScalingFunction,
mass: 1, mass: 1,
radiusMin: 10, radiusMin: 10,
radiusMax: 30, radiusMax: 30,
@ -69,7 +79,12 @@ function Network (container, data, options) {
fontFace: 'verdana', fontFace: 'verdana',
fontFill: undefined, fontFill: undefined,
fontStrokeWidth: 0, // px fontStrokeWidth: 0, // px
fontStrokeColor: 'white',
fontStrokeColor: '#ffffff',
fontDrawThreshold: 3,
scaleFontWithValue: false,
fontSizeMin: 14,
fontSizeMax: 30,
fontSizeMaxVisible: 30,
level: -1, level: -1,
color: { color: {
border: '#2B7CE9', border: '#2B7CE9',
@ -88,6 +103,7 @@ function Network (container, data, options) {
borderWidthSelected: undefined borderWidthSelected: undefined
}, },
edges: { edges: {
customScalingFunction: customScalingFunction,
widthMin: 1, // widthMin: 1, //
widthMax: 15,// widthMax: 15,//
width: 1, width: 1,
@ -99,6 +115,7 @@ function Network (container, data, options) {
highlight:'#848484', highlight:'#848484',
hover: '#848484' hover: '#848484'
}, },
opacity:1.0,
fontColor: '#343434', fontColor: '#343434',
fontSize: 14, // px fontSize: 14, // px
fontFace: 'arial', fontFace: 'arial',
@ -163,8 +180,8 @@ function Network (container, data, options) {
height: 1, // (px PNiC) | growth of the height per node in cluster. height: 1, // (px PNiC) | growth of the height per node in cluster.
radius: 1}, // (px PNiC) | growth of the radius per node in cluster. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
clusterLevelDifference: 2,
activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
clusterLevelDifference: 2, // used for normalization of the cluster levels
clusterByZoom: true // enable clustering through zooming in and out clusterByZoom: true // enable clustering through zooming in and out
}, },
navigation: { navigation: {
@ -193,7 +210,7 @@ function Network (container, data, options) {
type: "continuous", type: "continuous",
roundness: 0.5 roundness: 0.5
}, },
maxVelocity: 30,
maxVelocity: 50,
minVelocity: 0.1, // px/s minVelocity: 0.1, // px/s
stabilize: true, // stabilize before displaying the network stabilize: true, // stabilize before displaying the network
stabilizationIterations: 1000, // maximum number of iteration to stabilize stabilizationIterations: 1000, // maximum number of iteration to stabilize
@ -275,7 +292,7 @@ function Network (container, data, options) {
this.setOptions(options); this.setOptions(options);
// other vars // other vars
this.freezeSimulation = false;// freeze the simulation
this.freezeSimulationEnabled = false;// freeze the simulation
this.cachedFunctions = {}; this.cachedFunctions = {};
this.startedStabilization = false; this.startedStabilization = false;
this.stabilized = false; this.stabilized = false;
@ -346,7 +363,7 @@ function Network (container, data, options) {
else { else {
// zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
if (this.constants.stabilize == false) { if (this.constants.stabilize == false) {
this.zoomExtent(undefined, true,this.constants.clustering.enabled);
this.zoomExtent({duration:0}, true, this.constants.clustering.enabled);
} }
} }
@ -406,17 +423,45 @@ Network.prototype._getScriptPath = function() {
* Find the center position of the network * Find the center position of the network
* @private * @private
*/ */
Network.prototype._getRange = function() {
Network.prototype._getRange = function(specificNodes) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (minX > (node.boundingBox.left)) {minX = node.boundingBox.left;}
if (maxX < (node.boundingBox.right)) {maxX = node.boundingBox.right;}
if (minY > (node.boundingBox.bottom)) {minY = node.boundingBox.top;} // top is negative, bottom is positive
if (maxY < (node.boundingBox.top)) {maxY = node.boundingBox.bottom;} // top is negative, bottom is positive
if (specificNodes.length > 0) {
for (var i = 0; i < specificNodes.length; i++) {
node = this.nodes[specificNodes[i]];
if (minX > (node.boundingBox.left)) {
minX = node.boundingBox.left;
}
if (maxX < (node.boundingBox.right)) {
maxX = node.boundingBox.right;
}
if (minY > (node.boundingBox.bottom)) {
minY = node.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.boundingBox.top)) {
maxY = node.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
else {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (minX > (node.boundingBox.left)) {
minX = node.boundingBox.left;
}
if (maxX < (node.boundingBox.right)) {
maxX = node.boundingBox.right;
}
if (minY > (node.boundingBox.bottom)) {
minY = node.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.boundingBox.top)) {
maxY = node.boundingBox.bottom;
} // top is negative, bottom is positive
}
} }
} }
if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
minY = 0, maxY = 0, minX = 0, maxX = 0; minY = 0, maxY = 0, minX = 0, maxX = 0;
} }
@ -441,17 +486,37 @@ Network.prototype._findCenter = function(range) {
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
* @param {Boolean} [disableStart] | If true, start is not called. * @param {Boolean} [disableStart] | If true, start is not called.
*/ */
Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableStart) {
Network.prototype.zoomExtent = function(options, initialZoom, disableStart) {
this._redraw(true); this._redraw(true);
if (initialZoom === undefined) {initialZoom = false;} if (initialZoom === undefined) {initialZoom = false;}
if (disableStart === undefined) {disableStart = false;} if (disableStart === undefined) {disableStart = false;}
if (animationOptions === undefined) {animationOptions = false;}
if (options === undefined) {options = {nodes:[]};}
if (options.nodes === undefined) {
options.nodes = [];
}
var range = this._getRange();
var range;
var zoomLevel; var zoomLevel;
if (initialZoom == true) { if (initialZoom == true) {
// check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
var positionDefined = 0;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.predefinedPosition == true) {
positionDefined += 1;
}
}
}
if (positionDefined > 0.5 * this.nodeIndices.length) {
this.zoomExtent(options,false,disableStart);
return;
}
range = this._getRange(options.nodes);
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
if (this.constants.smoothCurves == true) { if (this.constants.smoothCurves == true) {
if (this.constants.clustering.enabled == true && if (this.constants.clustering.enabled == true &&
@ -477,6 +542,7 @@ Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableSt
zoomLevel *= factor; zoomLevel *= factor;
} }
else { else {
range = this._getRange(options.nodes);
var xDistance = Math.abs(range.maxX - range.minX) * 1.1; var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
var yDistance = Math.abs(range.maxY - range.minY) * 1.1; var yDistance = Math.abs(range.maxY - range.minY) * 1.1;
@ -492,7 +558,7 @@ Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableSt
var center = this._findCenter(range); var center = this._findCenter(range);
if (disableStart == false) { if (disableStart == false) {
var options = {position: center, scale: zoomLevel, animation: animationOptions};
var options = {position: center, scale: zoomLevel, animation: options};
this.moveTo(options); this.moveTo(options);
this.moving = true; this.moving = true;
this.start(); this.start();
@ -537,6 +603,10 @@ Network.prototype.setData = function(data, disableStart) {
if (disableStart === undefined) { if (disableStart === undefined) {
disableStart = false; disableStart = false;
} }
// unselect all to ensure no selections from old data are carried over.
this._unselectAll(true);
// we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added. // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
this.initializing = true; this.initializing = true;
@ -581,7 +651,7 @@ Network.prototype.setData = function(data, disableStart) {
} }
else { else {
// find a stable position or start animating to a stable position // find a stable position or start animating to a stable position
if (this.constants.stabilize) {
if (this.constants.stabilize == true) {
this._stabilize(); this._stabilize();
} }
} }
@ -732,7 +802,7 @@ Network.prototype.setOptions = function (options) {
// bind keys. If disabled, this will not do anything; // bind keys. If disabled, this will not do anything;
this._createKeyBinds(); this._createKeyBinds();
this._markAllEdgesAsDirty();
this.setSize(this.constants.width, this.constants.height); this.setSize(this.constants.width, this.constants.height);
this.moving = true; this.moving = true;
this.start(); this.start();
@ -783,6 +853,7 @@ Network.prototype._create = function () {
ctx.oBackingStorePixelRatio || ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1); ctx.backingStorePixelRatio || 1);
//this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens.
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
} }
@ -874,10 +945,11 @@ Network.prototype._createKeyBinds = function() {
this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup");
} }
this.keycharm.bind("1",this.increaseClusterLevel.bind(me), "keydown");
this.keycharm.bind("2",this.decreaseClusterLevel.bind(me), "keydown");
this.keycharm.bind("3",this.forceAggregateHubs.bind(me,true),"keydown");
this.keycharm.bind("4",this.normalizeClusterLevels.bind(me), "keydown");
//this.keycharm.bind("1",this.increaseClusterLevel.bind(me), "keydown");
//this.keycharm.bind("2",this.decreaseClusterLevel.bind(me), "keydown");
//this.keycharm.bind("3",this.forceAggregateHubs.bind(me,true),"keydown");
//this.keycharm.bind("4",this.normalizeClusterLevels.bind(me), "keydown");
if (this.constants.dataManipulation.enabled == true) { if (this.constants.dataManipulation.enabled == true) {
this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); this.keycharm.bind("esc",this._createManipulatorBar.bind(me));
this.keycharm.bind("delete",this._deleteSelected.bind(me)); this.keycharm.bind("delete",this._deleteSelected.bind(me));
@ -1599,8 +1671,16 @@ Network.prototype._updateNodes = function(ids,changedData) {
} }
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateValueRange(nodes); this._updateValueRange(nodes);
this._markAllEdgesAsDirty();
}; };
Network.prototype._markAllEdgesAsDirty = function() {
for (var edgeId in this.edges) {
this.edges[edgeId].colorDirty = true;
}
}
/** /**
* Remove existing nodes. If nodes do not exist, the method will just ignore it. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
* @param {Number[] | String[]} ids * @param {Number[] | String[]} ids
@ -1608,10 +1688,22 @@ Network.prototype._updateNodes = function(ids,changedData) {
*/ */
Network.prototype._removeNodes = function(ids) { Network.prototype._removeNodes = function(ids) {
var nodes = this.nodes; var nodes = this.nodes;
// remove from selection
for (var i = 0, len = ids.length; i < len; i++) {
if (this.selectionObj.nodes[ids[i]] !== undefined) {
this.nodes[ids[i]].unselect();
this._removeFromSelection(this.nodes[ids[i]]);
}
}
for (var i = 0, len = ids.length; i < len; i++) { for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i]; var id = ids[i];
delete nodes[id]; delete nodes[id];
} }
this._updateNodeIndexList(); this._updateNodeIndexList();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels(); this._resetLevels();
@ -1743,6 +1835,15 @@ Network.prototype._updateEdges = function (ids) {
*/ */
Network.prototype._removeEdges = function (ids) { Network.prototype._removeEdges = function (ids) {
var edges = this.edges; var edges = this.edges;
// remove from selection
for (var i = 0, len = ids.length; i < len; i++) {
if (this.selectionObj.edges[ids[i]] !== undefined) {
edges[ids[i]].unselect();
this._removeFromSelection(edges[ids[i]]);
}
}
for (var i = 0, len = ids.length; i < len; i++) { for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i]; var id = ids[i];
var edge = edges[id]; var edge = edges[id];
@ -1803,12 +1904,14 @@ Network.prototype._updateValueRange = function(obj) {
// determine the range of the objects // determine the range of the objects
var valueMin = undefined; var valueMin = undefined;
var valueMax = undefined; var valueMax = undefined;
var valueTotal = 0;
for (id in obj) { for (id in obj) {
if (obj.hasOwnProperty(id)) { if (obj.hasOwnProperty(id)) {
var value = obj[id].getValue(); var value = obj[id].getValue();
if (value !== undefined) { if (value !== undefined) {
valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
valueTotal += value;
} }
} }
} }
@ -1817,7 +1920,7 @@ Network.prototype._updateValueRange = function(obj) {
if (valueMin !== undefined && valueMax !== undefined) { if (valueMin !== undefined && valueMax !== undefined) {
for (id in obj) { for (id in obj) {
if (obj.hasOwnProperty(id)) { if (obj.hasOwnProperty(id)) {
obj[id].setValueRange(valueMin, valueMax);
obj[id].setValueRange(valueMin, valueMax, valueTotal);
} }
} }
} }
@ -1843,8 +1946,8 @@ Network.prototype._redraw = function(hidden) {
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
// clear the canvas // clear the canvas
var w = this.frame.canvas.width * this.pixelRatio;
var h = this.frame.canvas.height * this.pixelRatio;
var w = this.frame.canvas.clientWidth;
var h = this.frame.canvas.clientHeight;
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
// set scaling and translation // set scaling and translation
@ -1857,8 +1960,8 @@ Network.prototype._redraw = function(hidden) {
"y": this._YconvertDOMtoCanvas(0) "y": this._YconvertDOMtoCanvas(0)
}; };
this.canvasBottomRight = { this.canvasBottomRight = {
"x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth * this.pixelRatio),
"y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight * this.pixelRatio)
"x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
"y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
}; };
if (!(hidden == true)) { if (!(hidden == true)) {
@ -2093,16 +2196,23 @@ Network.prototype._stabilize = function() {
var count = 0; var count = 0;
while (this.moving && count < this.constants.stabilizationIterations) { while (this.moving && count < this.constants.stabilizationIterations) {
this._physicsTick(); this._physicsTick();
// TODO: cleanup
//if (count % 100 == 0) {
// console.log("stabilizationIterations",count);
//}
count++; count++;
} }
if (this.constants.zoomExtentOnStabilize == true) { if (this.constants.zoomExtentOnStabilize == true) {
this.zoomExtent(undefined, false, true);
this.zoomExtent({duration:0}, false, true);
} }
if (this.constants.freezeForStabilization == true) { if (this.constants.freezeForStabilization == true) {
this._restoreFrozenNodes(); this._restoreFrozenNodes();
} }
this.emit("stabilizationIterationsDone");
}; };
/** /**
@ -2152,8 +2262,10 @@ Network.prototype._restoreFrozenNodes = function() {
Network.prototype._isMoving = function(vmin) { Network.prototype._isMoving = function(vmin) {
var nodes = this.nodes; var nodes = this.nodes;
for (var id in nodes) { for (var id in nodes) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
return true;
if (nodes[id] !== undefined) {
if (nodes[id].isMoving(vmin) == true) {
return true;
}
} }
} }
return false; return false;
@ -2224,7 +2336,7 @@ Network.prototype._revertPhysicsTick = function() {
* @private * @private
*/ */
Network.prototype._physicsTick = function() { Network.prototype._physicsTick = function() {
if (!this.freezeSimulation) {
if (!this.freezeSimulationEnabled) {
if (this.moving == true) { if (this.moving == true) {
var mainMovingStatus = false; var mainMovingStatus = false;
var supportMovingStatus = false; var supportMovingStatus = false;
@ -2236,11 +2348,12 @@ Network.prototype._physicsTick = function() {
} }
// gather movement data from all sectors, if one moves, we are NOT stabilzied // gather movement data from all sectors, if one moves, we are NOT stabilzied
for (var i = 0; i < mainMoving.length; i++) {mainMovingStatus = mainMoving[0] || mainMovingStatus;}
for (var i = 0; i < mainMoving.length; i++) {
mainMovingStatus = mainMoving[i] || mainMovingStatus;
}
// determine if the network has stabilzied // determine if the network has stabilzied
this.moving = mainMovingStatus || supportMovingStatus; this.moving = mainMovingStatus || supportMovingStatus;
if (this.moving == false) { if (this.moving == false) {
this._revertPhysicsTick(); this._revertPhysicsTick();
} }
@ -2363,12 +2476,14 @@ Network.prototype._handleNavigation = function() {
/** /**
* Freeze the _animationStep * Freeze the _animationStep
*/ */
Network.prototype.toggleFreeze = function() {
if (this.freezeSimulation == false) {
this.freezeSimulation = true;
Network.prototype.freezeSimulation = function(freeze) {
if (freeze == true) {
this.freezeSimulationEnabled = true;
this.moving = false;
} }
else { else {
this.freezeSimulation = false;
this.freezeSimulationEnabled = false;
this.moving = true;
this.start(); this.start();
} }
}; };
@ -2741,4 +2856,28 @@ Network.prototype.getBoundingBox = function(nodeId) {
} }
} }
Network.prototype.getConnectedNodes = function(nodeId) {
var nodeList = [];
if (this.nodes[nodeId] !== undefined) {
var node = this.nodes[nodeId];
var nodeObj = {nodeId : true}; // used to quickly check if node already exists
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
if (edge.toId == nodeId) {
if (nodeObj[edge.fromId] === undefined) {
nodeList.push(edge.fromId);
nodeObj[edge.fromId] = true;
}
}
else if (edge.fromId == nodeId) {
if (nodeObj[edge.toId] === undefined) {
nodeList.push(edge.toId)
nodeObj[edge.toId] = true;
}
}
}
}
return nodeList;
}
module.exports = Network; module.exports = Network;

+ 42
- 23
lib/network/Node.js View File

@ -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,7 +74,6 @@ 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;
@ -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'
]; ];
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;}
@ -482,16 +479,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;
}; };
@ -1015,12 +1013,29 @@ Node.prototype._drawText = function (ctx) {
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 +1047,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 +1064,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 +1085,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++) {

+ 7
- 15
lib/network/Popup.js View File

@ -49,21 +49,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);
} }

+ 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);
}

+ 61
- 91
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();
@ -32,24 +32,22 @@ exports.startWithClustering = function() {
exports.clusterToFit = function(maxNumberOfNodes, reposition) { exports.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
var maxLevels = 2;
var maxLevels = 50;
var level = 0; var level = 0;
// 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) {
console.log("Performing clustering:", level, numberOfNodes, this.clusterSession);
if (level % 3 == 0.0) { if (level % 3 == 0.0) {
//this.forceAggregateHubs(true);
//this.normalizeClusterLevels();
this.forceAggregateHubs(true);
this.normalizeClusterLevels();
} }
else { else {
//this.increaseClusterLevel(); // this also includes a cluster normalization
this.increaseClusterLevel(); // this also includes a cluster normalization
} }
//this.forceAggregateHubs(true);
this.forceAggregateHubs(true);
numberOfNodes = this.nodeIndices.length; numberOfNodes = this.nodeIndices.length;
level += 1; level += 1;
} }
console.log("finished")
// after the clustering we reposition the nodes to reduce the initial chaos // after the clustering we reposition the nodes to reduce the initial chaos
if (level > 0 && reposition == true) { if (level > 0 && reposition == true) {
@ -59,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.
@ -82,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();
} }
@ -142,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
@ -161,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
@ -237,10 +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._updateCalculationNodes();
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;
@ -260,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);
}
} }
} }
} }
@ -302,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
@ -373,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());
@ -441,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();
@ -455,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
@ -479,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);
} }
} }
@ -505,8 +508,7 @@ 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
@ -588,14 +590,13 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
if (absorptionSizeOffset === undefined) { if (absorptionSizeOffset === undefined) {
absorptionSizeOffset = 0; absorptionSizeOffset = 0;
} }
if (hubNode.dynamicEdgesLength < 0) {
console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual)
}
//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;
@ -651,8 +652,13 @@ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSize
if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
(childNode.id != hubNode.id)) { (childNode.id != hubNode.id)) {
this._addToCluster(hubNode,childNode,force); this._addToCluster(hubNode,childNode,force);
}
else {
//console.log("WILL NOT MERGE:",childNode.dynamicEdges.length , (this.hubThreshold + absorptionSizeOffset))
} }
} }
} }
} }
}; };
@ -730,40 +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;
}
}
}
}
if (node.dynamicEdgesLength < correction) {
console.error("overshoot", node.dynamicEdgesLength, correction)
}
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
* *
@ -774,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
@ -813,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;
@ -989,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);
// } // }
// } // }
@ -1029,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;
@ -1090,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;
@ -1128,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;
@ -1149,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();
} }

+ 5
- 5
lib/network/mixins/ManipulationMixin.js View File

@ -15,7 +15,7 @@ exports._clearManipulatorBar = 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.freezeSimulationEnabled = false;
}; };
/** /**
@ -84,7 +84,7 @@ exports._createManipulatorBar = function() {
this._restoreOverloadedFunctions(); this._restoreOverloadedFunctions();
// resume calculation // resume calculation
this.freezeSimulation = false;
this.freezeSimulationEnabled = false;
// reset global variables // reset global variables
this.blockConnectingEdgeSelection = false; this.blockConnectingEdgeSelection = false;
@ -254,7 +254,7 @@ exports._createAddEdgeToolbar = function() {
// clear the toolbar // clear the toolbar
this._clearManipulatorBar(); this._clearManipulatorBar();
this._unselectAll(true); this._unselectAll(true);
this.freezeSimulation = true;
this.freezeSimulationEnabled = true;
if (this.boundFunction) { if (this.boundFunction) {
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
@ -385,7 +385,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.freezeSimulationEnabled = true;
} }
this._redraw(); this._redraw();
}; };
@ -429,7 +429,7 @@ exports._releaseControlNode = function(pointer) {
else { else {
this.edgeBeingEdited._restoreControlNodes(); this.edgeBeingEdited._restoreControlNodes();
} }
this.freezeSimulation = false;
this.freezeSimulationEnabled = false;
this._redraw(); this._redraw();
}; };

+ 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>' +

+ 20
- 9
lib/timeline/Core.js View File

@ -88,7 +88,7 @@ Core.prototype._create = function (container) {
this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
this.on('rangechange', this.redraw.bind(this));
this.on('rangechange', this._redraw.bind(this));
this.on('touch', this._onTouch.bind(this)); this.on('touch', this._onTouch.bind(this));
this.on('pinch', this._onPinch.bind(this)); this.on('pinch', this._onPinch.bind(this));
this.on('dragstart', this._onDragStart.bind(this)); this.on('dragstart', this._onDragStart.bind(this));
@ -101,13 +101,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();
} }
}); });
@ -222,7 +222,7 @@ Core.prototype.setOptions = function (options) {
} }
// redraw everything // redraw everything
this.redraw();
this._redraw();
}; };
/** /**
@ -392,6 +392,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
@ -407,12 +408,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);
} }
}; };
@ -451,10 +454,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;
@ -605,7 +616,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?');
@ -813,7 +824,7 @@ Core.prototype._onDrag = function (event) {
if (newScrollTop != oldScrollTop) { if (newScrollTop != oldScrollTop) {
this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
this.emit("verticalDrag"); this.emit("verticalDrag");
} }
}; };

+ 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

@ -157,7 +157,7 @@ Range.prototype.setRange = function(start, end, animate, byUser) {
me.animateTimer = setTimeout(next, 20); me.animateTimer = setTimeout(next, 20);
} }
} }
}
};
return next(); return next();
} }
@ -251,7 +251,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;
@ -271,8 +271,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;
@ -288,7 +289,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');

+ 22
- 18
lib/timeline/TimeStep.js View File

@ -321,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);
@ -337,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);
@ -352,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;
@ -365,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;
@ -378,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:
@ -387,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);
@ -402,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);
@ -416,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;

+ 18
- 3
lib/timeline/Timeline.js View File

@ -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

+ 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;

+ 71
- 20
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);
}, },
@ -271,7 +274,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) {
@ -324,11 +327,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();
});
}
}; };
/** /**
@ -1130,8 +1142,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;
@ -1151,12 +1170,14 @@ ItemSet.prototype._onDragStart = function (event) {
* @private * @private
*/ */
ItemSet.prototype._onDrag = function (event) { ItemSet.prototype._onDrag = function (event) {
event.preventDefault()
event.preventDefault();
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) {
@ -1167,17 +1188,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;
} }
@ -1337,7 +1361,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) {
@ -1356,20 +1380,23 @@ ItemSet.prototype._onAddItem = function (event) {
var xAbs = util.getAbsoluteLeft(this.dom.frame); var xAbs = util.getAbsoluteLeft(this.dom.frame);
var x = event.gesture.center.pageX - xAbs; var x = event.gesture.center.pageX - 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;
} }
@ -1499,13 +1526,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;

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

@ -433,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;

+ 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.

+ 8
- 8
package.json View File

@ -38,16 +38,16 @@
"6to5-loader": "^3.0.0", "6to5-loader": "^3.0.0",
"6to5ify": "^4.1.0", "6to5ify": "^4.1.0",
"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"
} }
} }

+ 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