Browse Source

Merge branch 'develop'

codeClimate v4.13.0
jos 9 years ago
parent
commit
7df01eece4
62 changed files with 3187 additions and 1794 deletions
  1. +39
    -0
      HISTORY.md
  2. +0
    -1
      bower.json
  3. +6
    -1
      dist/vis.css
  4. +1466
    -785
      dist/vis.js
  5. +1
    -1
      dist/vis.map
  6. +1
    -1
      dist/vis.min.css
  7. +22
    -21
      dist/vis.min.js
  8. +1
    -1
      docs/data/dataset.html
  9. +20
    -8
      docs/graph2d/index.html
  10. +7
    -0
      docs/network/edges.html
  11. +12
    -2
      docs/network/layout.html
  12. +3
    -3
      docs/network/nodes.html
  13. +9
    -1
      docs/timeline/index.html
  14. +2
    -0
      examples/network/datasources/largeHierarchicalDataset.js
  15. +8
    -2
      examples/network/exampleUtil.js
  16. +1
    -1
      examples/network/layout/hierarchicalLayoutMethods.html
  17. +107
    -102
      examples/network/layout/hierarchicalLayoutUserdefined.html
  18. +85
    -0
      examples/network/layout/hierarchicalLayoutWithoutPhysics.html
  19. +11
    -0
      examples/network/other/configuration.html
  20. +3
    -3
      gulpfile.js
  21. +1
    -1
      index.js
  22. +2
    -21
      lib/hammerUtil.js
  23. +0
    -1
      lib/network/Network.js
  24. +5
    -0
      lib/network/modules/EdgesHandler.js
  25. +31
    -28
      lib/network/modules/InteractionHandler.js
  26. +564
    -47
      lib/network/modules/LayoutEngine.js
  27. +1
    -1
      lib/network/modules/ManipulationSystem.js
  28. +4
    -0
      lib/network/modules/NodesHandler.js
  29. +36
    -7
      lib/network/modules/components/Edge.js
  30. +16
    -7
      lib/network/modules/components/edges/BezierEdgeDynamic.js
  31. +21
    -15
      lib/network/modules/components/edges/BezierEdgeStatic.js
  32. +14
    -12
      lib/network/modules/components/edges/CubicBezierEdge.js
  33. +7
    -4
      lib/network/modules/components/edges/StraightEdge.js
  34. +53
    -53
      lib/network/modules/components/edges/util/EdgeBase.js
  35. +8
    -5
      lib/network/modules/components/nodes/shapes/Box.js
  36. +11
    -9
      lib/network/modules/components/nodes/shapes/Database.js
  37. +1
    -1
      lib/network/modules/components/nodes/shapes/Dot.js
  38. +13
    -10
      lib/network/modules/components/nodes/shapes/Ellipse.js
  39. +12
    -11
      lib/network/modules/components/nodes/shapes/Image.js
  40. +11
    -10
      lib/network/modules/components/nodes/util/CircleImageBase.js
  41. +11
    -9
      lib/network/modules/components/nodes/util/ShapeBase.js
  42. +12
    -0
      lib/network/options.js
  43. +7
    -4
      lib/shared/ColorPicker.js
  44. +0
    -1
      lib/shared/Configurator.js
  45. +6
    -1
      lib/shared/configuration.css
  46. +29
    -13
      lib/timeline/Core.js
  47. +0
    -233
      lib/timeline/DataStep.js
  48. +5
    -4
      lib/timeline/Graph2d.js
  49. +6
    -2
      lib/timeline/Range.js
  50. +26
    -21
      lib/timeline/Timeline.js
  51. +52
    -103
      lib/timeline/component/DataAxis.js
  52. +235
    -0
      lib/timeline/component/DataScale.js
  53. +2
    -2
      lib/timeline/component/GraphGroup.js
  54. +10
    -8
      lib/timeline/component/ItemSet.js
  55. +3
    -2
      lib/timeline/component/Legend.js
  56. +104
    -135
      lib/timeline/component/LineGraph.js
  57. +3
    -15
      lib/timeline/component/graph2d_types/bar.js
  58. +2
    -0
      lib/timeline/optionsGraph2d.js
  59. +13
    -8
      lib/util.js
  60. +1
    -1
      misc/how_to_publish.md
  61. +3
    -3
      package.json
  62. +42
    -53
      test/networkTest.html

+ 39
- 0
HISTORY.md View File

@ -2,6 +2,45 @@
http://visjs.org
## 2016-02-01, version 4.13.0
### Network
- Added options to customize the hierarchical layout without the use of physics.
- Altered edges for arrows and added the arrowStrikethrough option.
- Improved the hierarchical layout algorithm by adding a condensing method to remove whitespace.
- Fixed #1556: Network throwing an error when clicking the "Edit" button
on the manipulation toolbar.
- Fixed #1334 (again): Network now ignores scroll when interaction:zoomView is false.
- Fixed #1588: destroy now unsubscribed from the dataset.
- Fixed #1584: Navigation buttons broken.
- Fixed #1596: correct clean up of manipulation dom elements.
- Fixed #1594: bug in hierarchical layout.
- Fixed #1597: Allow zero borders and addressed scaling artifacts.
- Fixed #1608: Fixed wrong variable reference
### Timeline
- Moved initial autoscale/fit method to an handler of the "changed" event.
- Fixed #1580: Invisible timeline/graph should not be drawn, as most inputs are invalid
- Fixed #1521: Prevent items from staying stuck to the left side of the viewport.
- Fixed #1592: Emit a "changed" event after each redraw.
- Fixed #1541: Timeline and Graph2d did not load synchronously anymore.
### Graph2d
- Major redesign of data axis/scales, with large focus on creating a sane slave axis setup
- Cleanup of linegraph's event handling.
- Fixed #1585: Allow bar groups to exclude from stacking
- Fixed #1580: Invisible timeline/graph should not be drawn, as most inputs are invalid
- Fixed #1177: Fix custom range of slaved right axis.
- Fixed #1592: Emit a "changed" event after each redraw.
- Fixed #1017: Fixed minWidth behavior for bars.
- Fixes #1557: Fix default axis formatting function.
- Fixed #1541: Timeline and Graph2d did not load synchronously anymore.
- Fixed a performance regression
## 2016-01-08, version 4.12.0
### Timeline

+ 0
- 1
bower.json View File

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

+ 6
- 1
dist/vis.css View File

@ -32,6 +32,11 @@ div.vis-configuration-wrapper {
width:700px;
}
div.vis-configuration-wrapper::after {
clear: both;
content: "";
display: block;
}
div.vis-configuration.vis-config-option-container{
display:block;
@ -133,7 +138,7 @@ input.vis-configuration.vis-config-rangeinput{
position:relative;
top:-5px;
width:60px;
height:13px;
/*height:13px;*/
padding:1px;
margin:0;
pointer-events:none;

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


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


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


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


+ 1
- 1
docs/data/dataset.html View File

@ -104,7 +104,7 @@
Vis.js comes with a flexible DataSet, which can be used to hold and
manipulate unstructured data and listen for changes in the data.
The DataSet is key/value based. Data items can be added, updated and
removed from the DatSet, and one can subscribe to changes in the DataSet.
removed from the DataSet, and one can subscribe to changes in the DataSet.
The data in the DataSet can be filtered and ordered, and fields (like
dates) can be converted to a specific type. Data can be normalized when
appending it to the DataSet as well.

+ 20
- 8
docs/graph2d/index.html View File

@ -434,7 +434,7 @@ var options = {
<td class="greenField indent">barChart.sideBySide</td>
<td>Boolean</td>
<td>false</td>
<td>If two datapoints of a barchart overlap, they are drawn over eachother by default. If sideBySide is set to true, they will be drawn side by side.
<td>If two datapoints of a barchart overlap, they are drawn over eachother by default. If sideBySide is set to true, they will be drawn side by side, within the same width as a single bar..
See <a href="../../examples/graph2d/10_barsSideBySide.html">example 10</a> for more information.
When using groups, see <a href="../../examples/graph2d/11_barsSideBySideGroups.html">example 11</a>.
</td>
@ -445,7 +445,12 @@ var options = {
<td>50</td>
<td>The width of the bars.</td>
</tr>
<tr parent="barChart" class="hidden">
<td class="greenField indent">barChart.minWidth</td>
<td>Number</td>
<td></td>
<td>The minimum width of the bars in pixels: by default the bars get smaller while zooming out to prevent overlap, this value is the minimum width of the bar. Default behavior (when minWidth is not set) is 10% of the bar width.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('g2dOptions','dataAxis', this);">
<td><span parent="dataAxis" class="right-caret"></span> dataAxis</td>
<td>Object</td>
@ -470,12 +475,11 @@ var options = {
<td>Function</td>
<td></td>
<td>Insert a custom function on how to format the label. The function will receive a numeric value and has to return a string. Default function is:
<pre class="code">
<pre class="prettyprint lang-js">
function (value) {
return value;
}
</pre>
which does nothing to it.</td>
return ''+value.toPrecision(3);
}</pre>
</td>
</tr>
<tr parent="dataAxis" class="hidden">
<td class="indent2">dataAxis.left.range.min</td>
@ -1331,7 +1335,14 @@ Graph2d.off('rangechanged', onChange);
<td>Fired when double clicked inside the Graph2d.
</td>
</tr>
<tr>
<td>changed</td>
<td>
Has no properties.
</td>
<td>Fired once after each graph redraw.
</td>
</tr>
<tr>
<td>rangechange</td>
<td>
@ -1355,6 +1366,7 @@ Graph2d.off('rangechanged', onChange);
<td>Fired once after the user has dragged the Graph2d window.
</td>
</tr>
<tr>
<td>timechange</td>
<td>

+ 7
- 0
docs/network/edges.html View File

@ -119,6 +119,7 @@ var options = {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',
@ -254,6 +255,12 @@ network.setOptions(options);
<td><code>Object</code></td>
<td>Exactly the same as the to object but with an arrowhead at the from node of the edge.</td>
</tr>
<tr>
<td class="indent">arrowStrikethrough</td>
<td>Boolean</td>
<td><code>true</code></td>
<td>When false, the edge stops at the arrow. This can be useful if you have thick lines and you want the arrow to end in a point. Middle arrows are not affected by this.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','color', this);">
<td><span parent="color" class="right-caret"></span> color</td>
<td>Object or String</td>

+ 12
- 2
docs/network/layout.html View File

@ -105,8 +105,12 @@ var options = {
hierarchical: {
enabled:false,
levelSeparation: 150,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
nodeSpacing: 100,
treeSpacing: 200,
blockShifting: true,
edgeMinimization: true,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
}
}
}
@ -132,6 +136,12 @@ network.setOptions(options);
<tr class='toggle collapsible' onclick="toggleTable('optionTable','hierarchical', this);"><td><span parent="repulsion" class="right-caret"></span> hierarchical</td><td>Object or Boolean</td><td><code>Object</code></td> <td>When true, the layout engine positions the nodes in a hierarchical fashion using default settings. For customization you can supply an object.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.enabled</td><td>Boolean</td><td><code>false</code></td> <td>Toggle the usage of the hierarchical layout system. If this option is not defined, it is set to true if any of the properties in this object are defined.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.levelSeparation</td><td>Number</td><td><code>150</code></td> <td>The distance between the different levels.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.nodeSpacing</td><td>Number</td><td><code>100</code></td> <td>Minimum distance between nodes on the free axis. This is only for the initial layout. If you enable physics, the node distance there will be the effective node distance.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.treeSpacing</td><td>Number</td><td><code>200</code></td> <td>Distance between different trees (independent networks). This is only for the initial layout. If you enable physics, the repulsion model will denote the distance between the trees.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.blockShifting</td><td>Boolean</td><td><code>true</code></td> <td>Method for reducing whitespace. Can be used alone or together with edge minimization. Each node will check for whitespace and will shift
it's branch along with it for as far as it can, respecting the nodeSpacing on any level. This is mainly for the initial layout. If you enable physics, they layout will be determined by the physics. This will greatly speed up the stabilization time though!</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.edgeMinimization</td><td>Boolean</td><td><code>true</code></td> <td>Method for reducing whitespace. Can be used alone or together with block shifting. Enabling block shifting will usually speed up the layout process.
Each node will try to move along its free axis to reduce the total length of it's edges. This is mainly for the initial layout. If you enable physics, they layout will be determined by the physics. This will greatly speed up the stabilization time though!</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.direction</td><td>String</td><td><code>'UD'</code></td> <td>The direction of the hierarchical layout. The available options are: <code>UD, DU, LR, RL</code>. To simplify: up-down, down-up, left-right, right-left.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.sortMethod</td><td>String</td><td><code>'hubsize'</code></td> <td>The algorithm used to ascertain the levels of the nodes based on the data. The possible options are: <code>hubsize, directed</code>. <br><br>
Hubsize takes the nodes with the most edges and puts them at the top. From that the rest of the hierarchy is evaluated. <br><br>

+ 3
- 3
docs/network/nodes.html View File

@ -108,7 +108,7 @@
var options = {
nodes:{
borderWidth: 1,
borderWidthSelected: undefined,
borderWidthSelected: 2,
brokenImage:undefined,
color: {
border: '#2B7CE9',
@ -227,8 +227,8 @@ network.setOptions(options);
<tr>
<td>borderWidthSelected</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>The width of the border of the node when it is selected. When undefined, the borderWidth is used</td>
<td><code>2</code></td>
<td>The width of the border of the node when it is selected. When undefined, the borderWidth * 2 is used.</td>
</tr>
<tr>
<td>brokenImage</td>

+ 9
- 1
docs/timeline/index.html View File

@ -1381,8 +1381,16 @@ timeline.off('select', onSelect);
</td>
<td>Fired after the dragging of a group is finished.
</td>
</tr>
</tr>
<tr>
<td>changed</td>
<td>
Has no properties.
</td>
<td>Fired once after each graph redraw.
</td>
</tr>
<tr>
<td>rangechange</td>
<td>

+ 2
- 0
examples/network/datasources/largeHierarchicalDataset.js
File diff suppressed because it is too large
View File


+ 8
- 2
examples/network/exampleUtil.js View File

@ -75,11 +75,15 @@ function seededRandom() {
return x - Math.floor(x);
}
function getScaleFreeNetworkSeeded(nodeCount) {
function getScaleFreeNetworkSeeded(nodeCount, seed) {
if (seed) {
randomSeed = Number(seed);
}
var nodes = [];
var edges = [];
var connectionCount = [];
randomSeed = 764;
var edgesId = 0;
// randomly create some nodes and edges
for (var i = 0; i < nodeCount; i++) {
@ -95,6 +99,7 @@ function getScaleFreeNetworkSeeded(nodeCount) {
var from = i;
var to = 0;
edges.push({
id: edgesId++,
from: from,
to: to
});
@ -115,6 +120,7 @@ function getScaleFreeNetworkSeeded(nodeCount) {
var from = i;
var to = j;
edges.push({
id: edgesId++,
from: from,
to: to
});

+ 1
- 1
examples/network/layout/hierarchicalLayoutMethods.html View File

@ -19,7 +19,7 @@
<script type="text/javascript">
var network = null;
var layoutMethod = "hubsize";
var layoutMethod = "directed";
function destroy() {
if (network !== null) {

+ 107
- 102
examples/network/layout/hierarchicalLayoutUserdefined.html View File

@ -1,120 +1,125 @@
<!doctype html>
<html>
<head>
<title>Network | Hierarchical Layout, userDefined</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mynetwork {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<title>Network | Hierarchical Layout, userDefined</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mynetwork {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript">
var nodes = null;
var edges = null;
var network = null;
var directionInput = document.getElementById("direction");
<script type="text/javascript">
var nodes = null;
var edges = null;
var network = null;
var directionInput = document.getElementById("direction");
function destroy() {
if (network !== null) {
network.destroy();
network = null;
}
}
function draw() {
destroy();
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
for (var i = 0; i < 15; i++) {
nodes.push({id: i,label: String(i)});
}
edges.push({from: 0, to: 1 });
edges.push({from: 0, to: 6 });
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2 });
edges.push({from: 2, to: 3 });
edges.push({from: 2, to: 4 });
edges.push({from: 3, to: 5 });
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7 });
edges.push({from: 2, to: 8 });
edges.push({from: 2, to: 9 });
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
nodes[0]["level"] = 0;
nodes[1]["level"] = 1;
nodes[2]["level"] = 3;
nodes[3]["level"] = 4;
nodes[4]["level"] = 4;
nodes[5]["level"] = 5;
nodes[6]["level"] = 1;
nodes[7]["level"] = 2;
nodes[8]["level"] = 4;
nodes[9]["level"] = 4;
nodes[10]["level"] = 2;
nodes[11]["level"] = 1;
nodes[12]["level"] = 2;
nodes[13]["level"] = 1;
nodes[14]["level"] = 5;
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
smooth: {
type:'cubicBezier',
forceDirection: (directionInput.value == "UD" || directionInput.value == "DU") ? 'vertical' : 'horizontal',
roundness: 0.4
}
},
layout: {
hierarchical:{
direction: directionInput.value
function destroy() {
if (network !== null) {
network.destroy();
network = null;
}
}
};
network = new vis.Network(container, data, options);
// add event listeners
network.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
function draw() {
destroy();
nodes = [];
edges = [];
var connectionCount = [];
</script>
<script src="../../googleAnalytics.js"></script>
// randomly create some nodes and edges
for (var i = 0; i < 15; i++) {
nodes.push({id: i, label: String(i)});
}
edges.push({from: 0, to: 1});
edges.push({from: 0, to: 6});
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2});
edges.push({from: 2, to: 3});
edges.push({from: 2, to: 4});
edges.push({from: 3, to: 5});
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7});
edges.push({from: 2, to: 8});
edges.push({from: 2, to: 9});
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
nodes[0]["level"] = 0;
nodes[1]["level"] = 1;
nodes[2]["level"] = 3;
nodes[3]["level"] = 4;
nodes[4]["level"] = 4;
nodes[5]["level"] = 5;
nodes[6]["level"] = 1;
nodes[7]["level"] = 2;
nodes[8]["level"] = 4;
nodes[9]["level"] = 4;
nodes[10]["level"] = 2;
nodes[11]["level"] = 1;
nodes[12]["level"] = 2;
nodes[13]["level"] = 1;
nodes[14]["level"] = 5;
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
smooth: {
type: 'cubicBezier',
forceDirection: (directionInput.value == "UD" || directionInput.value == "DU") ? 'vertical' : 'horizontal',
roundness: 0.4
}
},
layout: {
hierarchical: {
direction: directionInput.value
}
},
physics:false
};
network = new vis.Network(container, data, options);
// add event listeners
network.on('select', function (params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</script>
<script src="../../googleAnalytics.js"></script>
</head>
<body onload="draw();">
<h2>Hierarchical Layout - User-defined</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined.
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for
all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined.
If the smooth curves appear to be inverted, the direction of the edge is not in the same direction as the network.
</div>
<p>
<input type="button" id="btn-UD" value="Up-Down">
<input type="button" id="btn-DU" value="Down-Up">
<input type="button" id="btn-LR" value="Left-Right">
<input type="button" id="btn-RL" value="Right-Left">
<input type="hidden" id='direction' value="UD">
<input type="button" id="btn-UD" value="Up-Down">
<input type="button" id="btn-DU" value="Down-Up">
<input type="button" id="btn-LR" value="Left-Right">
<input type="button" id="btn-RL" value="Right-Left">
<input type="hidden" id='direction' value="UD">
</p>
<div id="mynetwork"></div>
@ -123,22 +128,22 @@
<script language="JavaScript">
var directionInput = document.getElementById("direction");
var btnUD = document.getElementById("btn-UD");
btnUD.onclick = function() {
btnUD.onclick = function () {
directionInput.value = "UD";
draw();
};
var btnDU = document.getElementById("btn-DU");
btnDU.onclick = function() {
btnDU.onclick = function () {
directionInput.value = "DU";
draw();
};
var btnLR = document.getElementById("btn-LR");
btnLR.onclick = function() {
btnLR.onclick = function () {
directionInput.value = "LR";
draw();
};
var btnRL = document.getElementById("btn-RL");
btnRL.onclick = function() {
btnRL.onclick = function () {
directionInput.value = "RL";
draw();
};

+ 85
- 0
examples/network/layout/hierarchicalLayoutWithoutPhysics.html View File

@ -0,0 +1,85 @@
<html>
<head>
<meta charset="utf-8">
<title>Hierarchical Layout without Physics</title>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<script type="text/javascript" src="../datasources/largeHierarchicalDataset.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
#network{
width: 1000px;
height: 400px;
border: 1px solid lightgray;
}
td {
vertical-align:top;
}
table {
width:800px;
}
</style>
</head>
<body>
<h1>Hierarchical Layout without Physics</h1>
The hierarchical layout can now be controlled without the use of physics. This is much quicker. The options for this are: <br /><br />
<table>
<tr>
<td width="150px"><code>levelSeparation</code></td>
<td width="400px">Distance between levels.</td>
</tr>
<tr>
<td><code>nodeSpacing</code></td>
<td>Minimum distance between nodes on the free axis.</td>
</tr>
<tr>
<td><code>treeSpacing</code></td>
<td>Distance between different trees (independent networks).</td>
</tr>
<tr>
<td><code>blockShifting</code></td>
<td>Method for reducing whitespace. Can be used alone or together with edge minimization. Each node will check for whitespace and will shift
it's branch along with it for as far as it can, respecting the nodeSpacing on any level.</td>
</tr>
<tr>
<td><code>edgeMinimization</code></td>
<td>Method for reducing whitespace. Can be used alone or together with block shifting. Enabling block shifting will usually speed up the layout process.
Each node will try to move along its free axis to reduce the total length of it's edges.</td>
</tr>
</table>
<br /><br />
Play with the settings below the network and see how the layout changes!
<div id="network"></div>
<script>
var data = {
nodes: nodes,
edges: edges
};
// create a network
var container = document.getElementById('network');
var options = {
layout: {
hierarchical: {
direction: "UD",
sortMethod: "directed"
}
},
interaction: {dragNodes :false},
physics: {
enabled: false
},
configure: {
filter: function (option, path) {
if (path.indexOf('hierarchical') !== -1) {
return true;
}
return false;
},
showButton:false
}
};
var network = new vis.Network(container, data, options);
</script>
</body>
</html>

+ 11
- 0
examples/network/other/configuration.html View File

@ -52,6 +52,15 @@
configure: true
};
network = new vis.Network(container, data, options);
network.on("configChange", function() {
// this will immediately fix the height of the configuration
// wrapper to prevent unecessary scrolls in chrome.
// see https://github.com/almende/vis/issues/1568
var div = container.getElementsByClassName('vis-configuration-wrapper')[0];
div.style["height"] = div.getBoundingClientRect().height + "px";
});
}
</script>
<script src="../../googleAnalytics.js"></script>
@ -64,6 +73,8 @@
You can also supply a custom filter function or filter string. You can press the generate options button below to have an options object printed. You can then use
this in the network.
</p>
<p><b>Note:</b> The configurator is recreated in the dom tree on input change. This may cause undesired scrolls in your application. In order to avoid this, explicitly set the height of the configurator (see this example's source code).
</p>
<br />
<div id="mynetwork"></div>

+ 3
- 3
gulpfile.js View File

@ -132,13 +132,13 @@ gulp.task('bundle-css', ['clean'], function () {
});
gulp.task('copy', ['clean'], function () {
var network = gulp.src('./lib/network/img/**/*')
var network = gulp.src('./lib/network/img/**/*')
.pipe(gulp.dest(DIST + '/img/network'));
var timeline = gulp.src('./lib/timeline/img/**/*')
var timeline = gulp.src('./lib/timeline/img/**/*')
.pipe(gulp.dest(DIST + '/img/timeline'));
return merge(network, timeline);
return merge(network, timeline);
});
gulp.task('minify', ['bundle-js'], function (cb) {

+ 1
- 1
index.js View File

@ -23,7 +23,6 @@ exports.Timeline = require('./lib/timeline/Timeline');
exports.Graph2d = require('./lib/timeline/Graph2d');
exports.timeline = {
Core: require('./lib/timeline/Core'),
DataStep: require('./lib/timeline/DataStep'),
DateUtil: require('./lib/timeline/DateUtil'),
Range: require('./lib/timeline/Range'),
stack: require('./lib/timeline/Stack'),
@ -43,6 +42,7 @@ exports.timeline = {
CurrentTime: require('./lib/timeline/component/CurrentTime'),
CustomTime: require('./lib/timeline/component/CustomTime'),
DataAxis: require('./lib/timeline/component/DataAxis'),
DataScale: require('./lib/timeline/component/DataScale'),
GraphGroup: require('./lib/timeline/component/GraphGroup'),
Group: require('./lib/timeline/component/Group'),
ItemSet: require('./lib/timeline/component/ItemSet'),

+ 2
- 21
lib/hammerUtil.js View File

@ -7,23 +7,14 @@ var Hammer = require('./module/hammer');
*/
exports.onTouch = function (hammer, callback) {
callback.inputHandler = function (event) {
if (event.isFirst && !isTouching) {
if (event.isFirst) {
callback(event);
isTouching = true;
setTimeout(function () {
isTouching = false;
}, 0);
}
};
hammer.on('hammer.input', callback.inputHandler);
};
// isTouching is true while a touch action is being emitted
// this is a hack to prevent `touch` from being fired twice
var isTouching = false;
/**
* Register a release event, taking place after a gesture
* @param {Hammer} hammer A hammer instance
@ -31,13 +22,8 @@ var isTouching = false;
*/
exports.onRelease = function (hammer, callback) {
callback.inputHandler = function (event) {
if (event.isFinal && !isReleasing) {
if (event.isFinal) {
callback(event);
isReleasing = true;
setTimeout(function () {
isReleasing = false;
}, 0);
}
};
@ -45,11 +31,6 @@ exports.onRelease = function (hammer, callback) {
};
// isReleasing is true while a release action is being emitted
// this is a hack to prevent `release` from being fired twice
var isReleasing = false;
/**
* Unregister a touch event, taking place before a gesture
* @param {Hammer} hammer A hammer instance

+ 0
- 1
lib/network/Network.js View File

@ -144,7 +144,6 @@ Emitter(Network.prototype);
*/
Network.prototype.setOptions = function (options) {
if (options !== undefined) {
let errorFound = Validator.validate(options, allOptions);
if (errorFound === true) {
console.log('%cErrors have been found in the supplied options object.', printStyle);

+ 5
- 0
lib/network/modules/EdgesHandler.js View File

@ -27,6 +27,7 @@ class EdgesHandler {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',
@ -140,6 +141,10 @@ class EdgesHandler {
this.body.emitter.on("refreshEdges", this.refresh.bind(this));
this.body.emitter.on("refresh", this.refresh.bind(this));
this.body.emitter.on("destroy", () => {
util.forEach(this.edgesListeners, (callback, event) => {
if (this.body.data.edges)
this.body.data.edges.off(event, callback);
});
delete this.body.functions.createEdge;
delete this.edgesListeners.add;
delete this.edgesListeners.update;

+ 31
- 28
lib/network/modules/InteractionHandler.js View File

@ -487,38 +487,41 @@ class InteractionHandler {
* @private
*/
onMouseWheel(event) {
// retrieve delta
let delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta / 120;
} else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail / 3;
}
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta !== 0) {
// calculate the new scale
let scale = this.body.view.scale;
let zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
if (this.options.zoomView === true) {
// retrieve delta
let delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta / 120;
}
else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail / 3;
}
scale *= (1 + zoom);
// calculate the pointer location
let pointer = this.getPointer({x:event.clientX, y:event.clientY});
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta !== 0) {
// apply the new scale
this.zoom(scale, pointer);
}
// calculate the new scale
let scale = this.body.view.scale;
let zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
}
scale *= (1 + zoom);
// Prevent default actions caused by mouse wheel.
event.preventDefault();
// calculate the pointer location
let pointer = this.getPointer({x: event.clientX, y: event.clientY});
// apply the new scale
this.zoom(scale, pointer);
}
// Prevent default actions caused by mouse wheel.
event.preventDefault();
}
}

+ 564
- 47
lib/network/modules/LayoutEngine.js View File

@ -9,9 +9,9 @@ class LayoutEngine {
this.initialRandomSeed = Math.round(Math.random() * 1000000);
this.randomSeed = this.initialRandomSeed;
this.setPhysics = false;
this.options = {};
this.optionsBackup = {};
this.optionsBackup = {physics:{}};
this.defaultOptions = {
randomSeed: undefined,
@ -19,16 +19,15 @@ class LayoutEngine {
hierarchical: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100,
treeSpacing: 200,
blockShifting: true,
edgeMinimization: true,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
}
};
util.extend(this.options, this.defaultOptions);
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.bindEventListeners();
}
@ -57,7 +56,7 @@ class LayoutEngine {
this.body.emitter.emit('refresh', true);
}
// make sure the level seperation is the right way up
// make sure the level separation is the right way up
if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') {
if (this.options.hierarchical.levelSeparation > 0) {
this.options.hierarchical.levelSeparation *= -1;
@ -88,19 +87,21 @@ class LayoutEngine {
if (this.options.hierarchical.enabled === true) {
// set the physics
if (allOptions.physics === undefined || allOptions.physics === true) {
allOptions.physics = {solver: 'hierarchicalRepulsion'};
this.optionsBackup.physics = {solver:'barnesHut'};
allOptions.physics = {
enabled:this.optionsBackup.physics.enabled === undefined ? true : this.optionsBackup.physics.enabled,
solver:'hierarchicalRepulsion'
};
this.optionsBackup.physics.enabled = this.optionsBackup.physics.enabled === undefined ? true : this.optionsBackup.physics.enabled;
this.optionsBackup.physics.solver = this.optionsBackup.physics.solver || 'barnesHut';
}
else if (typeof allOptions.physics === 'object') {
this.optionsBackup.physics = {solver:'barnesHut'};
if (allOptions.physics.solver !== undefined) {
this.optionsBackup.physics = {solver:allOptions.physics.solver};
}
allOptions.physics['solver'] = 'hierarchicalRepulsion';
this.optionsBackup.physics.enabled = allOptions.physics.enabled === undefined ? true : allOptions.physics.enabled;
this.optionsBackup.physics.solver = allOptions.physics.solver || 'barnesHut';
allOptions.physics.solver = 'hierarchicalRepulsion';
}
else if (allOptions.physics !== false) {
this.optionsBackup.physics = {solver:'barnesHut'};
allOptions.physics['solver'] = 'hierarchicalRepulsion';
this.optionsBackup.physics.solver ='barnesHut';
allOptions.physics = {solver:'hierarchicalRepulsion'};
}
// get the type of static smooth curve in case it is required
@ -131,7 +132,7 @@ class LayoutEngine {
this.optionsBackup.edges = {
smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
type:allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type,
type: allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type,
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
};
@ -147,6 +148,7 @@ class LayoutEngine {
// force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth.
this.body.emitter.emit('_forceDisableDynamicCurves', type);
}
return allOptions;
}
@ -174,12 +176,12 @@ class LayoutEngine {
/**
* Use KamadaKawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we
* Use Kamada Kawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we
* cluster them first to reduce the amount.
*/
layoutNetwork() {
if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// first check if we should Kamada Kawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
let positionDefined = 0;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
@ -288,13 +290,26 @@ class LayoutEngine {
// get the size of the largest hubs and check if the user has defined a level for a node.
let node, nodeId;
let definedLevel = false;
let definedPositions = true;
let undefinedLevel = false;
this.hierarchicalLevels = {};
this.nodeSpacing = 100;
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.hierarchicalTrees = {};
this.treeIndex = -1;
this.distributionOrdering = {};
this.distributionIndex = {};
this.distributionOrderingPresence = {};
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.options.x === undefined && node.options.y === undefined) {
definedPositions = false;
}
if (node.options.level !== undefined) {
definedLevel = true;
this.hierarchicalLevels[nodeId] = node.options.level;
@ -311,7 +326,7 @@ class LayoutEngine {
return;
}
else {
// define levels if undefined by the users. Based on hubsize
// define levels if undefined by the users. Based on hubsize.
if (undefinedLevel === true) {
if (this.options.hierarchical.sortMethod === 'hubsize') {
this._determineLevelsByHubsize();
@ -325,6 +340,14 @@ class LayoutEngine {
}
// fallback for cases where there are nodes but no edges
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
if (this.hierarchicalLevels[nodeId] === undefined) {
this.hierarchicalLevels[nodeId] = 0;
}
}
}
// check the distribution of the nodes per level.
let distribution = this._getDistribution();
@ -334,8 +357,8 @@ class LayoutEngine {
// place the nodes on the canvas.
this._placeNodesByHierarchy(distribution);
// Todo: condense the whitespace.
this._condenseHierarchy(distribution);
// condense the whitespace.
this._condenseHierarchy();
// shift to center so gravity does not have to do much
this._shiftToCenter();
@ -344,13 +367,465 @@ class LayoutEngine {
}
/**
* TODO: implement. Clear whitespace after positioning.
* @private
*/
_condenseHierarchy(distribution) {
_condenseHierarchy() {
// Global var in this scope to define when the movement has stopped.
let stillShifting = false;
let branches = {};
// first we have some methods to help shifting trees around.
// the main method to shift the trees
let shiftTrees = () => {
let treeSizes = getTreeSizes();
for (let i = 0; i < treeSizes.length - 1; i++) {
let diff = treeSizes[i].max - treeSizes[i+1].min;
if (diff !== this.options.hierarchical.treeSpacing) {
shiftTree(i + 1, diff - this.options.hierarchical.treeSpacing);
}
}
};
// shift a single tree by an offset
let shiftTree = (index, offset) => {
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
this._setPositionForHierarchy(this.body.nodes[nodeId], offset, undefined, true);
}
}
}
};
// get the width of a tree
let getTreeSize = (index) => {
let min = 1e9;
let max = -1e9;
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]);
min = Math.min(pos, min);
max = Math.max(pos, max);
}
}
}
return {min:min, max:max};
};
// get the width of all trees
let getTreeSizes = () => {
let treeWidths = [];
for (let i = 0; i < this.treeIndex; i++) {
treeWidths.push(getTreeSize(i));
}
return treeWidths;
};
// get a map of all nodes in this branch
let getBranchNodes = (source, map) => {
map[source.id] = true;
if (this.hierarchicalParents[source.id]) {
let children = this.hierarchicalParents[source.id].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
getBranchNodes(this.body.nodes[children[i]], map);
}
}
}
};
// get a min max width as well as the maximum movement space it has on either sides
// we use min max terminology because width and height can interchange depending on the direction of the layout
let getBranchBoundary = (branchMap, maxLevel = 1e9) => {
let minSpace = 1e9;
let maxSpace = 1e9;
let min = 1e9;
let max = -1e9;
for (let branchNode in branchMap) {
if (branchMap.hasOwnProperty(branchNode)) {
let node = this.body.nodes[branchNode];
let level = this.hierarchicalLevels[node.id];
let position = this._getPositionForHierarchy(node);
// get the space around the node.
let [minSpaceNode, maxSpaceNode] = this._getSpaceAroundNode(node,branchMap);
minSpace = Math.min(minSpaceNode, minSpace);
maxSpace = Math.min(maxSpaceNode, maxSpace);
// the width is only relevant for the levels two nodes have in common. This is why we filter on this.
if (level <= maxLevel) {
min = Math.min(position, min);
max = Math.max(position, max);
}
}
}
return [min, max, minSpace, maxSpace];
};
// get the maximum level of a branch.
let getMaxLevel = (nodeId) => {
let level = this.hierarchicalLevels[nodeId];
if (this.hierarchicalParents[nodeId]) {
let children = this.hierarchicalParents[nodeId].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,getMaxLevel(children[i]));
}
}
}
return level;
};
// check what the maximum level is these nodes have in common.
let getCollisionLevel = (node1, node2) => {
let maxLevel1 = getMaxLevel(node1.id);
let maxLevel2 = getMaxLevel(node2.id);
return Math.min(maxLevel1, maxLevel2);
};
// check if two nodes have the same parent(s)
let hasSameParent = (node1, node2) => {
let parents1 = this.hierarchicalChildren[node1.id];
let parents2 = this.hierarchicalChildren[node2.id];
if (parents1 === undefined || parents2 === undefined) {
return false;
}
parents1 = parents1.parents;
parents2 = parents2.parents;
for (let i = 0; i < parents1.length; i++) {
for (let j = 0; j < parents2.length; j++) {
if (parents1[i] == parents2[j]) {
return true;
}
}
}
return false;
};
// condense elements. These can be nodes or branches depending on the callback.
let shiftElementsCloser = (callback, levels, centerParents) => {
for (let i = 0; i < levels.length; i++) {
let level = levels[i];
let levelNodes = this.distributionOrdering[level];
if (levelNodes.length > 1) {
for (let j = 0; j < levelNodes.length - 1; j++) {
if (hasSameParent(levelNodes[j],levelNodes[j+1]) === true) {
if (this.hierarchicalTrees[levelNodes[j].id] === this.hierarchicalTrees[levelNodes[j+1].id]) {
callback(levelNodes[j],levelNodes[j+1], centerParents);
}
}}
}
}
};
// callback for shifting branches
let branchShiftCallback = (node1, node2, centerParent = false) => {
//window.CALLBACKS.push(() => {
let pos1 = this._getPositionForHierarchy(node1);
let pos2 = this._getPositionForHierarchy(node2);
let diffAbs = Math.abs(pos2 - pos1);
//console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
if (diffAbs > this.options.hierarchical.nodeSpacing) {
let branchNodes1 = {}; branchNodes1[node1.id] = true;
let branchNodes2 = {}; branchNodes2[node2.id] = true;
getBranchNodes(node1, branchNodes1);
getBranchNodes(node2, branchNodes2);
// check the largest distance between the branches
let maxLevel = getCollisionLevel(node1, node2);
let [min1,max1, minSpace1, maxSpace1] = getBranchBoundary(branchNodes1, maxLevel);
let [min2,max2, minSpace2, maxSpace2] = getBranchBoundary(branchNodes2, maxLevel);
//console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, getBranchBoundary(branchNodes2, maxLevel), maxLevel);
let diffBranch = Math.abs(max1 - min2);
if (diffBranch > this.options.hierarchical.nodeSpacing) {
let offset = max1 - min2 + this.options.hierarchical.nodeSpacing;
if (offset < -minSpace2 + this.options.hierarchical.nodeSpacing) {
offset = -minSpace2 + this.options.hierarchical.nodeSpacing;
//console.log("RESETTING OFFSET", max1 - min2 + this.options.hierarchical.nodeSpacing, -minSpace2, offset);
}
if (offset < 0) {
//console.log("SHIFTING", node2.id, offset);
this._shiftBlock(node2.id, offset);
stillShifting = true;
if (centerParent === true)
this._centerParent(node2);
}
}
}
//this.body.emitter.emit("_redraw");})
};
let minimizeEdgeLength = (iterations, node) => {
//window.CALLBACKS.push(() => {
// console.log("ts",node.id);
let nodeId = node.id;
let allEdges = node.edges;
let nodeLevel = this.hierarchicalLevels[node.id];
// gather constants
let C2 = this.options.hierarchical.levelSeparation * this.options.hierarchical.levelSeparation;
let referenceNodes = {};
let aboveEdges = [];
for (let i = 0; i < allEdges.length; i++) {
let edge = allEdges[i];
if (edge.toId != edge.fromId) {
let otherNode = edge.toId == nodeId ? edge.from : edge.to;
referenceNodes[allEdges[i].id] = otherNode;
if (this.hierarchicalLevels[otherNode.id] < nodeLevel) {
aboveEdges.push(edge);
}
}
}
// differentiated sum of lengths based on only moving one node over one axis
let getFx = (point, edges) => {
let sum = 0;
for (let i = 0; i < edges.length; i++) {
if (referenceNodes[edges[i].id] !== undefined) {
let a = this._getPositionForHierarchy(referenceNodes[edges[i].id]) - point;
sum += a / Math.sqrt(a * a + C2);
}
}
return sum;
};
// doubly differentiated sum of lengths based on only moving one node over one axis
let getDFx = (point, edges) => {
let sum = 0;
for (let i = 0; i < edges.length; i++) {
if (referenceNodes[edges[i].id] !== undefined) {
let a = this._getPositionForHierarchy(referenceNodes[edges[i].id]) - point;
sum -= (C2 * Math.pow(a * a + C2, -1.5));
}
}
return sum;
};
let getGuess = (iterations, edges) => {
let guess = this._getPositionForHierarchy(node);
// Newton's method for optimization
let guessMap = {};
for (let i = 0; i < iterations; i++) {
let fx = getFx(guess, edges);
let dfx = getDFx(guess, edges);
// we limit the movement to avoid instability.
let limit = 40;
let ratio = Math.max(-limit, Math.min(limit, Math.round(fx/dfx)));
guess = guess - ratio;
// reduce duplicates
if (guessMap[guess] !== undefined) {
break;
}
guessMap[guess] = i;
}
return guess;
};
let moveBranch = (guess) => {
// position node if there is space
let nodePosition = this._getPositionForHierarchy(node);
// check movable area of the branch
if (branches[node.id] === undefined) {
let branchNodes = {};
branchNodes[node.id] = true;
getBranchNodes(node, branchNodes);
branches[node.id] = branchNodes;
}
let [minBranch, maxBranch, minSpaceBranch, maxSpaceBranch] = getBranchBoundary(branches[node.id]);
let diff = guess - nodePosition;
// check if we are allowed to move the node:
let branchOffset = 0;
if (diff > 0) {
branchOffset = Math.min(diff, maxSpaceBranch - this.options.hierarchical.nodeSpacing);
}
else if (diff < 0) {
branchOffset = -Math.min(-diff, minSpaceBranch - this.options.hierarchical.nodeSpacing);
}
if (branchOffset != 0) {
//console.log("moving branch:",branchOffset, maxSpaceBranch, minSpaceBranch)
this._shiftBlock(node.id, branchOffset);
//this.body.emitter.emit("_redraw");
stillShifting = true;
}
};
let moveNode = (guess) => {
let nodePosition = this._getPositionForHierarchy(node);
// position node if there is space
let [minSpace, maxSpace] = this._getSpaceAroundNode(node);
let diff = guess - nodePosition;
// check if we are allowed to move the node:
let newPosition = nodePosition;
if (diff > 0) {
newPosition = Math.min(nodePosition + (maxSpace - this.options.hierarchical.nodeSpacing), guess);
}
else if (diff < 0) {
newPosition = Math.max(nodePosition - (minSpace - this.options.hierarchical.nodeSpacing), guess);
}
if (newPosition !== nodePosition) {
//console.log("moving Node:",diff, minSpace, maxSpace)
this._setPositionForHierarchy(node, newPosition, undefined, true);
//this.body.emitter.emit("_redraw");
stillShifting = true;
}
};
let guess = getGuess(iterations, aboveEdges);
moveBranch(guess);
guess = getGuess(iterations, allEdges);
moveNode(guess);
//})
};
// method to remove whitespace between branches. Because we do bottom up, we can center the parents.
let minimizeEdgeLengthBottomUp = (iterations) => {
let levels = Object.keys(this.distributionOrdering);
levels = levels.reverse();
for (let i = 0; i < iterations; i++) {
stillShifting = false;
for (let j = 0; j < levels.length; j++) {
let level = levels[j];
let levelNodes = this.distributionOrdering[level];
for (let k = 0; k < levelNodes.length; k++) {
minimizeEdgeLength(1000, levelNodes[k]);
}
}
if (stillShifting !== true) {
//console.log("FINISHED minimizeEdgeLengthBottomUp IN " + i);
break;
}
}
};
//// method to remove whitespace between branches. Because we do bottom up, we can center the parents.
let shiftBranchesCloserBottomUp = (iterations) => {
let levels = Object.keys(this.distributionOrdering);
levels = levels.reverse();
for (let i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(branchShiftCallback, levels, true);
if (stillShifting !== true) {
//console.log("FINISHED shiftBranchesCloserBottomUp IN " + (i+1));
break;
}
}
};
// center all parents
let centerAllParents = () => {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId))
this._centerParent(this.body.nodes[nodeId]);
}
};
// the actual work is done here.
if (this.options.hierarchical.blockShifting === true) {
shiftBranchesCloserBottomUp(5);
centerAllParents();
}
// minimize edge length
if (this.options.hierarchical.edgeMinimization === true) {
minimizeEdgeLengthBottomUp(20);
}
shiftTrees();
}
/**
* This gives the space around the node. IF a map is supplied, it will only check against nodes NOT in the map.
* This is used to only get the distances to nodes outside of a branch.
* @param node
* @param map
* @returns {*[]}
* @private
*/
_getSpaceAroundNode(node, map) {
let useMap = true;
if (map === undefined) {
useMap = false;
}
let level = this.hierarchicalLevels[node.id];
if (level !== undefined) {
let index = this.distributionIndex[node.id];
let position = this._getPositionForHierarchy(node);
let minSpace = 1e9;
let maxSpace = 1e9;
if (index !== 0) {
let prevNode = this.distributionOrdering[level][index - 1];
if ((useMap === true && map[prevNode.id] === undefined) || useMap === false) {
let prevPos = this._getPositionForHierarchy(prevNode);
minSpace = position - prevPos;
}
}
if (index != this.distributionOrdering[level].length - 1) {
let nextNode = this.distributionOrdering[level][index + 1];
if ((useMap === true && map[nextNode.id] === undefined) || useMap === false) {
let nextPos = this._getPositionForHierarchy(nextNode);
maxSpace = Math.min(maxSpace, nextPos - position);
}
}
return [minSpace, maxSpace];
}
else {
return [0, 0];
}
}
/**
* We use this method to center a parent node and check if it does not cross other nodes when it does.
* @param node
* @private
*/
_centerParent(node) {
if (this.hierarchicalChildren[node.id]) {
let parents = this.hierarchicalChildren[node.id].parents;
for (var i = 0; i < parents.length; i++) {
let parentId = parents[i];
let parentNode = this.body.nodes[parentId];
if (this.hierarchicalParents[parentId]) {
// get the range of the children
let minPos = 1e9;
let maxPos = -1e9;
let children = this.hierarchicalParents[parentId].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
let childNode = this.body.nodes[children[i]];
minPos = Math.min(minPos, this._getPositionForHierarchy(childNode));
maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode));
}
}
let position = this._getPositionForHierarchy(parentNode);
let [minSpace, maxSpace] = this._getSpaceAroundNode(parentNode);
let newPosition = 0.5 * (minPos + maxPos);
let diff = position - newPosition;
if ((diff < 0 && Math.abs(diff) < maxSpace - this.options.hierarchical.nodeSpacing) || (diff > 0 && Math.abs(diff) < minSpace - this.options.hierarchical.nodeSpacing)) {
this._setPositionForHierarchy(parentNode, newPosition, undefined, true);
}
}
}
}
}
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
@ -370,7 +845,7 @@ class LayoutEngine {
for (let i = 0; i < nodeArray.length; i++) {
let node = nodeArray[i];
if (this.positionedNodes[node.id] === undefined) {
this._setPositionForHierarchy(node, this.nodeSpacing * i);
this._setPositionForHierarchy(node, this.options.hierarchical.nodeSpacing * i, level);
this.positionedNodes[node.id] = true;
this._placeBranchNodes(node.id, level);
}
@ -546,14 +1021,18 @@ class LayoutEngine {
// get the minimum level
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel);
if (this.hierarchicalLevels[nodeId] !== undefined) {
minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel);
}
}
}
// subtract the minimum from the set so we have a range starting from 0
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.hierarchicalLevels[nodeId] -= minLevel;
if (this.hierarchicalLevels[nodeId] !== undefined) {
this.hierarchicalLevels[nodeId] -= minLevel;
}
}
}
}
@ -561,8 +1040,6 @@ class LayoutEngine {
/**
* Update the bookkeeping of parent and child.
* @param parentNodeId
* @param childNodeId
* @private
*/
_generateMap() {
@ -586,7 +1063,7 @@ class LayoutEngine {
/**
* Crawl over the entire network and use a callback on each node couple that is connected to eachother.
* Crawl over the entire network and use a callback on each node couple that is connected to each other.
* @param callback | will receive nodeA nodeB and the connecting edge. A and B are unique.
* @param startingNodeId
* @private
@ -598,12 +1075,18 @@ class LayoutEngine {
progress[node.id] = true;
let childNode;
for (let i = 0; i < node.edges.length; i++) {
if (node.edges[i].toId === node.id) {childNode = node.edges[i].from;}
else {childNode = node.edges[i].to;}
if (node.edges[i].connected === true) {
if (node.edges[i].toId === node.id) {
childNode = node.edges[i].from;
}
else {
childNode = node.edges[i].to;
}
if (node.id !== childNode.id) {
callback(node, childNode, node.edges[i]);
crawler(childNode);
if (node.id !== childNode.id) {
callback(node, childNode, node.edges[i]);
crawler(childNode);
}
}
}
}
@ -625,8 +1108,6 @@ class LayoutEngine {
}
crawler(node);
}
}
@ -657,21 +1138,21 @@ class LayoutEngine {
for (let i = 0; i < childNodes.length; i++) {
let childNode = childNodes[i];
let childNodeLevel = this.hierarchicalLevels[childNode.id];
// check if the childnode is below the parent node and if it has already been positioned.
// check if the child node is below the parent node and if it has already been positioned.
if (childNodeLevel > parentLevel && this.positionedNodes[childNode.id] === undefined) {
// get the amount of space required for this node. If parent the width is based on the amount of children.
let pos;
// we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y
if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);}
else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.nodeSpacing;}
this._setPositionForHierarchy(childNode, pos);
else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;}
this._setPositionForHierarchy(childNode, pos, childNodeLevel);
// if overlap has been detected, we shift the branch
if (this.lastNodeOnLevel[childNodeLevel] !== undefined) {
let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[childNodeLevel]]);
if (pos - previousPos < this.nodeSpacing) {
let diff = (previousPos + this.nodeSpacing) - pos;
if (pos - previousPos < this.options.hierarchical.nodeSpacing) {
let diff = (previousPos + this.options.hierarchical.nodeSpacing) - pos;
let sharedParent = this._findCommonParent(this.lastNodeOnLevel[childNodeLevel], childNode.id);
this._shiftBlock(sharedParent.withChild, diff);
}
@ -685,7 +1166,7 @@ class LayoutEngine {
this._placeBranchNodes(childNode.id, childNodeLevel);
}
else {
return
return;
}
}
@ -697,7 +1178,7 @@ class LayoutEngine {
minPos = Math.min(minPos, this._getPositionForHierarchy(this.body.nodes[childNodeId]));
maxPos = Math.max(maxPos, this._getPositionForHierarchy(this.body.nodes[childNodeId]));
}
this._setPositionForHierarchy(this.body.nodes[parentId], 0.5 * (minPos + maxPos));
this._setPositionForHierarchy(this.body.nodes[parentId], 0.5 * (minPos + maxPos), parentLevel);
}
@ -764,9 +1245,45 @@ class LayoutEngine {
* Abstract the getting of the position so we won't have to repeat the check for direction all the time
* @param node
* @param position
* @param level
* @private
*/
_setPositionForHierarchy(node, position) {
_setPositionForHierarchy(node, position, level, doNotUpdate = false) {
if (doNotUpdate !== true) {
if (this.distributionOrdering[level] === undefined) {
this.distributionOrdering[level] = [];
this.distributionOrderingPresence[level] = {};
}
if (this.distributionOrderingPresence[level][node.id] === undefined) {
this.distributionOrdering[level].push(node);
this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1;
}
this.distributionOrderingPresence[level][node.id] = true;
if (this.hierarchicalTrees[node.id] === undefined) {
if (this.hierarchicalChildren[node.id] !== undefined) {
let tree = 1;
// get the lowest tree denominator.
for (let i = 0; i < this.hierarchicalChildren[node.id].parents.length; i++) {
let parentId = this.hierarchicalChildren[node.id].parents[i];
if (this.hierarchicalTrees[parentId] !== undefined) {
//tree = Math.min(tree,this.hierarchicalTrees[parentId]);
tree = this.hierarchicalTrees[parentId];
}
}
//for (let i = 0; i < this.hierarchicalChildren.parents.length; i++) {
// let parentId = this.hierarchicalChildren.parents[i];
// this.hierarchicalTrees[parentId] = tree;
//}
this.hierarchicalTrees[node.id] = tree;
}
else {
this.hierarchicalTrees[node.id] = ++this.treeIndex;
}
}
}
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.x = position;
}

+ 1
- 1
lib/network/modules/ManipulationSystem.js View File

@ -642,7 +642,7 @@ class ManipulationSystem {
// remove the manipulation divs
if (this.manipulationDiv) {this.canvas.frame.removeChild(this.manipulationDiv);}
if (this.editModeDiv) {this.canvas.frame.removeChild(this.editModeDiv);}
if (this.closeDiv) {this.canvas.frame.removeChild(this.manipulationDiv);}
if (this.closeDiv) {this.canvas.frame.removeChild(this.closeDiv);}
// set the references to undefined
this.manipulationDiv = undefined;

+ 4
- 0
lib/network/modules/NodesHandler.js View File

@ -115,6 +115,10 @@ class NodesHandler {
this.body.emitter.on('refreshNodes', this.refresh.bind(this));
this.body.emitter.on('refresh', this.refresh.bind(this));
this.body.emitter.on('destroy', () => {
util.forEach(this.nodesListeners, (callback, event) => {
if (this.body.data.nodes)
this.body.data.nodes.off(event, callback);
});
delete this.body.functions.createNode;
delete this.nodesListeners.add;
delete this.nodesListeners.update;

+ 36
- 7
lib/network/modules/components/Edge.js View File

@ -95,6 +95,7 @@ class Edge {
static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
var fields = [
'arrowStrikethrough',
'id',
'from',
'hidden',
@ -373,17 +374,45 @@ class Edge {
* @param {CanvasRenderingContext2D} ctx
*/
draw(ctx) {
let via = this.edgeType.drawLine(ctx, this.selected, this.hover);
this.drawArrows(ctx, via);
this.drawLabel (ctx, via);
// get the via node from the edge type
let viaNode = this.edgeType.getViaNode();
let arrowData = {};
// restore edge targets to defaults
this.edgeType.fromPoint = this.from;
this.edgeType.toPoint = this.to;
// from and to arrows give a different end point for edges. we set them here
if (this.options.arrows.from.enabled === true) {
arrowData.from = this.edgeType.getArrowData(ctx,'from', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === false)
this.edgeType.fromPoint = arrowData.from.core;
}
if (this.options.arrows.to.enabled === true) {
arrowData.to = this.edgeType.getArrowData(ctx,'to', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === false)
this.edgeType.toPoint = arrowData.to.core;
}
// the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
if (this.options.arrows.middle.enabled === true) {
arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover);
}
// draw everything
this.edgeType.drawLine(ctx, this.selected, this.hover, viaNode);
this.drawArrows(ctx, arrowData);
this.drawLabel (ctx, viaNode);
}
drawArrows(ctx, viaNode) {
if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode, this.selected, this.hover);}
if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode, this.selected, this.hover);}
if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode, this.selected, this.hover);}
drawArrows(ctx, arrowData) {
if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.from);}
if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.middle);}
if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.to);}
}
drawLabel(ctx, viaNode) {
if (this.options.label !== undefined) {
// set style

+ 16
- 7
lib/network/modules/components/edges/BezierEdgeDynamic.js View File

@ -102,15 +102,24 @@ class BezierEdgeDynamic extends BezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNode) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (viaNode.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.quadraticCurveTo(viaNode.x, viaNode.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
}
getViaNode() {
return this.via;
}
@ -118,14 +127,14 @@ class BezierEdgeDynamic extends BezierEdgeBase {
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param percentage
* @param via
* @param viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage) {
getPoint(percentage, viaNode = this.via) {
let t = percentage;
let x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * this.via.x + Math.pow(t, 2) * this.to.x;
let y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * this.via.y + Math.pow(t, 2) * this.to.y;
let x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
let y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
return {x: x, y: y};
}

+ 21
- 15
lib/network/modules/components/edges/BezierEdgeStatic.js View File

@ -10,28 +10,34 @@ class BezierEdgeStatic extends BezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNode) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
let via = this._getViaCoordinates();
let returnValue = via;
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (via.x === undefined) {
ctx.lineTo(this.to.x, this.to.y);
returnValue = undefined;
if (viaNode.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y);
ctx.quadraticCurveTo(viaNode.x, viaNode.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
return returnValue;
}
getViaNode() {
return this._getViaCoordinates();
}
/**
* We do not use the to and fromPoints here to make the via nodes the same as edges without arrows.
* @returns {{x: undefined, y: undefined}}
* @private
*/
_getViaCoordinates() {
let xVia = undefined;
let yVia = undefined;
@ -214,21 +220,21 @@ class BezierEdgeStatic extends BezierEdgeBase {
return this._findBorderPositionBezier(nearNode, ctx, options.via);
}
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, via = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via);
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, viaNode = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, viaNode);
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param percentage
* @param via
* @param viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, via = this._getViaCoordinates()) {
getPoint(percentage, viaNode = this._getViaCoordinates()) {
var t = percentage;
var x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * this.to.x;
var y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * this.to.y;
var x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
var y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
return {x: x, y: y};
}

+ 14
- 12
lib/network/modules/components/edges/CubicBezierEdge.js View File

@ -10,28 +10,26 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNodes) {
// get the coordinates of the support points.
let [via1,via2] = this._getViaCoordinates();
let returnValue = [via1,via2];
let via1 = viaNodes[0];
let via2 = viaNodes[1];
// start drawing the line.
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (via1.x === undefined) {
ctx.lineTo(this.to.x, this.to.y);
returnValue = undefined;
if (viaNodes === undefined || via1.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.bezierCurveTo(via1.x, via1.y, via2.x, via2.y, this.to.x, this.to.y);
ctx.bezierCurveTo(via1.x, via1.y, via2.x, via2.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
return returnValue;
}
_getViaCoordinates() {
@ -39,7 +37,7 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
let dy = this.from.y - this.to.y;
let x1, y1, x2, y2;
let roundness = this.options.smooth.roundness;;
let roundness = this.options.smooth.roundness;
// horizontal if x > y or if direction is forced or if direction is horizontal
if ((Math.abs(dx) > Math.abs(dy) || this.options.smooth.forceDirection === true || this.options.smooth.forceDirection === 'horizontal') && this.options.smooth.forceDirection !== 'vertical') {
@ -58,6 +56,10 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
return [{x: x1, y: y1},{x: x2, y: y2}];
}
getViaNode() {
return this._getViaCoordinates();
}
_findBorderPosition(nearNode, ctx) {
return this._findBorderPositionBezier(nearNode, ctx);
}
@ -80,8 +82,8 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
let x = vec[0] * this.from.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.to.x;
let y = vec[0] * this.from.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.to.y;
let x = vec[0] * this.fromPoint.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.toPoint.x;
let y = vec[0] * this.fromPoint.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.toPoint.y;
return {x: x, y: y};
}

+ 7
- 4
lib/network/modules/components/edges/StraightEdge.js View File

@ -13,12 +13,15 @@ class StraightEdge extends EdgeBase {
_line(ctx) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.lineTo(this.to.x, this.to.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
ctx.lineTo(this.toPoint.x, this.toPoint.y);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
}
getViaNode() {
return undefined;
}
@ -31,8 +34,8 @@ class StraightEdge extends EdgeBase {
*/
getPoint(percentage) {
return {
x: (1 - percentage) * this.from.x + percentage * this.to.x,
y: (1 - percentage) * this.from.y + percentage * this.to.y
x: (1 - percentage) * this.fromPoint.x + percentage * this.toPoint.x,
y: (1 - percentage) * this.fromPoint.y + percentage * this.toPoint.y
}
}

+ 53
- 53
lib/network/modules/components/edges/util/EdgeBase.js View File

@ -10,6 +10,8 @@ class EdgeBase {
this.color = {};
this.selectionWidth = 2;
this.hoverWidth = 1.5;
this.fromPoint = this.from;
this.toPoint = this.to;
}
connect() {
@ -32,36 +34,32 @@ class EdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
drawLine(ctx, selected, hover) {
drawLine(ctx, selected, hover, viaNode) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.lineWidth = this.getLineWidth(selected, hover);
let via = undefined;
if (this.options.dashes !== false) {
via = this._drawDashedLine(ctx);
this._drawDashedLine(ctx, viaNode);
}
else {
via = this._drawLine(ctx);
this._drawLine(ctx, viaNode);
}
return via;
}
_drawLine(ctx) {
let via = undefined;
_drawLine(ctx, viaNode, fromPoint, toPoint) {
if (this.from != this.to) {
// draw line
via = this._line(ctx);
this._line(ctx, viaNode, fromPoint, toPoint);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, x, y, radius);
}
return via;
}
_drawDashedLine(ctx) {
let via = undefined;
_drawDashedLine(ctx, viaNode, fromPoint, toPoint) {
ctx.lineCap = 'round';
let pattern = [5,5];
if (Array.isArray(this.options.dashes) === true) {
@ -79,7 +77,7 @@ class EdgeBase {
// draw the line
if (this.from != this.to) {
// draw line
via = this._line(ctx);
this._line(ctx, viaNode);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
@ -108,7 +106,6 @@ class EdgeBase {
// disable shadows for other elements.
this.disableShadow(ctx);
}
return via;
}
@ -400,26 +397,22 @@ class EdgeBase {
return Math.sqrt(dx * dx + dy * dy);
}
/**
*
* @param ctx
* @param position
* @param viaNode
*/
drawArrowHead(ctx, position, viaNode, selected, hover) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this.getLineWidth(selected, hover);
getArrowData(ctx, position, viaNode, selected, hover) {
// set lets
let angle;
let length;
let arrowPos;
let arrowPoint;
let node1;
let node2;
let guideOffset;
let scaleFactor;
let lineWidth = this.getLineWidth(selected, hover);
if (position === 'from') {
node1 = this.from;
@ -444,61 +437,68 @@ class EdgeBase {
if (position !== 'middle') {
// draw arrow head
if (this.options.smooth.enabled === true) {
arrowPos = this.findBorderPosition(node1, ctx, {via: viaNode});
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPos.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
arrowPoint = this.findBorderPosition(node1, ctx, {via: viaNode});
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPoint.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPoint.y - guidePos.y), (arrowPoint.x - guidePos.x));
}
else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPos = this.findBorderPosition(node1, ctx);
arrowPoint = this.findBorderPosition(node1, ctx);
}
}
else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
arrowPoint = this.getPoint(0.5, viaNode); // this is 0.6 to account for the size of the arrow.
}
// draw arrow at the end of the line
length = (10 + 5 * this.options.width) * scaleFactor;
ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx);
ctx.stroke();
}
else {
// draw circle
let angle, point;
let [x,y,radius] = this._getCircleData(ctx);
if (position === 'from') {
point = this.findBorderPosition(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
arrowPoint = this.findBorderPosition(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
else if (position === 'to') {
point = this.findBorderPosition(this.from, ctx, {x, y, low:0.6, high:1.0, direction:1});
angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
arrowPoint = this.findBorderPosition(this.from, ctx, {x, y, low:0.6, high:1.0, direction:1});
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
}
else {
point = this._pointOnCircle(x, y, radius, 0.175);
arrowPoint = this._pointOnCircle(x, y, radius, 0.175);
angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
}
// draw the arrowhead
let length = (10 + 5 * this.options.width) * scaleFactor;
ctx.arrow(point.x, point.y, angle, length);
let length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
var xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
var yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
let arrowCore = {x: xi, y: yi};
// disable shadows for other elements.
this.disableShadow(ctx);
ctx.stroke();
}
return {point: arrowPoint, core: arrowCore, angle: angle, length: length};
}
/**
*
* @param ctx
* @param selected
* @param hover
* @param arrowData
*/
drawArrowHead(ctx, selected, hover, arrowData) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this.getLineWidth(selected, hover);
// draw arrow at the end of the line
ctx.arrow(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx);
}

+ 8
- 5
lib/network/modules/components/nodes/shapes/Box.js View File

@ -44,11 +44,14 @@ class Box extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x,y,ctx,selected);

+ 11
- 9
lib/network/modules/components/nodes/shapes/Database.js View File

@ -23,13 +23,12 @@ class Database extends NodeBase {
this.left = x - this.width / 2;
this.top = y - this.height / 2;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height);
@ -43,11 +42,14 @@ class Database extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x,y,ctx,selected);

+ 1
- 1
lib/network/modules/components/nodes/shapes/Dot.js View File

@ -17,7 +17,7 @@ class Dot extends ShapeBase {
distanceToBorder(ctx, angle) {
this.resize(ctx);
return this.options.size + this.options.borderWidth;
return this.options.size;
}
}

+ 13
- 10
lib/network/modules/components/nodes/shapes/Ellipse.js View File

@ -25,15 +25,13 @@ class Ellipse extends NodeBase {
this.left = x - this.width * 0.5;
this.top = y - this.height * 0.5;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth /= this.body.view.scale;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.ellipse(this.left, this.top, this.width, this.height);
@ -46,11 +44,16 @@ class Ellipse extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x, y, ctx, selected);

+ 12
- 11
lib/network/modules/components/nodes/shapes/Image.js View File

@ -18,17 +18,15 @@ class Image extends CircleImageBase {
this.top = y - this.height / 2;
if (this.options.shapeProperties.useBorderWithImage === true) {
let borderWidth = this.options.borderWidth;
let selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.beginPath();
// setup the line properties.
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth /= this.body.view.scale;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
// set a fillstyle
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
@ -42,11 +40,14 @@ class Image extends CircleImageBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
ctx.closePath();

+ 11
- 10
lib/network/modules/components/nodes/util/CircleImageBase.js View File

@ -66,14 +66,12 @@ class CircleImageBase extends NodeBase {
}
_drawRawCircle(ctx, x, y, selected, hover, size) {
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.circle(x, y, size);
@ -86,11 +84,14 @@ class CircleImageBase extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
}

+ 11
- 9
lib/network/modules/components/nodes/util/ShapeBase.js View File

@ -20,13 +20,12 @@ class ShapeBase extends NodeBase {
this.left = x - this.width / 2;
this.top = y - this.height / 2;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth /= this.body.view.scale;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx[shape](x, y, this.options.size);
@ -39,11 +38,14 @@ class ShapeBase extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
if (this.options.label !== undefined) {

+ 12
- 0
lib/network/options.js View File

@ -29,6 +29,7 @@ let allOptions = {
from: { enabled: { boolean }, scaleFactor: { number }, __type__: { object, boolean } },
__type__: { string: ['from', 'to', 'middle'], object }
},
arrowStrikethrough: { boolean },
color: {
color: { string },
highlight: { string },
@ -122,6 +123,10 @@ let allOptions = {
hierarchical: {
enabled: { boolean },
levelSeparation: { number },
nodeSpacing: { number },
treeSpacing: { number },
blockShifting: { boolean },
edgeMinimization: { boolean },
direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL
sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed
__type__: { object, boolean }
@ -347,6 +352,7 @@ let configureOptions = {
},
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: [10, 0, 20, 1],
x: [5, -30, 30, 1],
y: [5, -30, 30, 1]
@ -365,6 +371,7 @@ let configureOptions = {
middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] },
from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }
},
arrowStrikethrough: true,
color: {
color: ['color', '#848484'],
highlight: ['color', '#848484'],
@ -401,6 +408,7 @@ let configureOptions = {
selfReferenceSize: [20, 0, 200, 1],
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: [10, 0, 20, 1],
x: [5, -30, 30, 1],
y: [5, -30, 30, 1]
@ -419,6 +427,10 @@ let configureOptions = {
hierarchical: {
enabled: false,
levelSeparation: [150, 20, 500, 5],
nodeSpacing: [100, 20, 500, 5],
treeSpacing: [200, 20, 500, 5],
blockShifting: true,
edgeMinimization: true,
direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL
sortMethod: ['hubsize', 'directed'] // hubsize, directed
}

+ 7
- 4
lib/shared/ColorPicker.js View File

@ -168,10 +168,13 @@ class ColorPicker {
this.frame.style.display = 'none';
// call the closing callback, restoring the onclick method.
if (this.closeCallback !== undefined) {
this.closeCallback();
this.closeCallback = undefined;
}
// this is in a setTimeout because it will trigger the show again before the click is done.
setTimeout(() => {
if (this.closeCallback !== undefined) {
this.closeCallback();
this.closeCallback = undefined;
}
},0);
}

+ 0
- 1
lib/shared/Configurator.js View File

@ -695,7 +695,6 @@ class Configurator {
}
}
return optionsObj;
}
_printOptions() {

+ 6
- 1
lib/shared/configuration.css View File

@ -10,6 +10,11 @@ div.vis-configuration-wrapper {
width:700px;
}
div.vis-configuration-wrapper::after {
clear: both;
content: "";
display: block;
}
div.vis-configuration.vis-config-option-container{
display:block;
@ -111,7 +116,7 @@ input.vis-configuration.vis-config-rangeinput{
position:relative;
top:-5px;
width:60px;
height:13px;
/*height:13px;*/
padding:1px;
margin:0;
pointer-events:none;

+ 29
- 13
lib/timeline/Core.js View File

@ -29,6 +29,7 @@ Emitter(Core.prototype);
*/
Core.prototype._create = function (container) {
this.dom = {};
this.options = {};
this.dom.container = container;
@ -91,13 +92,15 @@ Core.prototype._create = function (container) {
this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
this.on('rangechange', function () {
this._redraw(); // this allows overriding the _redraw method
if (this.initialDrawDone === true) {
this._redraw(); // this allows overriding the _redraw method
}
}.bind(this));
this.on('touch', this._onTouch.bind(this));
this.on('pan', this._onDrag.bind(this));
var me = this;
this.on('change', function (properties) {
this.on('_change', function (properties) {
if (properties && properties.queue == true) {
// redraw once on next tick
if (!me._redrawTimer) {
@ -181,6 +184,7 @@ Core.prototype._create = function (container) {
this.touch = {};
this.redrawCount = 0;
this.initialDrawDone = false;
// attach the root panel to the provided container
if (!container) throw new Error('No container provided');
@ -223,6 +227,7 @@ Core.prototype.setOptions = function (options) {
];
util.selectiveExtend(fields, this.options, options);
this.options.orientation = {item:undefined,axis:undefined};
if ('orientation' in options) {
if (typeof options.orientation === 'string') {
this.options.orientation = {
@ -317,11 +322,12 @@ Core.prototype.setOptions = function (options) {
// override redraw with a throttled version
if (!this._origRedraw) {
this._origRedraw = this._redraw.bind(this);
this._redraw = util.throttle(this._origRedraw, this.options.throttleRedraw);
} else {
// Not the initial run: redraw everything
this._redraw();
}
this._redraw = util.throttle(this._origRedraw, this.options.throttleRedraw);
// redraw everything
this._redraw();
};
/**
@ -621,12 +627,13 @@ Core.prototype.redraw = function() {
* @protected
*/
Core.prototype._redraw = function() {
this.redrawCount++;
var resized = false;
var options = this.options;
var props = this.props;
var dom = this.dom;
if (!dom) return; // when destroyed
if (!dom|| !dom.container || dom.container.clientWidth == 0 ) return;// when destroyed, or invisible
DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);
@ -766,18 +773,22 @@ Core.prototype._redraw = function() {
this.components.forEach(function (component) {
resized = component.redraw() || resized;
});
var MAX_REDRAW = 5;
if (resized) {
// keep repainting until all sizes are settled
var MAX_REDRAWS = 3; // maximum number of consecutive redraws
if (this.redrawCount < MAX_REDRAWS) {
this.redrawCount++;
this._redraw();
if (this.redrawCount < MAX_REDRAW) {
this.body.emitter.emit('_change');
return;
}
else {
console.log('WARNING: infinite loop in redraw?');
}
} else {
this.redrawCount = 0;
}
this.initialDrawDone = true;
//Emit public 'changed' event for UI updates, see issue #1592
this.body.emitter.emit("changed");
};
// TODO: deprecated since version 1.1.0, remove some day
@ -907,7 +918,7 @@ Core.prototype._startAutoResize = function () {
me.props.lastWidth = me.dom.root.offsetWidth;
me.props.lastHeight = me.dom.root.offsetHeight;
me.emit('change');
me.body.emitter.emit('_change');
}
}
};
@ -915,6 +926,12 @@ Core.prototype._startAutoResize = function () {
// add event listener to window resize
util.addEventListener(window, 'resize', this._onResize);
//Prevent initial unnecessary redraw
if (me.dom.root) {
me.props.lastWidth = me.dom.root.offsetWidth;
me.props.lastHeight = me.dom.root.offsetHeight;
}
this.watchTimer = setInterval(this._onResize, 1000);
};
@ -971,7 +988,6 @@ Core.prototype._onDrag = function (event) {
if (newScrollTop != oldScrollTop) {
this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
this.emit("verticalDrag");
}
};

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

@ -1,233 +0,0 @@
/**
* @constructor DataStep
* The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
* end data point. The class itself determines the best scale (step size) based on the
* provided start Date, end Date, and minimumStep.
*
* If minimumStep is provided, the step size is chosen as close as possible
* to the minimumStep but larger than minimumStep. If minimumStep is not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6 characters
*
* Alternatively, you can set a scale by hand.
* After creation, you can initialize the class by executing first(). Then you
* can iterate from the start date to the end date via next(). You can check if
* the end date is reached with the function hasNext(). After each step, you can
* retrieve the current date via getCurrent().
* The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
* days, to years.
*
* Version: 1.2
*
* @param {Date} [start] The start date, for example new Date(2010, 9, 21)
* or new Date(2010, 9, 21, 23, 45, 00)
* @param {Date} [end] The end date
* @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
function DataStep(start, end, minimumStep, containerHeight, customRange, formattingFunction, alignZeros) {
// variables
this.current = 0;
this.autoScale = true;
this.stepIndex = 0;
this.step = 1;
this.scale = 1;
this.formattingFunction = formattingFunction;
this.marginStart;
this.marginEnd;
this.deadSpace = 0;
this.majorSteps = [1, 2, 5, 10];
this.minorSteps = [0.25, 0.5, 1, 2];
this.alignZeros = alignZeros;
this.setRange(start, end, minimumStep, containerHeight, customRange);
}
/**
* Set a new range
* If minimumStep is provided, the step size is chosen as close as possible
* to the minimumStep but larger than minimumStep. If minimumStep is not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6 characters
* @param {Number} [start] The start date and time.
* @param {Number} [end] The end date and time.
* @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
this._start = customRange.min === undefined ? start : customRange.min;
this._end = customRange.max === undefined ? end : customRange.max;
if (this._start === this._end) {
this._start = customRange.min === undefined ? this._start - 0.75 : this._start;
this._end = customRange.max === undefined ? this._end + 1 : this._end;;
}
if (this.autoScale === true) {
this.setMinimumStep(minimumStep, containerHeight);
}
this.setFirst(customRange);
};
/**
* Automatically determine the scale that bests fits the provided minimum step
* @param {Number} [minimumStep] The minimum step size in pixels
*/
DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
// round to floor
var range = this._end - this._start;
var safeRange = range * 1.2;
var minimumStepValue = minimumStep * (safeRange / containerHeight);
var orderOfMagnitude = Math.round(Math.log(safeRange)/Math.LN10);
var minorStepIdx = -1;
var magnitudefactor = Math.pow(10,orderOfMagnitude);
var start = 0;
if (orderOfMagnitude < 0) {
start = orderOfMagnitude;
}
var solutionFound = false;
for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
magnitudefactor = Math.pow(10,i);
for (var j = 0; j < this.minorSteps.length; j++) {
var stepSize = magnitudefactor * this.minorSteps[j];
if (stepSize >= minimumStepValue) {
solutionFound = true;
minorStepIdx = j;
break;
}
}
if (solutionFound === true) {
break;
}
}
this.stepIndex = minorStepIdx;
this.scale = magnitudefactor;
this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
/**
* Round the current date to the first minor date value
* This must be executed once when the current date is set to start Date
*/
DataStep.prototype.setFirst = function(customRange) {
if (customRange === undefined) {
customRange = {};
}
var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
// if we need to align the zero's we need to make sure that there is a zero to use.
if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) {
this.marginEnd += this.marginEnd % this.step;
}
this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
this.marginRange = this.marginEnd - this.marginStart;
this.current = this.marginEnd;
};
DataStep.prototype.roundToMinor = function(value) {
var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
else {
return rounded;
}
}
/**
* Check if the there is a next step
* @return {boolean} true if the current date has not passed the end date
*/
DataStep.prototype.hasNext = function () {
return (this.current >= this.marginStart);
};
/**
* Do the next step
*/
DataStep.prototype.next = function() {
var prev = this.current;
this.current -= this.step;
// safety mechanism: if current time is still unchanged, move to the end
if (this.current === prev) {
this.current = this._end;
}
};
/**
* Do the next step
*/
DataStep.prototype.previous = function() {
this.current += this.step;
this.marginEnd += this.step;
this.marginRange = this.marginEnd - this.marginStart;
};
/**
* Get the current datetime
* @return {String} current The current date
*/
DataStep.prototype.getCurrent = function() {
// prevent round-off errors when close to zero
var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current;
var returnValue = current.toPrecision(5);
if (typeof this.formattingFunction === 'function') {
returnValue = this.formattingFunction(current);
}
if (typeof returnValue === 'number') {
return '' + returnValue;
}
else if (typeof returnValue === 'string') {
return returnValue;
}
else {
return current.toPrecision(5);
}
};
/**
* 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)
* @return {boolean} true if current date is major, else false.
*/
DataStep.prototype.isMajor = function() {
return (this.current % (this.scale * this.majorSteps[this.stepIndex]) === 0);
};
DataStep.prototype.shift = function(steps) {
if (steps < 0) {
for (let i = 0; i < -steps; i++) {
this.previous();
}
}
else if (steps > 0) {
for (let i = 0; i < steps; i++) {
this.next();
}
}
}
module.exports = DataStep;

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

@ -93,11 +93,13 @@ function Graph2d (container, items, groups, options) {
// item set
this.linegraph = new LineGraph(this.body);
this.components.push(this.linegraph);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
this.on('tap', function (event) {
me.emit('click', me.getEventProperties(event))
});
@ -122,9 +124,9 @@ function Graph2d (container, items, groups, options) {
if (items) {
this.setItems(items);
}
else {
this._redraw();
}
// draw for the first time
this._redraw();
}
// Extend the functionality from Core
@ -173,7 +175,6 @@ Graph2d.prototype.setItems = function(items) {
if (this.options.start != undefined || this.options.end != undefined) {
var start = this.options.start != undefined ? this.options.start : null;
var end = this.options.end != undefined ? this.options.end : null;
this.setWindow(start, end, {animation: false});
}
else {

+ 6
- 2
lib/timeline/Range.js View File

@ -429,10 +429,14 @@ Range.prototype._onDrag = function (event) {
this.previousDelta = delta;
this._applyRange(newStart, newEnd);
var startDate = new Date(this.start);
var endDate = new Date(this.end);
// fire a rangechange event
this.body.emitter.emit('rangechange', {
start: new Date(this.start),
end: new Date(this.end),
start: startDate,
end: endDate,
byUser: true
});
};

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

@ -121,6 +121,28 @@ function Timeline (container, items, groups, options) {
me.emit('contextmenu', me.getEventProperties(event))
};
//Single time autoscale/fit
this.fitDone = false;
this.on('changed', function (){
if (this.itemsData == null) return;
if (!me.fitDone) {
me.fitDone = true;
if (me.options.start != undefined || me.options.end != undefined) {
if (me.options.start == undefined || me.options.end == undefined) {
var range = me.getItemRange();
}
var start = me.options.start != undefined ? me.options.start : range.min;
var end = me.options.end != undefined ? me.options.end : range.max;
me.setWindow(start, end, {animation: false});
}
else {
me.fit({animation: false});
}
}
});
// apply options
if (options) {
this.setOptions(options);
@ -135,9 +157,9 @@ function Timeline (container, items, groups, options) {
if (items) {
this.setItems(items);
}
else {
this._redraw();
}
// draw for the first time
this._redraw();
}
// Extend the functionality from Core
@ -194,8 +216,6 @@ Timeline.prototype.setOptions = function (options) {
* @param {vis.DataSet | Array | null} items
*/
Timeline.prototype.setItems = function(items) {
var initialLoad = (this.itemsData == null);
// convert to type DataSet when needed
var newDataSet;
if (!items) {
@ -217,22 +237,6 @@ Timeline.prototype.setItems = function(items) {
// set items
this.itemsData = newDataSet;
this.itemSet && this.itemSet.setItems(newDataSet);
if (initialLoad) {
if (this.options.start != undefined || this.options.end != undefined) {
if (this.options.start == undefined || this.options.end == undefined) {
var range = this.getItemRange();
}
var start = this.options.start != undefined ? this.options.start : range.min;
var end = this.options.end != undefined ? this.options.end : range.max;
this.setWindow(start, end, {animation: false});
}
else {
this.fit({animation: false});
}
}
};
/**
@ -403,6 +407,7 @@ Timeline.prototype.getItemRange = function () {
// calculate the date of the left side and right side of the items given
util.forEach(this.itemSet.items, function (item) {
item.show();
item.repositionX();
var start = getStart(item);
var end = getEnd(item);

+ 52
- 103
lib/timeline/component/DataAxis.js View File

@ -1,8 +1,7 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var Component = require('./Component');
var DataStep = require('../DataStep');
var DataScale = require('./DataScale');
/**
* A horizontal time axis
* @param {Object} [options] See DataAxis.setOptions for the available
@ -19,7 +18,7 @@ function DataAxis (body, options, svg, linegraphOptions) {
orientation: 'left', // supported: 'left', 'right'
showMinorLabels: true,
showMajorLabels: true,
icons: true,
icons: false,
majorLinesOffset: 7,
minorLinesOffset: 4,
labelOffsetX: 10,
@ -30,12 +29,12 @@ function DataAxis (body, options, svg, linegraphOptions) {
alignZeros: true,
left:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
format: function (value) {return ''+Number.parseFloat(value.toPrecision(3));},
title: {text:undefined,style:undefined}
},
right:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
format: function (value) {return ''+Number.parseFloat(value.toPrecision(3));},
title: {text:undefined,style:undefined}
}
};
@ -50,7 +49,7 @@ function DataAxis (body, options, svg, linegraphOptions) {
};
this.dom = {};
this.scale= undefined;
this.range = {start:0, end:0};
this.options = util.extend({}, this.defaultOptions);
@ -68,10 +67,10 @@ function DataAxis (body, options, svg, linegraphOptions) {
this.lineOffset = 0;
this.master = true;
this.masterAxis = null;
this.svgElements = {};
this.iconsRemoved = false;
this.groups = {};
this.amountOfGroups = 0;
@ -96,6 +95,9 @@ DataAxis.prototype.addGroup = function(label, graphOptions) {
};
DataAxis.prototype.updateGroup = function(label, graphOptions) {
if (!this.groups.hasOwnProperty(label)) {
this.amountOfGroups += 1;
}
this.groups[label] = graphOptions;
};
@ -129,10 +131,9 @@ DataAxis.prototype.setOptions = function (options) {
'right',
'alignZeros'
];
util.selectiveExtend(fields, this.options, options);
util.selectiveDeepExtend(fields, this.options, options);
this.minWidth = Number(('' + this.options.width).replace("px",""));
if (redraw === true && this.dom.frame) {
this.hide();
this.show();
@ -245,11 +246,6 @@ DataAxis.prototype.hide = function() {
* @param end
*/
DataAxis.prototype.setRange = function (start, end) {
if (this.master === false && this.options.alignZeros === true && this.zeroCrossing != -1) {
if (start > 0) {
start = 0;
}
}
this.range.start = start;
this.range.end = end;
};
@ -261,7 +257,7 @@ DataAxis.prototype.setRange = function (start, end) {
DataAxis.prototype.redraw = function () {
var resized = false;
var activeGroups = 0;
// Make sure the line container adheres to the vertical scrolling.
this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px';
@ -349,105 +345,60 @@ DataAxis.prototype._redrawLabels = function () {
DOMutil.prepareElements(this.DOMelements.lines);
DOMutil.prepareElements(this.DOMelements.labels);
var orientation = this.options['orientation'];
var customRange = this.options[orientation].range != undefined? this.options[orientation].range:{};
// get the range for the slaved axis
var step;
if (this.master === false) {
var stepSize, rangeStart, rangeEnd, minimumStep;
if (this.zeroCrossing !== -1 && this.options.alignZeros === true) {
if (this.range.end > 0) {
stepSize = this.range.end / this.zeroCrossing; // size of one step
rangeStart = this.range.end - this.amountOfSteps * stepSize;
rangeEnd = this.range.end;
}
else {
// all of the range (including start) has to be done before the zero crossing.
stepSize = -1 * this.range.start / (this.amountOfSteps - this.zeroCrossing); // absolute size of a step
rangeStart = this.range.start;
rangeEnd = this.range.start + stepSize * this.amountOfSteps;
}
}
else {
rangeStart = this.range.start;
rangeEnd = this.range.end;
}
minimumStep = this.stepPixels;
//Override range with manual options:
var autoScaleEnd = true;
if (customRange.max != undefined){
this.range.end = customRange.max;
autoScaleEnd = false;
}
else {
// calculate range and step (step such that we have space for 7 characters per label)
minimumStep = this.props.majorCharHeight;
rangeStart = this.range.start;
rangeEnd = this.range.end;
var autoScaleStart = true;
if (customRange.min != undefined){
this.range.start = customRange.min;
autoScaleStart = false;
}
this.step = step = new DataStep(
rangeStart,
rangeEnd,
minimumStep,
this.scale = new DataScale(
this.range.start,
this.range.end,
autoScaleStart,
autoScaleEnd,
this.dom.frame.offsetHeight,
this.options[this.options.orientation].range,
this.options[this.options.orientation].format,
this.master === false && this.options.alignZeros // does the step have to align zeros? only if not master and the options is on
this.props.majorCharHeight,
this.options.alignZeros,
this.options[orientation].format
);
// the slave axis needs to use the same horizontal lines as the master axis.
if (this.master === true) {
this.stepPixels = ((this.dom.frame.offsetHeight) / step.marginRange) * step.step;
this.amountOfSteps = Math.ceil(this.dom.frame.offsetHeight / this.stepPixels);
if (this.master === false && this.masterAxis != undefined){
this.scale.followScale(this.masterAxis.scale);
}
else {
// align with zero
if (this.options.alignZeros === true && this.zeroCrossing !== -1) {
// distance is the amount of steps away from the zero crossing we are.
let distance = (step.current - this.zeroCrossing * step.step) / step.step;
this.step.shift(distance);
}
}
// value at the bottom of the SVG
this.valueAtBottom = step.marginEnd;
//Is updated in side-effect of _redrawLabel():
this.maxLabelSize = 0;
var y = 0; // init value
var stepIndex = 0; // init value
var isMajor = false; // init value
while (stepIndex < this.amountOfSteps) {
y = Math.round(stepIndex * this.stepPixels);
isMajor = step.isMajor();
if (stepIndex > 0 && stepIndex !== this.amountOfSteps) {
if (this.options['showMinorLabels'] && isMajor === false || this.master === false && this.options['showMinorLabels'] === true) {
this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight);
}
if (isMajor && this.options['showMajorLabels'] && this.master === true ||
this.options['showMinorLabels'] === false && this.master === false && isMajor === true) {
var lines = this.scale.getLines();
lines.forEach(
line=> {
var y = line.y;
var isMajor = line.major;
if (this.options['showMinorLabels'] && isMajor === false) {
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight);
}
if (isMajor) {
if (y >= 0) {
this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-major', this.props.majorCharHeight);
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-major', this.props.majorCharHeight);
}
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth);
if (this.master === true) {
if (isMajor) {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
}
}
// get zero crossing
if (this.master === true && step.current === 0) {
this.zeroCrossing = stepIndex;
}
step.next();
stepIndex += 1;
}
// get zero crossing if it's the last step
if (this.master === true && step.current === 0) {
this.zeroCrossing = stepIndex;
}
this.conversionFactor = this.stepPixels / step.step;
});
// Note that title is rotated, so we're using the height, not width!
var titleWidth = 0;
@ -484,13 +435,11 @@ DataAxis.prototype._redrawLabels = function () {
};
DataAxis.prototype.convertValue = function (value) {
var invertedValue = this.valueAtBottom - value;
var convertedValue = invertedValue * this.conversionFactor;
return convertedValue;
return this.scale.convertValue(value);
};
DataAxis.prototype.screenToValue = function (x) {
return this.valueAtBottom - (x / this.conversionFactor);
return this.scale.screenToValue(x);
};
/**

+ 235
- 0
lib/timeline/component/DataScale.js View File

@ -0,0 +1,235 @@
/**
* Created by ludo on 25-1-16.
*/
function DataScale(start, end, autoScaleStart, autoScaleEnd, containerHeight, majorCharHeight, zeroAlign = false, formattingFunction=false) {
this.majorSteps = [1, 2, 5, 10];
this.minorSteps = [0.25, 0.5, 1, 2];
this.customLines = null;
this.containerHeight = containerHeight;
this.majorCharHeight = majorCharHeight;
this._start = start;
this._end = end;
this.scale = 1;
this.minorStepIdx = -1;
this.magnitudefactor = 1;
this.determineScale();
this.zeroAlign = zeroAlign;
this.autoScaleStart = autoScaleStart;
this.autoScaleEnd = autoScaleEnd;
this.formattingFunction = formattingFunction;
if (autoScaleStart || autoScaleEnd) {
var me = this;
var roundToMinor = function (value) {
var rounded = value - (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]));
if (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]) > 0.5 * (me.magnitudefactor * me.minorSteps[me.minorStepIdx])) {
return rounded + (me.magnitudefactor * me.minorSteps[me.minorStepIdx]);
}
else {
return rounded;
}
};
if (autoScaleStart) {
this._start -= this.magnitudefactor * 2 * this.minorSteps[this.minorStepIdx];
this._start = roundToMinor(this._start);
}
if (autoScaleEnd) {
this._end += this.magnitudefactor * this.minorSteps[this.minorStepIdx];
this._end = roundToMinor(this._end);
}
this.determineScale();
}
}
DataScale.prototype.setCharHeight = function (majorCharHeight) {
this.majorCharHeight = majorCharHeight;
};
DataScale.prototype.setHeight = function (containerHeight) {
this.containerHeight = containerHeight;
};
DataScale.prototype.determineScale = function () {
var range = this._end - this._start;
this.scale = this.containerHeight / range;
var minimumStepValue = this.majorCharHeight / this.scale;
var orderOfMagnitude = Math.round(Math.log(range) / Math.LN10);
this.minorStepIdx = -1;
this.magnitudefactor = Math.pow(10, orderOfMagnitude);
var start = 0;
if (orderOfMagnitude < 0) {
start = orderOfMagnitude;
}
var solutionFound = false;
for (var l = start; Math.abs(l) <= Math.abs(orderOfMagnitude); l++) {
this.magnitudefactor = Math.pow(10, l);
for (var j = 0; j < this.minorSteps.length; j++) {
var stepSize = this.magnitudefactor * this.minorSteps[j];
if (stepSize >= minimumStepValue) {
solutionFound = true;
this.minorStepIdx = j;
break;
}
}
if (solutionFound === true) {
break;
}
}
};
DataScale.prototype.is_major = function (value) {
return (value % (this.magnitudefactor * this.majorSteps[this.minorStepIdx]) === 0);
};
DataScale.prototype.getStep = function(){
return this.magnitudefactor * this.minorSteps[this.minorStepIdx];
};
DataScale.prototype.getFirstMajor = function(){
var majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
return this.convertValue(this._start + ((majorStep - (this._start % majorStep)) % majorStep));
};
DataScale.prototype.formatValue = function(current) {
var returnValue = current.toPrecision(5);
if (typeof this.formattingFunction === 'function') {
returnValue = this.formattingFunction(current);
}
if (typeof returnValue === 'number') {
return '' + returnValue;
}
else if (typeof returnValue === 'string') {
return returnValue;
}
else {
return current.toPrecision(5);
}
};
DataScale.prototype.getLines = function () {
var lines = [];
var step = this.getStep();
var bottomOffset = (step - (this._start % step)) % step;
for (var i = (this._start + bottomOffset); this._end-i > 0.00001; i += step) {
if (i != this._start) { //Skip the bottom line
lines.push({major: this.is_major(i), y: this.convertValue(i), val: this.formatValue(i)});
}
}
return lines;
};
DataScale.prototype.followScale = function (other) {
var oldStepIdx = this.minorStepIdx;
var oldStart = this._start;
var oldEnd = this._end;
var me = this;
var increaseMagnitude = function () {
me.magnitudefactor *= 2;
};
var decreaseMagnitude = function () {
me.magnitudefactor /= 2;
};
if ((other.minorStepIdx <= 1 && this.minorStepIdx <= 1) || (other.minorStepIdx > 1 && this.minorStepIdx > 1)) {
//easy, no need to change stepIdx nor multiplication factor
} else if (other.minorStepIdx < this.minorStepIdx) {
//I'm 5, they are 4 per major.
this.minorStepIdx = 1;
if (oldStepIdx == 2) {
increaseMagnitude();
} else {
increaseMagnitude();
increaseMagnitude();
}
} else {
//I'm 4, they are 5 per major
this.minorStepIdx = 2;
if (oldStepIdx == 1) {
decreaseMagnitude();
} else {
decreaseMagnitude();
decreaseMagnitude();
}
}
//Get masters stats:
var lines = other.getLines();
var otherZero = other.convertValue(0);
var otherStep = other.getStep() * other.scale;
var done = false;
var count = 0;
//Loop until magnitude is correct for given constrains.
while (!done && count++ <5) {
//Get my stats:
this.scale = otherStep / (this.minorSteps[this.minorStepIdx] * this.magnitudefactor);
var newRange = this.containerHeight / this.scale;
//For the case the magnitudefactor has changed:
this._start = oldStart;
this._end = this._start + newRange;
var myOriginalZero = this._end * this.scale;
var majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
var majorOffset = this.getFirstMajor() - other.getFirstMajor();
if (this.zeroAlign) {
var zeroOffset = otherZero - myOriginalZero;
this._end += (zeroOffset / this.scale);
this._start = this._end - newRange;
} else {
if (!this.autoScaleStart) {
this._start += majorStep - (majorOffset / this.scale);
this._end = this._start + newRange;
} else {
this._start -= majorOffset / this.scale;
this._end = this._start + newRange;
}
}
if (!this.autoScaleEnd && this._end > oldEnd+0.00001) {
//Need to decrease magnitude to prevent scale overshoot! (end)
decreaseMagnitude();
done = false;
continue;
}
if (!this.autoScaleStart && this._start < oldStart-0.00001) {
if (this.zeroAlign && oldStart >= 0) {
console.warn("Can't adhere to given 'min' range, due to zeroalign");
} else {
//Need to decrease magnitude to prevent scale overshoot! (start)
decreaseMagnitude();
done = false;
continue;
}
}
if (this.autoScaleStart && this.autoScaleEnd && newRange < (oldEnd-oldStart)){
increaseMagnitude();
done = false;
continue;
}
done = true;
}
};
DataScale.prototype.convertValue = function (value) {
return this.containerHeight - ((value - this._start) * this.scale);
};
DataScale.prototype.screenToValue = function (pixels) {
return ((this.containerHeight - pixels) / this.scale) + this._start;
};
module.exports = DataScale;

+ 2
- 2
lib/timeline/component/GraphGroup.js View File

@ -16,7 +16,7 @@ var Points = require('./graph2d_types/points');
*/
function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) {
this.id = groupId;
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation', 'zIndex'];
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation', 'zIndex','excludeFromStacking', 'excludeFromLegend'];
this.options = util.selectiveBridgeObject(fields, options);
this.usingDefaultStyle = group.className === undefined;
this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
@ -65,7 +65,7 @@ GraphGroup.prototype.setZeroPosition = function (pos) {
*/
GraphGroup.prototype.setOptions = function (options) {
if (options !== undefined) {
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'excludeFromLegend', 'excludeFromStacking', 'zIndex'];
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'zIndex','excludeFromStacking', 'excludeFromLegend'];
util.selectiveDeepExtend(fields, this.options, options);
// if the group's drawPoints is a function delegate the callback to the onRender property

+ 10
- 8
lib/timeline/component/ItemSet.js View File

@ -726,6 +726,8 @@ ItemSet.prototype.setItems = function(items) {
// update the group holding all ungrouped items
this._updateUngrouped();
}
this.body.emitter.emit('_change', {queue: true});
};
/**
@ -785,7 +787,7 @@ ItemSet.prototype.setGroups = function(groups) {
// update the order of all items in each group
this._order();
this.body.emitter.emit('change', {queue: true});
this.body.emitter.emit('_change', {queue: true});
};
/**
@ -896,7 +898,7 @@ ItemSet.prototype._onUpdate = function(ids) {
this._order();
this.stackDirty = true; // force re-stacking of all items next redraw
this.body.emitter.emit('change', {queue: true});
this.body.emitter.emit('_change', {queue: true});
};
/**
@ -926,7 +928,7 @@ ItemSet.prototype._onRemove = function(ids) {
// update order
this._order();
this.stackDirty = true; // force re-stacking of all items next redraw
this.body.emitter.emit('change', {queue: true});
this.body.emitter.emit('_change', {queue: true});
}
};
@ -996,7 +998,7 @@ ItemSet.prototype._onAddGroups = function(ids) {
}
});
this.body.emitter.emit('change', {queue: true});
this.body.emitter.emit('_change', {queue: true});
};
/**
@ -1017,7 +1019,7 @@ ItemSet.prototype._onRemoveGroups = function(ids) {
this.markDirty();
this.body.emitter.emit('change', {queue: true});
this.body.emitter.emit('_change', {queue: true});
};
/**
@ -1393,7 +1395,7 @@ ItemSet.prototype._onDrag = function (event) {
}.bind(this));
this.stackDirty = true; // force re-stacking of all items next redraw
this.body.emitter.emit('change');
this.body.emitter.emit('_change');
}
};
@ -1444,7 +1446,7 @@ ItemSet.prototype._onDragEnd = function (event) {
// force re-stacking of all items next redraw
me.stackDirty = true;
me.body.emitter.emit('change');
me.body.emitter.emit('_change');
});
}
else {
@ -1461,7 +1463,7 @@ ItemSet.prototype._onDragEnd = function (event) {
props.item.setData(props.data);
me.stackDirty = true; // force re-stacking of all items next redraw
me.body.emitter.emit('change');
me.body.emitter.emit('_change');
}
});
}

+ 3
- 2
lib/timeline/component/Legend.js View File

@ -8,7 +8,7 @@ var Component = require('./Component');
function Legend(body, options, side, linegraphOptions) {
this.body = body;
this.defaultOptions = {
enabled: true,
enabled: false,
icons: true,
iconSize: 20,
iconSpacing: 6,
@ -18,9 +18,10 @@ function Legend(body, options, side, linegraphOptions) {
},
right: {
visible: true,
position: 'top-left' // top/bottom - left,center,right
position: 'top-right' // top/bottom - left,center,right
}
}
this.side = side;
this.options = util.extend({},this.defaultOptions);
this.linegraphOptions = linegraphOptions;

+ 104
- 135
lib/timeline/component/LineGraph.js View File

@ -50,40 +50,8 @@ function LineGraph(body, options) {
size: 6,
style: 'square' // square, circle
},
dataAxis: {
showMinorLabels: true,
showMajorLabels: true,
icons: false,
width: '40px',
visible: true,
alignZeros: true,
left: {
range: {min: undefined, max: undefined},
format: function (value) {
return value;
},
title: {text: undefined, style: undefined}
},
right: {
range: {min: undefined, max: undefined},
format: function (value) {
return value;
},
title: {text: undefined, style: undefined}
}
},
legend: {
enabled: false,
icons: true,
left: {
visible: true,
position: 'top-left' // top/bottom - left,right
},
right: {
visible: true,
position: 'top-right' // top/bottom - left,right
}
},
dataAxis: {}, //Defaults are done on DataAxis level
legend: {}, //Defaults are done on Legend level
groups: {
visibility: {}
}
@ -98,6 +66,7 @@ function LineGraph(body, options) {
this.abortedGraphUpdate = false;
this.updateSVGheight = false;
this.updateSVGheightOnResize = false;
this.forceGraphUpdate = true;
var me = this;
this.itemsData = null; // DataSet
@ -137,18 +106,18 @@ function LineGraph(body, options) {
this.svgElements = {};
this.setOptions(options);
this.groupsUsingDefaultStyles = [0];
this.COUNTER = 0;
this.body.emitter.on('rangechanged', function () {
me.lastStart = me.body.range.start;
me.svg.style.left = util.option.asSize(-me.props.width);
me.redraw.call(me, true);
me.forceGraphUpdate = true;
//Is this local redraw necessary? (Core also does a change event!)
me.redraw.call(me);
});
// create the HTML DOM
this._create();
this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
this.body.emitter.emit('change');
}
LineGraph.prototype = new Component();
@ -190,7 +159,7 @@ LineGraph.prototype._create = function () {
LineGraph.prototype.setOptions = function (options) {
if (options) {
var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups'];
if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) {
if (options.graphHeight === undefined && options.height !== undefined) {
this.updateSVGheight = true;
this.updateSVGheightOnResize = true;
}
@ -242,8 +211,9 @@ LineGraph.prototype.setOptions = function (options) {
}
// this is used to redraw the graph if the visibility of the groups is changed.
if (this.dom.frame) {
this.redraw(true);
if (this.dom.frame) { //not on initial run?
this.forceGraphUpdate=true;
this.body.emitter.emit("_change",{queue: true});
}
};
@ -363,7 +333,6 @@ LineGraph.prototype.setGroups = function (groups) {
LineGraph.prototype._onUpdate = function (ids) {
this._updateAllGroupData();
this.redraw(true);
};
LineGraph.prototype._onAdd = function (ids) {
this._onUpdate(ids);
@ -373,7 +342,6 @@ LineGraph.prototype._onRemove = function (ids) {
};
LineGraph.prototype._onUpdateGroups = function (groupIds) {
this._updateAllGroupData();
this.redraw(true);
};
LineGraph.prototype._onAddGroups = function (groupIds) {
this._onUpdateGroups(groupIds);
@ -388,7 +356,8 @@ LineGraph.prototype._onRemoveGroups = function (groupIds) {
for (var i = 0; i < groupIds.length; i++) {
this._removeGroup(groupIds[i]);
}
this.redraw(true);
this.forceGraphUpdate = true;
this.body.emitter.emit("_change",{queue: true});
};
/**
@ -436,10 +405,16 @@ LineGraph.prototype._updateGroup = function (group, groupId) {
if (this.groups[groupId].options.yAxisOrientation == 'right') {
this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
this.legendRight.updateGroup(groupId, this.groups[groupId]);
//If yAxisOrientation changed, clean out the group from the other axis.
this.yAxisLeft.removeGroup(groupId);
this.legendLeft.removeGroup(groupId);
}
else {
this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
this.legendLeft.updateGroup(groupId, this.groups[groupId]);
//If yAxisOrientation changed, clean out the group from the other axis.
this.yAxisRight.removeGroup(groupId);
this.legendRight.removeGroup(groupId);
}
}
this.legendLeft.redraw();
@ -480,7 +455,6 @@ LineGraph.prototype._updateAllGroupData = function () {
var extended = util.bridgeObject(item);
extended.x = util.convert(item.x, 'Date');
extended.orginalY = item.y; //real Y
// typecast all items to numbers. Takes around 10ms for 500.000 items
extended.y = Number(item.y);
var index= groupsContent[groupId].length - groupCounts[groupId]--;
@ -516,6 +490,8 @@ LineGraph.prototype._updateAllGroupData = function () {
}
}
}
this.forceGraphUpdate = true;
this.body.emitter.emit("_change",{queue: true});
}
};
@ -523,7 +499,7 @@ LineGraph.prototype._updateAllGroupData = function () {
* Redraw the component, mandatory function
* @return {boolean} Returns true if the component is resized
*/
LineGraph.prototype.redraw = function (forceGraphUpdate) {
LineGraph.prototype.redraw = function () {
var resized = false;
// calculate actual size and position
@ -532,11 +508,6 @@ LineGraph.prototype.redraw = function (forceGraphUpdate) {
- this.body.domProps.border.top
- this.body.domProps.border.bottom;
// update the graph if there is no lastWidth or with, used for the initial draw
if (this.lastWidth === undefined && this.props.width) {
forceGraphUpdate = true;
}
// check if this component is resized
resized = this._isResized() || resized;
@ -571,8 +542,9 @@ LineGraph.prototype.redraw = function (forceGraphUpdate) {
}
// zoomed is here to ensure that animations are shown correctly.
if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) {
if (resized == true || zoomed == true || this.abortedGraphUpdate == true || this.forceGraphUpdate == true) {
resized = this._updateGraph() || resized;
this.forceGraphUpdate = false;
}
else {
// move the whole svg while dragging
@ -586,7 +558,6 @@ LineGraph.prototype.redraw = function (forceGraphUpdate) {
}
}
}
this.legendLeft.redraw();
this.legendRight.redraw();
return resized;
@ -653,101 +624,97 @@ LineGraph.prototype._updateGraph = function () {
this._getYRanges(groupIds, groupsData, groupRanges);
// update the Y axis first, we use this data to draw at the correct Y points
// changeCalled is required to clean the SVG on a change emit.
changeCalled = this._updateYAxis(groupIds, groupRanges);
var MAX_CYCLES = 5;
if (changeCalled == true && this.COUNTER < MAX_CYCLES) {
// at changeCalled, abort this update cycle as the graph needs another update with new Width input from the Redraw container.
// Cleanup SVG elements on abort.
if (changeCalled == true) {
DOMutil.cleanupElements(this.svgElements);
this.abortedGraphUpdate = true;
this.COUNTER++;
this.body.emitter.emit('change');
return true;
}
else {
if (this.COUNTER > MAX_CYCLES) {
console.log("WARNING: there may be an infinite loop in the _updateGraph emitter cycle.");
}
this.COUNTER = 0;
this.abortedGraphUpdate = false;
// With the yAxis scaled correctly, use this to get the Y values of the points.
var below = undefined;
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (this.options.stack === true && this.options.style === 'line') {
if (group.options.excludeFromStacking == undefined || !group.options.excludeFromStacking) {
if (below != undefined) {
this._stack(groupsData[group.id], groupsData[below.id]);
if (group.options.shaded.enabled == true && group.options.shaded.orientation !== "group"){
if (group.options.shaded.orientation == "top" && below.options.shaded.orientation !== "group"){
below.options.shaded.orientation="group";
below.options.shaded.groupId=group.id;
} else {
group.options.shaded.orientation="group";
group.options.shaded.groupId=below.id;
}
this.abortedGraphUpdate = false;
// With the yAxis scaled correctly, use this to get the Y values of the points.
var below = undefined;
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (this.options.stack === true && this.options.style === 'line') {
if (group.options.excludeFromStacking == undefined || !group.options.excludeFromStacking) {
if (below != undefined) {
this._stack(groupsData[group.id], groupsData[below.id]);
if (group.options.shaded.enabled == true && group.options.shaded.orientation !== "group"){
if (group.options.shaded.orientation == "top" && below.options.shaded.orientation !== "group"){
below.options.shaded.orientation="group";
below.options.shaded.groupId=group.id;
} else {
group.options.shaded.orientation="group";
group.options.shaded.groupId=below.id;
}
}
below = group;
}
below = group;
}
this._convertYcoordinates(groupsData[groupIds[i]], group);
}
this._convertYcoordinates(groupsData[groupIds[i]], group);
}
//Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines.
var paths = {};
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (group.options.style === 'line' && group.options.shaded.enabled == true) {
var dataset = groupsData[groupIds[i]];
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(dataset, group);
}
if (group.options.shaded.orientation === "group") {
var subGroupId = group.options.shaded.groupId;
if (groupIds.indexOf(subGroupId) === -1) {
console.log(group.id + ": Unknown shading group target given:" + subGroupId);
continue;
}
if (!paths.hasOwnProperty(subGroupId)) {
paths[subGroupId] = Lines.calcPath(groupsData[subGroupId], this.groups[subGroupId]);
}
Lines.drawShading(paths[groupIds[i]], group, paths[subGroupId], this.framework);
//Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines.
var paths = {};
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (group.options.style === 'line' && group.options.shaded.enabled == true) {
var dataset = groupsData[groupIds[i]];
if (dataset == null || dataset.length == 0) {
continue;
}
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(dataset, group);
}
if (group.options.shaded.orientation === "group") {
var subGroupId = group.options.shaded.groupId;
if (groupIds.indexOf(subGroupId) === -1) {
console.log(group.id + ": Unknown shading group target given:" + subGroupId);
continue;
}
else {
Lines.drawShading(paths[groupIds[i]], group, undefined, this.framework);
if (!paths.hasOwnProperty(subGroupId)) {
paths[subGroupId] = Lines.calcPath(groupsData[subGroupId], this.groups[subGroupId]);
}
Lines.drawShading(paths[groupIds[i]], group, paths[subGroupId], this.framework);
}
else {
Lines.drawShading(paths[groupIds[i]], group, undefined, this.framework);
}
}
}
// draw the groups, calculating paths if still necessary.
Bars.draw(groupIds, groupsData, this.framework);
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (groupsData[groupIds[i]].length > 0) {
switch (group.options.style) {
case "line":
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(groupsData[groupIds[i]], group);
}
Lines.draw(paths[groupIds[i]], group, this.framework);
//explicit no break;
case "point":
//explicit no break;
case "points":
if (group.options.style == "point" || group.options.style == "points" || group.options.drawPoints.enabled == true) {
Points.draw(groupsData[groupIds[i]], group, this.framework);
}
break;
case "bar":
// bar needs to be drawn enmasse
//explicit no break
default:
//do nothing...
}
// draw the groups, calculating paths if still necessary.
Bars.draw(groupIds, groupsData, this.framework);
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (groupsData[groupIds[i]].length > 0) {
switch (group.options.style) {
case "line":
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(groupsData[groupIds[i]], group);
}
Lines.draw(paths[groupIds[i]], group, this.framework);
//explicit no break;
case "point":
//explicit no break;
case "points":
if (group.options.style == "point" || group.options.style == "points" || group.options.drawPoints.enabled == true) {
Points.draw(groupsData[groupIds[i]], group, this.framework);
}
break;
case "bar":
// bar needs to be drawn enmasse
//explicit no break
default:
//do nothing...
}
}
}
}
}
@ -824,8 +791,11 @@ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate,
var itemsData = group.getItems();
// optimization for sorted data
if (group.options.sort == true) {
var first = Math.max(0, util.binarySearchValue(itemsData, minDate, 'x', 'before'));
var last = Math.min(itemsData.length, util.binarySearchValue(itemsData, maxDate, 'x', 'after')+1);
var dateComparator = function (a, b) {
return a.getTime() == b.getTime() ? 0 : a < b ? -1 : 1
};
var first = Math.max(0, util.binarySearchValue(itemsData, minDate, 'x', 'before', dateComparator));
var last = Math.min(itemsData.length, util.binarySearchValue(itemsData, maxDate, 'x', 'after', dateComparator) + 1);
if (last <= 0) {
last = itemsData.length;
}
@ -990,6 +960,8 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
this.yAxisRight.drawIcons = false;
}
this.yAxisRight.master = !yAxisLeftUsed;
this.yAxisRight.masterAxis = this.yAxisLeft;
if (this.yAxisRight.master == false) {
if (yAxisRightUsed == true) {
this.yAxisLeft.lineOffset = this.yAxisRight.width;
@ -999,9 +971,6 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
}
resized = this.yAxisLeft.redraw() || resized;
this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels;
this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing;
this.yAxisRight.amountOfSteps = this.yAxisLeft.amountOfSteps;
resized = this.yAxisRight.redraw() || resized;
}
else {

+ 3
- 15
lib/timeline/component/graph2d_types/bar.js View File

@ -94,7 +94,7 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
// plot barchart
for (i = 0; i < combinedData.length; i++) {
group = framework.groups[combinedData[i].groupId];
var minWidth = 0.1 * group.options.barChart.width;
var minWidth = group.options.barChart.minWidth != undefined ? group.options.barChart.minWidth : 0.1 * group.options.barChart.width;
key = combinedData[i].screen_x;
var heightOffset = 0;
@ -102,9 +102,6 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
if (i + 1 < combinedData.length) {
coreDistance = Math.abs(combinedData[i + 1].screen_x - key);
}
if (i > 0) {
coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].screen_x - key));
}
drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
}
else {
@ -113,13 +110,10 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
if (nextKey < combinedData.length) {
coreDistance = Math.abs(combinedData[nextKey].screen_x - key);
}
if (prevKey > 0) {
coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].screen_x - key));
}
drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
intersections[key].resolved += 1;
if (group.options.stack === true) {
if (group.options.stack === true && group.options.excludeFromStacking !== true) {
if (combinedData[i].screen_y < group.zeroPosition) {
heightOffset = intersections[key].accumulatedNegative;
intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].screen_y;
@ -132,12 +126,6 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
else if (group.options.barChart.sideBySide === true) {
drawData.width = drawData.width / intersections[key].amount;
drawData.offset += (intersections[key].resolved) * drawData.width - (0.5 * drawData.width * (intersections[key].amount + 1));
if (group.options.barChart.align === 'left') {
drawData.offset -= 0.5 * drawData.width;
}
else if (group.options.barChart.align === 'right') {
drawData.offset += 0.5 * drawData.width;
}
}
}
DOMutil.drawBar(combinedData[i].screen_x + drawData.offset, combinedData[i].screen_y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].screen_y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style);
@ -201,7 +189,7 @@ Bargraph._getDataIntersections = function (intersections, combinedData) {
Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) {
var width, offset;
if (coreDistance < group.options.barChart.width && coreDistance > 0) {
width = coreDistance < minWidth ? minWidth : coreDistance;
width = coreDistance < minWidth ? minWidth : coreDistance
offset = 0; // recalculate offset with the new width;
if (group.options.barChart.align === 'left') {

+ 2
- 0
lib/timeline/optionsGraph2d.js View File

@ -40,6 +40,7 @@ let allOptions = {
style: {string:['line','bar','points']}, // line, bar
barChart: {
width: {number},
minWidth: {number},
sideBySide: {boolean},
align: {string:['left','center','right']},
__type__: {object}
@ -179,6 +180,7 @@ let configureOptions = {
style: ['line','bar','points'], // line, bar
barChart: {
width: [50,5,100,5],
minWidth: [50,5,100,5],
sideBySide: false,
align: ['left','center','right'] // left, center, right
},

+ 13
- 8
lib/util.js View File

@ -1308,13 +1308,13 @@ exports.mergeOptions = function (mergeTarget, options, option, allowDeletion = f
* this function will then iterate in both directions over this sorted list to find all visible items.
*
* @param {Item[]} orderedItems | Items ordered by start
* @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher
* @param {function} comparator | -1 is lower, 0 is equal, 1 is higher
* @param {String} field
* @param {String} field2
* @returns {number}
* @private
*/
exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) {
exports.binarySearchCustom = function (orderedItems, comparator, field, field2) {
var maxIterations = 10000;
var iteration = 0;
var low = 0;
@ -1326,7 +1326,7 @@ exports.binarySearchCustom = function (orderedItems, searchFunction, field, fiel
var item = orderedItems[middle];
var value = (field2 === undefined) ? item[field] : item[field][field2];
var searchResult = searchFunction(value);
var searchResult = comparator(value);
if (searchResult == 0) { // jihaa, found a visible item!
return middle;
}
@ -1352,16 +1352,21 @@ exports.binarySearchCustom = function (orderedItems, searchFunction, field, fiel
* @param {{start: number, end: number}} target
* @param {String} field
* @param {String} sidePreference 'before' or 'after'
* @param {function} comparator an optional comparator, returning -1,0,1 for <,==,>.
* @returns {number}
* @private
*/
exports.binarySearchValue = function (orderedItems, target, field, sidePreference) {
exports.binarySearchValue = function (orderedItems, target, field, sidePreference, comparator) {
var maxIterations = 10000;
var iteration = 0;
var low = 0;
var high = orderedItems.length - 1;
var prevValue, value, nextValue, middle;
var comparator = comparator != undefined ? comparator : function (a, b) {
return a == b ? 0 : a < b ? -1 : 1
};
while (low <= high && iteration < maxIterations) {
// get a new guess
middle = Math.floor(0.5 * (high + low));
@ -1369,17 +1374,17 @@ exports.binarySearchValue = function (orderedItems, target, field, sidePreferenc
value = orderedItems[middle][field];
nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field];
if (value == target) { // we found the target
if (comparator(value, target) == 0) { // we found the target
return middle;
}
else if (prevValue < target && value > target) { // target is in between of the previous and the current
else if (comparator(prevValue, target) < 0 && comparator(value, target) > 0) { // target is in between of the previous and the current
return sidePreference == 'before' ? Math.max(0, middle - 1) : middle;
}
else if (value < target && nextValue > target) { // target is in between of the current and the next
else if (comparator(value, target) < 0 && comparator(nextValue, target) > 0) { // target is in between of the current and the next
return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1);
}
else { // didnt find the target, we need to change our boundaries.
if (value < target) { // it is too small --> increase low
if (comparator(value, target) < 0) { // it is too small --> increase low
low = middle + 1;
}
else { // it is too big --> decrease high

+ 1
- 1
misc/how_to_publish.md View File

@ -5,7 +5,7 @@ This document describes how to publish vis.js.
## Build
- Change the version number of the library in both `package.json` and `bower.json`.
- Change the version number of the library in `package.json`.
- Open `HISTORY.md`, write down the changes, version number, and release date.
- Build the library by running:

+ 3
- 3
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "4.12.0",
"version": "4.13.0",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"license": "(Apache-2.0 OR MIT)",
@ -21,7 +21,7 @@
"network",
"browser"
],
"main": "./dist/vis.min.js",
"main": "./dist/vis.js",
"scripts": {
"test": "mocha",
"build": "gulp",
@ -34,7 +34,7 @@
"hammerjs": "^2.0.6",
"keycharm": "^0.2.0",
"moment": "^2.10.2",
"propagating-hammerjs": "^1.4.4",
"propagating-hammerjs": "^1.4.5",
"uuid": "^2.0.1",
"babel": "^5.1.11",
"babel-loader": "^5.0.0",

+ 42
- 53
test/networkTest.html View File

@ -7,7 +7,7 @@
<link href="../dist/vis.css" rel="stylesheet" type="text/css"/>
<style type="text/css">
#network{
width: 1200px;
width: 1900px;
height: 800px;
border: 1px solid lightgray;
}
@ -17,63 +17,52 @@
<h1>Network Test</h1>
<div id="network"></div>
<script>
var nodes = new vis.DataSet([
{id: 1, label: '1'},
{id: 2, label: '2'},
{id: 3, label: '3'},
{id: 4, label: '4'},
{id: 5, label: '5'},
{id: 6, label: '6'},
{id: 7, label: '7'},
]);
var edges = new vis.DataSet([
{id: "e1", from: 2, to: 1, label: "e1"},
{id: "e2", from: 3, to: 1, label: "e2"},
{id: "e3", from: 4, to: 1, label: "e3"},
{id: "e4", from: 5, to: 1, label: "e4"},
{id: "e5", from: 6, to: 1, label: "e5"},
{id: "e6", from: 2, to: 7, label: "e6"},
{id: "e7", from: 3, to: 7, label: "e7"},
{id: "e8", from: 4, to: 7, label: "e8"},
{id: "e9", from: 5, to: 7, label: "e9"},
{id: "e10", from: 6, to: 7, label: "e10"},
]);
var network = null;
// create a network
var container = document.getElementById('network');
var data = {
nodes: nodes,
edges: edges
};
var options = {
layout: {randomSeed: 8},
edges: {
arrows: {
to: {
scaleFactor: 0.5
var idmap = {};
for (var i = 0; i < nodes.length; i++) {
nodes[i].label = i;
idmap[nodes[i].id] = i;
nodes[i].id = i;
}
for (var i = 0; i < edges.length; i++) {
edges[i].from = idmap[edges[i].from];
edges[i].to = idmap[edges[i].to];
edges[i].id = 'e'+i;
}
console.log(JSON.stringify(nodes), JSON.stringify(edges));
// randomly create some nodes and edges
var data = {
nodes: nodes,
edges: edges
}
// create a network
var status = document.getElementById('status');
var container = document.getElementById('network');
var options = {
layout: {
hierarchical: {
direction: "UD",
sortMethod: "directed"
}
},
font: {
align: "middle"
physics: {
enabled: false,
stabilization:false
}
}
};
var network = new vis.Network(container, data, options);
var clusterOptionsByData = {
joinCondition: function(node) {
if (node.id > 1 && node.id < 7)
return true;
return false;
},
clusterNodeProperties: {id:"c1", label:'c1'}
};
network.cluster(clusterOptionsByData);
setTimeout(function() {
network.setOptions({layout: { hierarchical: { direction: "LR" }}});
},2000)
network = new vis.Network(container, data, options);
network.on("stabilizationProgress", function(params) {
var prog = params.iterations/params.total;
status.innerText = Math.round(prog*100)+'%';
});
network.on("stabilizationIterationsDone", function() {
status.innerText = "stabilized";
});
</script>
</body>
</html>

Loading…
Cancel
Save