Browse Source

Merge branch 'develop' of https://github.com/almende/vis into develop

revert-3409-performance
Yotam Berkowitz 7 years ago
parent
commit
545fb18b8f
51 changed files with 1345 additions and 597 deletions
  1. +2
    -0
      docs/css/style.css
  2. +18
    -0
      docs/graph3d/index.html
  3. +45
    -2
      docs/timeline/index.html
  4. +3
    -1
      examples/graph3d/08_dot_cloud_size.html
  5. +25
    -17
      examples/network/other/saveAndLoad.html
  6. +8
    -0
      examples/timeline/interaction/eventListeners.html
  7. +16
    -1
      examples/timeline/items/tooltip.html
  8. +111
    -0
      examples/timeline/styling/weekStyling.html
  9. +17
    -9
      lib/graph3d/Graph3d.js
  10. +2
    -0
      lib/graph3d/Settings.js
  11. +160
    -0
      lib/network/CachedImage.js
  12. +43
    -30
      lib/network/Images.js
  13. +18
    -18
      lib/network/Network.js
  14. +26
    -20
      lib/network/modules/Canvas.js
  15. +2
    -2
      lib/network/modules/Clustering.js
  16. +2
    -2
      lib/network/modules/EdgesHandler.js
  17. +2
    -2
      lib/network/modules/InteractionHandler.js
  18. +249
    -137
      lib/network/modules/LayoutEngine.js
  19. +4
    -4
      lib/network/modules/NodesHandler.js
  20. +8
    -8
      lib/network/modules/PhysicsEngine.js
  21. +8
    -9
      lib/network/modules/SelectionHandler.js
  22. +1
    -1
      lib/network/modules/View.js
  23. +5
    -5
      lib/network/modules/components/Edge.js
  24. +59
    -33
      lib/network/modules/components/Node.js
  25. +2
    -8
      lib/network/modules/components/nodes/shapes/CircularImage.js
  26. +0
    -4
      lib/network/modules/components/nodes/shapes/Diamond.js
  27. +0
    -4
      lib/network/modules/components/nodes/shapes/Dot.js
  28. +1
    -1
      lib/network/modules/components/nodes/shapes/Ellipse.js
  29. +2
    -8
      lib/network/modules/components/nodes/shapes/Image.js
  30. +0
    -4
      lib/network/modules/components/nodes/shapes/Square.js
  31. +0
    -4
      lib/network/modules/components/nodes/shapes/Star.js
  32. +0
    -4
      lib/network/modules/components/nodes/shapes/Triangle.js
  33. +0
    -4
      lib/network/modules/components/nodes/shapes/TriangleDown.js
  34. +39
    -42
      lib/network/modules/components/nodes/util/CircleImageBase.js
  35. +7
    -4
      lib/network/modules/components/nodes/util/ShapeBase.js
  36. +173
    -121
      lib/network/modules/components/shared/Label.js
  37. +3
    -1
      lib/network/shapes.js
  38. +1
    -1
      lib/shared/Configurator.js
  39. +2
    -2
      lib/timeline/Graph2d.js
  40. +6
    -2
      lib/timeline/Range.js
  41. +55
    -4
      lib/timeline/TimeStep.js
  42. +23
    -5
      lib/timeline/Timeline.js
  43. +2
    -2
      lib/timeline/component/BackgroundGroup.js
  44. +30
    -24
      lib/timeline/component/Group.js
  45. +23
    -38
      lib/timeline/component/ItemSet.js
  46. +3
    -1
      lib/timeline/component/LineGraph.js
  47. +5
    -1
      lib/timeline/component/TimeAxis.js
  48. +20
    -5
      lib/timeline/component/item/Item.js
  49. +7
    -1
      lib/timeline/optionsTimeline.js
  50. +106
    -0
      misc/labels.md
  51. +1
    -1
      package.json

+ 2
- 0
docs/css/style.css View File

@ -106,6 +106,8 @@ table.events td:nth-child(2) {
pre {
margin: 20px 0;
white-space: pre-wrap;
max-width: 100%;
}
a code {

+ 18
- 0
docs/graph3d/index.html View File

@ -386,6 +386,24 @@ var options = {
<td>Ratio of the size of the dots with respect to the width of the graph.</td>
</tr>
<tr>
<td>dotSizeMinFraction</td>
<td>number</td>
<td>0.5</td>
<td>Size of minimum-value dot as a fraction of dotSizeRatio.
Applicable when using style <code>dot-size</code>.</td>
</td>
</tr>
<tr>
<td>dotSizeMaxFraction</td>
<td>number</td>
<td>2.5</td>
<td>Size of maximum-value dot as a fraction of dotSizeRatio.
Applicable when using style <code>dot-size</code>.</td>
</td>
</tr>
<tr>
<td>gridColor</td>
<td>string</td>

+ 45
- 2
docs/timeline/index.html View File

@ -612,6 +612,7 @@ function (option, path) {
hour: 'HH:mm',
weekday: 'ddd D',
day: 'D',
week: 'w',
month: 'MMM',
year: 'YYYY'
},
@ -622,6 +623,7 @@ function (option, path) {
hour: 'ddd D MMMM',
weekday: 'MMMM YYYY',
day: 'MMMM YYYY',
week: 'MMMM YYYY',
month: 'YYYY',
year: ''
}
@ -1024,6 +1026,13 @@ function (option, path) {
visible.</td>
</tr>
<tr>
<td>showTooltips</td>
<td>boolean</td>
<td><code>true</code></td>
<td>If true, items with titles will display a tooltip. If false, item tooltips are prevented from showing.</td>
</tr>
<tr>
<td>stack</td>
<td>boolean</td>
@ -1044,7 +1053,7 @@ function (option, path) {
<td>function</td>
<td>When moving items on the Timeline, they will be snapped to nice dates like full hours or days, depending on the current scale. The <code>snap</code> function can be replaced with a custom function, or can be set to <code>null</code> to disable snapping. The signature of the snap function is:
<pre class="prettyprint lang-js">function snap(date: Date, scale: string, step: number) : Date or number</pre>
The parameter <code>scale</code> can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'day, 'month, or 'year'. The parameter <code>step</code> is a number like 1, 2, 4, 5.
The parameter <code>scale</code> can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'week', 'day, 'month, or 'year'. The parameter <code>step</code> is a number like 1, 2, 4, 5.
</td>
</tr>
@ -1088,10 +1097,11 @@ function (option, path) {
<td class="indent">timeAxis.scale</td>
<td>String</td>
<td>none</td>
<td>Set a fixed scale for the time axis of the Timeline. Choose from <code>'millisecond'</code>, <code>'second'</code>, <code>'minute'</code>, <code>'hour'</code>, <code>'weekday'</code>, <code>'day'</code>, <code>'month'</code>, <code>'year'</code>. Example usage:
<td>Set a fixed scale for the time axis of the Timeline. Choose from <code>'millisecond'</code>, <code>'second'</code>, <code>'minute'</code>, <code>'hour'</code>, <code>'weekday'</code>, <code>'week'</code>, <code>'day'</code>, <code>'month'</code>, <code>'year'</code>. Example usage:
<pre class="prettyprint lang-js">var options = {
timeAxis: {scale: 'minute', step: 5}
}</pre>
<p>Note: The 'week' scale only works properly when <a href="#Localization">locales</a> are enabled.</p>
</td>
</tr>
@ -1581,6 +1591,33 @@ timeline.off('select', onSelect);
</td>
</tr>
<tr>
<td>mouseDown</td>
<td>
Passes a properties object as returned by the method <a href="#getEventProperties"><code>Timeline.getEventProperties(event)</code></a>.
</td>
<td>Fired when the mouse down event is triggered over a timeline element.
</td>
</tr>
<tr>
<td>mouseUp</td>
<td>
Passes a properties object as returned by the method <a href="#getEventProperties"><code>Timeline.getEventProperties(event)</code></a>.
</td>
<td>Fired when the mouse up event is triggered over a timeline element.
</td>
</tr>
<tr>
<td>mouseMove</td>
<td>
Passes a properties object as returned by the method <a href="#getEventProperties"><code>Timeline.getEventProperties(event)</code></a>.
</td>
<td>Fired when the mouse is moved over a timeline element.
</td>
</tr>
<tr>
<td>groupDragged</td>
<td>
@ -2023,6 +2060,9 @@ var options = {
<tr>
<td>Days</td><td><code>vis-day1</code>, <code>vis-day2</code>, ..., <code>vis-day31</code></td>
</tr>
<tr>
<td>Week</td><td><code>vis-week1</code>, <code>vis-week2</code>, ..., <code>vis-week53</code></td>
</tr>
<tr>
<td>Months</td><td><code>vis-january</code>, <code>vis-february</code>, <code>vis-march</code>, <code>vis-april</code>, <code>vis-may</code>, <code>vis-june</code>, <code>vis-july</code>, <code>vis-august</code>, <code>vis-september</code>, <code>vis-october</code>, <code>vis-november</code>, <code>vis-december</code></td>
</tr>
@ -2030,6 +2070,9 @@ var options = {
<td>Years</td><td><code>vis-year2014</code>, <code>vis-year2015</code>, ...</td>
</tr>
</table>
<p>
Note: the 'week' scale is not included in the automatic zoom levels as its scale is not a direct logical successor of 'days' nor a logical predecessor of 'months'
</p>
<p>Examples:</p>

+ 3
- 1
examples/graph3d/08_dot_cloud_size.html View File

@ -49,7 +49,9 @@
horizontal: -0.54,
vertical: 0.5,
distance: 1.6
}
},
dotSizeMinFraction: 0.5,
dotSizeMaxFraction: 2.5
};
// create our graph

+ 25
- 17
examples/network/other/saveAndLoad.html View File

@ -71,15 +71,6 @@
draw();
}
function addContextualInformation(elem, index, array) {
addId(elem, index);
addConnections(elem, index);
}
function addId(elem, index) {
elem.id = index;
}
function addConnections(elem, index) {
// need to replace this with a tree of the network, then get child direct children of the element
elem.connections = network.getConnectedNodes(index);
@ -107,7 +98,7 @@
var nodes = objectToArray(network.getPositions());
nodes.forEach(addContextualInformation);
nodes.forEach(addConnections);
// pretty print node data
var exportValue = JSON.stringify(nodes, undefined, 2);
@ -141,30 +132,47 @@
return new vis.DataSet(networkNodes);
}
function getNodeById(data, id) {
for (var n = 0; n < data.length; n++) {
if (data[n].id == id) { // double equals since id can be numeric or string
return data[n];
}
};
throw 'Can not find id \'' + id + '\' in data';
}
function getEdgeData(data) {
var networkEdges = [];
data.forEach(function(node, index, array) {
data.forEach(function(node) {
// add the connection
node.connections.forEach(function(connId, cIndex, conns) {
networkEdges.push({from: node.id, to: connId});
let cNode = getNodeById(data, connId);
var elementConnections = array[connId].connections;
var elementConnections = cNode.connections;
// remove the connection from the other node to prevent duplicate connections
var duplicateIndex = elementConnections.findIndex(function(connection) {
connection === node.id;
return connection == node.id; // double equals since id can be numeric or string
});
elementConnections = elementConnections.splice(0, duplicateIndex - 1).concat(elementConnections.splice(duplicateIndex + 1, elementConnections.length))
});
if (duplicateIndex != -1) {
elementConnections.splice(duplicateIndex, 1);
};
});
});
return new vis.DataSet(networkEdges);
}
function objectToArray(obj) {
return Object.keys(obj).map(function (key) { return obj[key]; });
return Object.keys(obj).map(function (key) {
obj[key].id = key;
return obj[key];
});
}
function resizeExportArea() {
@ -174,4 +182,4 @@
init();
</script>
</body>
</html>
</html>

+ 8
- 0
examples/timeline/interaction/eventListeners.html View File

@ -72,6 +72,14 @@
logEvent('contextmenu', properties);
});
timeline.on('mouseDown', function (properties) {
logEvent('mouseDown', properties);
});
timeline.on('mouseUp', function (properties) {
logEvent('mouseUp', properties);
});
// other possible events:
// timeline.on('mouseOver', function (properties) {

+ 16
- 1
examples/timeline/items/tooltip.html View File

@ -12,7 +12,7 @@
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
@ -38,6 +38,12 @@
<div id="tooltips-cap"></div>
<p>
Disable item tooltips.
</p>
<div id="tooltips-hide"></div>
<script type="text/javascript">
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
@ -78,6 +84,15 @@
var timelineCap = new vis.Timeline(document.getElementById('tooltips-cap'),
items, cap_options);
// Hide options
var hide_options = {
showTooltips: false
}
var timelineHide = new vis.Timeline(document.getElementById('tooltips-hide'),
items, hide_options);
</script>
</body>

+ 111
- 0
examples/timeline/styling/weekStyling.html View File

@ -0,0 +1,111 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Grid styling</title>
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js"></script>
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css"/>
<style type="text/css">
body, html {
font-family: sans-serif;
}
/* alternating column backgrounds */
.vis-time-axis .vis-grid.vis-odd {
background: #f5f5f5;
}
</style>
</head>
<body>
<p>
Week numbers are calculated based on locales. For this to properly work, the timeline must be loaded with a version
of moment.js including locales.</p>
<p>To set a locale for the timeline, specify the option
<code>{locale: STRING}</code>.</p>
<p>To set the scale to use week numbers, use for example <code>{scale: 'week', step: 1}</code>.</p>
<p>The following timeline is initialized with the 'de' locale and items are added with locally localized moment.js
objects. The timeline locale can be switched to another locale at runtime. If you choose the 'en' locale, the week
numbers will be calculated according to the US week calendar numbering scheme.</p>
<p>
<label for="locale">Select a locale:</label>
<select id="locale">
<option value="en">en</option>
<option value="it">it</option>
<option value="nl">nl</option>
<option value="de" selected>de</option>
</select>
</p>
<div id="visualization"></div>
<script type="text/javascript">
var itemCount = 26;
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
// just a group for the effects
var groups = new vis.DataSet();
groups.add([{id: 1, content: "ISO Weeks"}, {id: 2, content: "US Weeks"}]);
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet();
// create a localized moment object based on the current date
var USdate = moment().locale('en').hours(0).minutes(0).seconds(0).milliseconds(0);
// use the locale-aware weekday function to move to the begin of the current week
USdate.weekday(0);
// Iterate and just add a week to use it again in the next iteration
for (var i = 0; i < itemCount; i++) {
var USweekNumber = USdate.format('w');
var USweekStart = USdate.format();
var USweekEnd = USdate.add(1, 'week').format();
items.add({
id: i,
group: 2,
content: 'US week ' + USweekNumber,
start: USweekStart,
end: USweekEnd
});
}
// create another localized moment object - the 'de' locale works according to the ISO8601 leap week calendar system
var DEdate = moment().locale('de').hours(0).minutes(0).seconds(0).milliseconds(0);
DEdate.weekday(0);
for (var j = 0; j < itemCount; j++) {
var DEweekNumber = DEdate.format('w');
var DEweekStart = DEdate.format();
var DEweekEnd = DEdate.add(1, 'week').format();
items.add({
id: itemCount + j,
group: 1,
content: 'ISO week ' + DEweekNumber,
start: DEweekStart,
end: DEweekEnd
});
}
// Configuration for the Timeline
var options = {timeAxis: {scale: 'week', step: 1}, locale: 'de'};
// Create a Timeline
var timeline = new vis.Timeline(container, items, groups, options);
// update the locale when changing the select box value
var select = document.getElementById('locale');
select.onchange = function () {
timeline.setOptions({
locale: this.value
});
};
select.onchange();
</script>
</body>
</html>

+ 17
- 9
lib/graph3d/Graph3d.js View File

@ -53,7 +53,10 @@ var DEFAULTS = {
showShadow : false,
keepAspectRatio : true,
verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube'
dotSizeRatio : 0.02, // size of the dots as a fraction of the graph width
dotSizeRatio : 0.02, // size of the dots as a fraction of the graph width
dotSizeMinFraction: 0.5, // size of min-value dot as a fraction of dotSizeRatio
dotSizeMaxFraction: 2.5, // size of max-value dot as a fraction of dotSizeRatio
showAnimationControls: autoByDefault,
animationInterval : 1000, // milliseconds
@ -1014,7 +1017,8 @@ Graph3d.prototype._getLegendWidth = function() {
if (this.style === Graph3d.STYLE.DOTSIZE) {
var dotSize = this._dotSize();
width = dotSize / 2 + dotSize * 2;
//width = dotSize / 2 + dotSize * 2;
width = dotSize * this.dotSizeMaxFraction;
} else if (this.style === Graph3d.STYLE.BARSIZE) {
width = this.xBarWidth ;
} else {
@ -1086,8 +1090,8 @@ Graph3d.prototype._redrawLegend = function() {
// draw the size legend box
var widthMin;
if (this.style === Graph3d.STYLE.DOTSIZE) {
var dotSize = this._dotSize();
widthMin = dotSize / 2; // px
// Get the proportion to max and min right
widthMin = width * (this.dotSizeMinFraction / this.dotSizeMaxFraction);
} else if (this.style === Graph3d.STYLE.BARSIZE) {
//widthMin = this.xBarWidth * 0.2 this is wrong - barwidth measures in terms of xvalues
}
@ -1096,7 +1100,7 @@ Graph3d.prototype._redrawLegend = function() {
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(right, top);
ctx.lineTo(right - width + widthMin, bottom);
ctx.lineTo(left + widthMin, bottom);
ctx.lineTo(left, bottom);
ctx.closePath();
ctx.fill();
@ -1821,10 +1825,14 @@ Graph3d.prototype._redrawDotColorGraphPoint = function(ctx, point) {
* Draw single datapoint for graph style 'dot-size'.
*/
Graph3d.prototype._redrawDotSizeGraphPoint = function(ctx, point) {
var dotSize = this._dotSize();
var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range();
var size = dotSize/2 + 2*dotSize * fraction;
var colors = this._getColorsSize();
var dotSize = this._dotSize();
var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range();
var sizeMin = dotSize*this.dotSizeMinFraction;
var sizeRange = dotSize*this.dotSizeMaxFraction - sizeMin;
var size = sizeMin + sizeRange*fraction;
var colors = this._getColorsSize();
this._drawCircle(ctx, point, colors.fill, colors.border, size);
};

+ 2
- 0
lib/graph3d/Settings.js View File

@ -63,6 +63,8 @@ var OPTIONKEYS = [
'keepAspectRatio',
'verticalRatio',
'dotSizeRatio',
'dotSizeMinFraction',
'dotSizeMaxFraction',
'showAnimationControls',
'animationInterval',
'animationPreload',

+ 160
- 0
lib/network/CachedImage.js View File

@ -0,0 +1,160 @@
/**
* Associates a canvas to a given image, containing a number of renderings
* of the image at various sizes.
*
* This technique is known as 'mipmapping'.
*
* NOTE: Images can also be of type 'data:svg+xml`. This code also works
* for svg, but the mipmapping may not be necessary.
*/
class CachedImage {
constructor(image) {
this.NUM_ITERATIONS = 4; // Number of items in the coordinates array
this.image = new Image();
this.canvas = document.createElement('canvas');
}
/**
* Called when the image has been succesfully loaded.
*/
init() {
if (this.initialized()) return;
var w = this.image.width;
var h = this.image.height;
// Ease external access
this.width = w;
this.height = h;
// Make canvas as small as possible
this.canvas.width = 3*w/4;
this.canvas.height = h/2;
// Coordinates and sizes of images contained in the canvas
// Values per row: [top x, left y, width, height]
this.coordinates = [
[ 0 , 0 , w/2 , h/2],
[ w/2 , 0 , w/4 , h/4],
[ w/2 , h/4, w/8 , h/8],
[ 5*w/8, h/4, w/16, h/16]
];
this._fillMipMap();
}
/**
* @return {Boolean} true if init() has been called, false otherwise.
*/
initialized() {
return (this.coordinates !== undefined);
}
/**
* Redraw main image in various sizes to the context.
*
* The rationale behind this is to reduce artefacts due to interpolation
* at differing zoom levels.
*
* Source: http://stackoverflow.com/q/18761404/1223531
*
* This methods takes the resizing out of the drawing loop, in order to
* reduce performance overhead.
*
* @private
*/
_fillMipMap() {
var ctx = this.canvas.getContext('2d');
// First zoom-level comes from the image
var to = this.coordinates[0];
ctx.drawImage(this.image, to[0], to[1], to[2], to[3]);
// The rest are copy actions internal to the canvas/context
for (let iterations = 1; iterations < this.NUM_ITERATIONS; iterations++) {
let from = this.coordinates[iterations - 1];
let to = this.coordinates[iterations];
ctx.drawImage(this.canvas,
from[0], from[1], from[2], from[3],
to[0], to[1], to[2], to[3]
);
}
}
/**
* Draw the image, using the mipmap if necessary.
*
* MipMap is only used if param factor > 2; otherwise, original bitmap
* is resized. This is also used to skip mipmap usage, e.g. by setting factor = 1
*
* Credits to 'Alex de Mulder' for original implementation.
*
* ctx {Context} context on which to draw zoomed image
* factor {Float} scale factor at which to draw
*/
drawImageAtPosition(ctx, factor, left, top, width, height) {
if (factor > 2 && this.initialized()) {
// Determine which zoomed image to use
factor *= 0.5;
let iterations = 0;
while (factor > 2 && iterations < this.NUM_ITERATIONS) {
factor *= 0.5;
iterations += 1;
}
if (iterations >= this.NUM_ITERATIONS) {
iterations = this.NUM_ITERATIONS - 1;
}
//console.log("iterations: " + iterations);
let from = this.coordinates[iterations];
ctx.drawImage(this.canvas,
from[0], from[1], from[2], from[3],
left, top, width, height
);
} else if (this._isImageOk()) {
// Draw image directly
ctx.drawImage(this.image, left, top, width, height);
}
}
/**
* Check if image is loaded
*
* Source: http://stackoverflow.com/a/1977898/1223531
*
* @private
*/
_isImageOk(img) {
var img = this.image;
// During the onload event, IE correctly identifies any images that
// werenโ€™t downloaded as not complete. Others should too. Gecko-based
// browsers act like NS4 in that they report this incorrectly.
if (!img.complete) {
return false;
}
// However, they do have two very useful properties: naturalWidth and
// naturalHeight. These give the true size of the image. If it failed
// to load, either of these should be zero.
if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) {
return false;
}
// No other way of checking: assume itโ€™s ok.
return true;
}
}
export default CachedImage;

+ 43
- 30
lib/network/Images.js View File

@ -1,29 +1,17 @@
import CachedImage from './CachedImage';
/**
* @class Images
* This class loads images and keeps them stored.
*/
class Images{
class Images {
constructor(callback){
this.images = {};
this.imageBroken = {};
this.callback = callback;
}
/**
* @param {string} url The Url to cache the image as
* @return {Image} imageToLoadBrokenUrlOn The image object
*/
_addImageToCache (url, imageToCache) {
// IE11 fix -- thanks dponch!
if (imageToCache.width === 0) {
document.body.appendChild(imageToCache);
imageToCache.width = imageToCache.offsetWidth;
imageToCache.height = imageToCache.offsetHeight;
document.body.removeChild(imageToCache);
}
this.images[url] = imageToCache;
}
/**
* @param {string} url The original Url that failed to load, if the broken image is successfully loaded it will be added to the cache using this Url as the key so that subsequent requests for this Url will return the broken image
@ -31,18 +19,21 @@ class Images{
* @return {Image} imageToLoadBrokenUrlOn The image object
*/
_tryloadBrokenUrl (url, brokenUrl, imageToLoadBrokenUrlOn) {
//If any of the parameters aren't specified then exit the function because nothing constructive can be done
if (url === undefined || brokenUrl === undefined || imageToLoadBrokenUrlOn === undefined) return;
//If these parameters aren't specified then exit the function because nothing constructive can be done
if (url === undefined || imageToLoadBrokenUrlOn === undefined) return;
if (brokenUrl === undefined) {
console.warn("No broken url image defined");
return;
}
//Clear the old subscription to the error event and put a new in place that only handle errors in loading the brokenImageUrl
imageToLoadBrokenUrlOn.onerror = () => {
console.error("Could not load brokenImage:", brokenUrl);
//Add an empty image to the cache so that when subsequent load calls are made for the url we don't try load the image and broken image again
this._addImageToCache(url, new Image());
// cache item will contain empty image, this should be OK for default
};
//Set the source of the image to the brokenUrl, this is actually what kicks off the loading of the broken image
imageToLoadBrokenUrlOn.src = brokenUrl;
imageToLoadBrokenUrlOn.image.src = brokenUrl;
}
/**
@ -65,28 +56,50 @@ class Images{
if (cachedImage) return cachedImage;
//Create a new image
var img = new Image();
var img = new CachedImage();
// Need to add to cache here, otherwise final return will spawn different copies of the same image,
// Also, there will be multiple loads of the same image.
this.images[url] = img;
//Subscribe to the event that is raised if the image loads successfully
img.onload = () => {
//Add the image to the cache and then request a redraw
this._addImageToCache(url, img);
img.image.onload = () => {
// Properly init the cached item and then request a redraw
this._fixImageCoordinates(img.image);
img.init();
this._redrawWithImage(img);
};
//Subscribe to the event that is raised if the image fails to load
img.onerror = () => {
img.image.onerror = () => {
console.error("Could not load image:", url);
//Try and load the image specified by the brokenUrl using
this._tryloadBrokenUrl(url, brokenUrl, img);
}
//Set the source of the image to the url, this is actuall what kicks off the loading of the image
img.src = url;
//Set the source of the image to the url, this is what actually kicks off the loading of the image
img.image.src = url;
//Return the new image
return img;
}
}
/**
* IE11 fix -- thanks dponch!
*
* Local helper function
*
* @private
*/
_fixImageCoordinates(imageToCache) {
if (imageToCache.width === 0) {
document.body.appendChild(imageToCache);
imageToCache.width = imageToCache.offsetWidth;
imageToCache.height = imageToCache.offsetHeight;
document.body.removeChild(imageToCache);
}
}
}
export default Images;
export default Images;

+ 18
- 18
lib/network/Network.js View File

@ -10,24 +10,24 @@ let gephiParser = require('./gephiParser');
let Activator = require('../shared/Activator');
let locales = require('./locales');
import Images from './Images';
import Groups from './modules/Groups';
import NodesHandler from './modules/NodesHandler';
import EdgesHandler from './modules/EdgesHandler';
import PhysicsEngine from './modules/PhysicsEngine';
import ClusterEngine from './modules/Clustering';
import CanvasRenderer from './modules/CanvasRenderer';
import Canvas from './modules/Canvas';
import View from './modules/View';
import InteractionHandler from './modules/InteractionHandler';
import SelectionHandler from "./modules/SelectionHandler";
import LayoutEngine from "./modules/LayoutEngine";
import ManipulationSystem from "./modules/ManipulationSystem";
import Configurator from "./../shared/Configurator";
import Validator from "./../shared/Validator";
import {printStyle} from "./../shared/Validator";
import {allOptions, configureOptions} from './options.js';
import KamadaKawai from "./modules/KamadaKawai.js"
var Images = require('./Images').default;
var Groups = require('./modules/Groups').default;
var NodesHandler = require('./modules/NodesHandler').default;
var EdgesHandler = require('./modules/EdgesHandler').default;
var PhysicsEngine = require('./modules/PhysicsEngine').default;
var ClusterEngine = require('./modules/Clustering').default;
var CanvasRenderer = require('./modules/CanvasRenderer').default;
var Canvas = require('./modules/Canvas').default;
var View = require('./modules/View').default;
var InteractionHandler = require('./modules/InteractionHandler').default;
var SelectionHandler = require("./modules/SelectionHandler").default;
var LayoutEngine = require("./modules/LayoutEngine").default;
var ManipulationSystem = require("./modules/ManipulationSystem").default;
var Configurator = require("./../shared/Configurator").default;
var Validator = require("./../shared/Validator").default;
var {printStyle} = require('./../shared/Validator');
var {allOptions, configureOptions} = require('./options.js');
var KamadaKawai = require("./modules/KamadaKawai.js").default;
/**

+ 26
- 20
lib/network/modules/Canvas.js View File

@ -189,12 +189,7 @@ class Canvas {
}
else {
let ctx = this.frame.canvas.getContext("2d");
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
this._setPixelRatio(ctx);
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
}
@ -264,11 +259,7 @@ class Canvas {
// update the pixel ratio
let ctx = this.frame.canvas.getContext("2d");
let previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
this._setPixelRatio(ctx);
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
this._getCameraState(previousRatio);
@ -296,26 +287,29 @@ class Canvas {
// this would adapt the width of the canvas to the width from 100% if and only if
// there is a change.
let newWidth = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
let newHeight = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
// store the camera if there is a change in size.
if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio) || this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) {
if (this.frame.canvas.width !== newWidth || this.frame.canvas.height !== newHeight) {
this._getCameraState(previousRatio);
}
if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio)) {
this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
if (this.frame.canvas.width !== newWidth) {
this.frame.canvas.width = newWidth;
emitEvent = true;
}
if (this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) {
this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
if (this.frame.canvas.height !== newHeight) {
this.frame.canvas.height = newHeight;
emitEvent = true;
}
}
if (emitEvent === true) {
this.body.emitter.emit('resize', {
width:Math.round(this.frame.canvas.width / this.pixelRatio),
height:Math.round(this.frame.canvas.height / this.pixelRatio),
oldWidth: Math.round(oldWidth / this.pixelRatio),
width : Math.round(this.frame.canvas.width / this.pixelRatio),
height : Math.round(this.frame.canvas.height / this.pixelRatio),
oldWidth : Math.round(oldWidth / this.pixelRatio),
oldHeight: Math.round(oldHeight / this.pixelRatio)
});
@ -330,6 +324,18 @@ class Canvas {
};
/**
* @private
*/
_setPixelRatio(ctx) {
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
}
/**
* Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
* the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
@ -397,4 +403,4 @@ class Canvas {
}
export default Canvas;
export default Canvas;

+ 2
- 2
lib/network/modules/Clustering.js View File

@ -1,6 +1,6 @@
let util = require("../../util");
import NetworkUtil from '../NetworkUtil';
import Cluster from './components/nodes/Cluster'
var NetworkUtil = require('../NetworkUtil').default;
var Cluster = require('./components/nodes/Cluster').default;
class ClusterEngine {
constructor(body) {

+ 2
- 2
lib/network/modules/EdgesHandler.js View File

@ -2,8 +2,8 @@ var util = require("../../util");
var DataSet = require('../../DataSet');
var DataView = require('../../DataView');
import Edge from "./components/Edge"
import Label from "./components/shared/Label"
var Edge = require("./components/Edge").default;
var Label = require("./components/shared/Label").default;
class EdgesHandler {
constructor(body, images, groups) {

+ 2
- 2
lib/network/modules/InteractionHandler.js View File

@ -1,7 +1,7 @@
let util = require('../../util');
import NavigationHandler from './components/NavigationHandler'
import Popup from './../../shared/Popup'
var NavigationHandler = require('./components/NavigationHandler').default;
var Popup = require('./../../shared/Popup').default;
class InteractionHandler {
constructor(body, canvas, selectionHandler) {

+ 249
- 137
lib/network/modules/LayoutEngine.js View File

@ -1,7 +1,172 @@
'use strict';
let util = require('../../util');
import NetworkUtil from '../NetworkUtil';
var NetworkUtil = require('../NetworkUtil').default;
/**
* Container for derived data on current network, relating to hierarchy.
*
* Local, private class.
*
* TODO: Perhaps move more code for hierarchy state handling to this class.
* Till now, only the required and most obvious has been done.
*/
class HierarchicalStatus {
constructor() {
this.childrenReference = {};
this.parentReference = {};
this.levels = {};
this.trees = {};
this.isTree = false;
}
/**
* Add the relation between given nodes to the current state.
*/
addRelation(parentNodeId, childNodeId) {
if (this.childrenReference[parentNodeId] === undefined) {
this.childrenReference[parentNodeId] = [];
}
this.childrenReference[parentNodeId].push(childNodeId);
if (this.parentReference[childNodeId] === undefined) {
this.parentReference[childNodeId] = [];
}
this.parentReference[childNodeId].push(parentNodeId);
}
/**
* Check if the current state is for a tree or forest network.
*
* This is the case if every node has at most one parent.
*
* Pre: parentReference init'ed properly for current network
*/
checkIfTree() {
for (let i in this.parentReference) {
if (this.parentReference[i].length > 1) {
this.isTree = false;
return;
}
}
this.isTree = true;
}
/**
* Ensure level for given id is defined.
*
* Sets level to zero for given node id if not already present
*/
ensureLevel(nodeId) {
if (this.levels[nodeId] === undefined) {
this.levels[nodeId] = 0;
}
}
/**
* get the maximum level of a branch.
*
* TODO: Never entered; find a test case to test this!
*/
getMaxLevel(nodeId) {
let accumulator = {};
let _getMaxLevel = (nodeId) => {
if (accumulator[nodeId] !== undefined) {
return accumulator[nodeId];
}
let level = this.levels[nodeId];
if (this.childrenReference[nodeId]) {
let children = this.childrenReference[nodeId];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,_getMaxLevel(children[i]));
}
}
}
accumulator[nodeId] = level;
return level;
};
return _getMaxLevel(nodeId);
}
levelDownstream(nodeA, nodeB) {
if (this.levels[nodeB.id] === undefined) {
// set initial level
if (this.levels[nodeA.id] === undefined) {
this.levels[nodeA.id] = 0;
}
// set level
this.levels[nodeB.id] = this.levels[nodeA.id] + 1;
}
}
/**
* Small util method to set the minimum levels of the nodes to zero.
*/
setMinLevelToZero(nodes) {
let minLevel = 1e9;
// get the minimum level
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (this.levels[nodeId] !== undefined) {
minLevel = Math.min(this.levels[nodeId], minLevel);
}
}
}
// subtract the minimum from the set so we have a range starting from 0
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (this.levels[nodeId] !== undefined) {
this.levels[nodeId] -= minLevel;
}
}
}
}
/**
* Get the min and max xy-coordinates of a given tree
*/
getTreeSize(nodes, index) {
let min_x = 1e9;
let max_x = -1e9;
let min_y = 1e9;
let max_y = -1e9;
for (let nodeId in this.trees) {
if (this.trees.hasOwnProperty(nodeId)) {
if (this.trees[nodeId] === index) {
let node = nodes[nodeId];
min_x = Math.min(node.x, min_x);
max_x = Math.max(node.x, max_x);
min_y = Math.min(node.y, min_y);
max_y = Math.max(node.y, max_y);
}
}
}
return {
min_x: min_x,
max_x: max_x,
min_y: min_y,
max_y: max_y
};
}
}
class LayoutEngine {
constructor(body) {
@ -308,11 +473,8 @@ class LayoutEngine {
let definedLevel = false;
let definedPositions = true;
let undefinedLevel = false;
this.hierarchicalLevels = {};
this.lastNodeOnLevel = {};
this.hierarchicalChildrenReference = {};
this.hierarchicalParentReference = {};
this.hierarchicalTrees = {};
this.hierarchical = new HierarchicalStatus();
this.treeIndex = -1;
this.distributionOrdering = {};
@ -328,7 +490,7 @@ class LayoutEngine {
}
if (node.options.level !== undefined) {
definedLevel = true;
this.hierarchicalLevels[nodeId] = node.options.level;
this.hierarchical.levels[nodeId] = node.options.level;
}
else {
undefinedLevel = true;
@ -358,9 +520,7 @@ 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;
}
this.hierarchical.ensureLevel(nodeId);
}
}
// check the distribution of the nodes per level.
@ -402,9 +562,9 @@ class LayoutEngine {
// 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) {
for (let nodeId in this.hierarchical.trees) {
if (this.hierarchical.trees.hasOwnProperty(nodeId)) {
if (this.hierarchical.trees[nodeId] === index) {
let node = this.body.nodes[nodeId];
let pos = this._getPositionForHierarchy(node);
this._setPositionForHierarchy(node, pos + offset, undefined, true);
@ -415,18 +575,12 @@ class LayoutEngine {
// 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);
}
}
let res = this.hierarchical.getTreeSize(this.body.nodes, index);
if (this._isVertical()) {
return {min: res.min_x, max: res.max_x};
} else {
return {min: res.min_y, max: res.max_y};
}
return {min:min, max:max};
};
// get the width of all trees
@ -445,8 +599,8 @@ class LayoutEngine {
return;
}
map[source.id] = true;
if (this.hierarchicalChildrenReference[source.id]) {
let children = this.hierarchicalChildrenReference[source.id];
if (this.hierarchical.childrenReference[source.id]) {
let children = this.hierarchical.childrenReference[source.id];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
getBranchNodes(this.body.nodes[children[i]], map);
@ -465,7 +619,7 @@ class LayoutEngine {
for (let branchNode in branchMap) {
if (branchMap.hasOwnProperty(branchNode)) {
let node = this.body.nodes[branchNode];
let level = this.hierarchicalLevels[node.id];
let level = this.hierarchical.levels[node.id];
let position = this._getPositionForHierarchy(node);
// get the space around the node.
@ -484,39 +638,18 @@ class LayoutEngine {
return [min, max, minSpace, maxSpace];
};
// get the maximum level of a branch.
let getMaxLevel = (nodeId) => {
let accumulator = {};
let _getMaxLevel = (nodeId) => {
if (accumulator[nodeId] !== undefined) {
return accumulator[nodeId];
}
let level = this.hierarchicalLevels[nodeId];
if (this.hierarchicalChildrenReference[nodeId]) {
let children = this.hierarchicalChildrenReference[nodeId];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,_getMaxLevel(children[i]));
}
}
}
accumulator[nodeId] = level;
return level;
};
return _getMaxLevel(nodeId);
};
// 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);
let maxLevel1 = this.hierarchical.getMaxLevel(node1.id);
let maxLevel2 = this.hierarchical.getMaxLevel(node2.id);
return Math.min(maxLevel1, maxLevel2);
};
// check if two nodes have the same parent(s)
let hasSameParent = (node1, node2) => {
let parents1 = this.hierarchicalParentReference[node1.id];
let parents2 = this.hierarchicalParentReference[node2.id];
let parents1 = this.hierarchical.parentReference[node1.id];
let parents2 = this.hierarchical.parentReference[node2.id];
if (parents1 === undefined || parents2 === undefined) {
return false;
}
@ -539,7 +672,7 @@ class LayoutEngine {
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]) {
if (this.hierarchical.trees[levelNodes[j].id] === this.hierarchical.trees[levelNodes[j+1].id]) {
callback(levelNodes[j],levelNodes[j+1], centerParents);
}
}}
@ -593,7 +726,7 @@ class LayoutEngine {
// console.log("ts",node.id);
let nodeId = node.id;
let allEdges = node.edges;
let nodeLevel = this.hierarchicalLevels[node.id];
let nodeLevel = this.hierarchical.levels[node.id];
// gather constants
let C2 = this.options.hierarchical.levelSeparation * this.options.hierarchical.levelSeparation;
@ -604,7 +737,7 @@ class LayoutEngine {
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) {
if (this.hierarchical.levels[otherNode.id] < nodeLevel) {
aboveEdges.push(edge);
}
}
@ -802,7 +935,7 @@ class LayoutEngine {
if (map === undefined) {
useMap = false;
}
let level = this.hierarchicalLevels[node.id];
let level = this.hierarchical.levels[node.id];
if (level !== undefined) {
let index = this.distributionIndex[node.id];
let position = this._getPositionForHierarchy(node);
@ -837,16 +970,16 @@ class LayoutEngine {
* @private
*/
_centerParent(node) {
if (this.hierarchicalParentReference[node.id]) {
let parents = this.hierarchicalParentReference[node.id];
if (this.hierarchical.parentReference[node.id]) {
let parents = this.hierarchical.parentReference[node.id];
for (var i = 0; i < parents.length; i++) {
let parentId = parents[i];
let parentNode = this.body.nodes[parentId];
if (this.hierarchicalChildrenReference[parentId]) {
if (this.hierarchical.childrenReference[parentId]) {
// get the range of the children
let minPos = 1e9;
let maxPos = -1e9;
let children = this.hierarchicalChildrenReference[parentId];
let children = this.hierarchical.childrenReference[parentId];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
let childNode = this.body.nodes[children[i]];
@ -893,7 +1026,7 @@ class LayoutEngine {
// 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 (handledNodeCount > 0) {pos = this._getPositionForHierarchy(nodeArray[i-1]) + this.options.hierarchical.nodeSpacing;}
this._setPositionForHierarchy(node, pos, level);
this._validataPositionAndContinue(node, level, pos);
this._validatePositionAndContinue(node, level, pos);
handledNodeCount++;
}
@ -913,14 +1046,14 @@ class LayoutEngine {
*/
_placeBranchNodes(parentId, parentLevel) {
// if this is not a parent, cancel the placing. This can happen with multiple parents to one child.
if (this.hierarchicalChildrenReference[parentId] === undefined) {
if (this.hierarchical.childrenReference[parentId] === undefined) {
return;
}
// get a list of childNodes
let childNodes = [];
for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) {
childNodes.push(this.body.nodes[this.hierarchicalChildrenReference[parentId][i]]);
for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) {
childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]);
}
// use the positions to order the nodes.
@ -929,7 +1062,7 @@ class LayoutEngine {
// position the childNodes
for (let i = 0; i < childNodes.length; i++) {
let childNode = childNodes[i];
let childNodeLevel = this.hierarchicalLevels[childNode.id];
let childNodeLevel = this.hierarchical.levels[childNode.id];
// 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.
@ -939,7 +1072,7 @@ class LayoutEngine {
if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);}
else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;}
this._setPositionForHierarchy(childNode, pos, childNodeLevel);
this._validataPositionAndContinue(childNode, childNodeLevel, pos);
this._validatePositionAndContinue(childNode, childNodeLevel, pos);
}
else {
return;
@ -966,7 +1099,11 @@ class LayoutEngine {
* @param pos
* @private
*/
_validataPositionAndContinue(node, level, pos) {
_validatePositionAndContinue(node, level, pos) {
// This only works for strict hierarchical networks, i.e. trees and forests
// Early exit if this is not the case
if (!this.hierarchical.isTree) return;
// if overlap has been detected, we shift the branch
if (this.lastNodeOnLevel[level] !== undefined) {
let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[level]]);
@ -1013,7 +1150,7 @@ class LayoutEngine {
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId];
let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId];
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.y = this.options.hierarchical.levelSeparation * level;
node.options.fixed.y = true;
@ -1043,7 +1180,7 @@ class LayoutEngine {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (this.hierarchicalLevels[nodeId] === undefined) {
if (this.hierarchical.levels[nodeId] === undefined) {
hubSize = node.edges.length < hubSize ? hubSize : node.edges.length;
}
}
@ -1062,15 +1199,8 @@ class LayoutEngine {
let hubSize = 1;
let levelDownstream = (nodeA, nodeB) => {
if (this.hierarchicalLevels[nodeB.id] === undefined) {
// set initial level
if (this.hierarchicalLevels[nodeA.id] === undefined) {
this.hierarchicalLevels[nodeA.id] = 0;
}
// set level
this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1;
}
};
this.hierarchical.levelDownstream(nodeA, nodeB);
}
while (hubSize > 0) {
// determine hubs
@ -1089,8 +1219,11 @@ class LayoutEngine {
}
}
/**
* TODO: release feature
* TODO: Determine if this feature is needed at all
*
* @private
*/
_determineLevelsCustomCallback() {