Browse Source

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

revert-3409-performance
Yotam Berkowitz 7 years ago
parent
commit
73423c075b
39 changed files with 1464 additions and 808 deletions
  1. +2
    -1
      .gitignore
  2. +62
    -0
      HISTORY.md
  3. +126
    -54
      docs/graph3d/index.html
  4. +17
    -6
      docs/network/index.html
  5. +4
    -1
      docs/network/nodes.html
  6. +21
    -2
      docs/timeline/index.html
  7. +38
    -0
      examples/timeline/editing/itemsAlwaysDraggable.html
  8. +307
    -0
      lib/graph3d/DataGroup.js
  9. +4
    -9
      lib/graph3d/Filter.js
  10. +91
    -277
      lib/graph3d/Graph3d.js
  11. +4
    -0
      lib/network/CachedImage.js
  12. +15
    -1
      lib/network/Network.js
  13. +30
    -2
      lib/network/dotparser.js
  14. +51
    -7
      lib/network/modules/Canvas.js
  15. +9
    -23
      lib/network/modules/CanvasRenderer.js
  16. +14
    -4
      lib/network/modules/Clustering.js
  17. +16
    -4
      lib/network/modules/EdgesHandler.js
  18. +42
    -56
      lib/network/modules/InteractionHandler.js
  19. +259
    -147
      lib/network/modules/LayoutEngine.js
  20. +11
    -3
      lib/network/modules/NodesHandler.js
  21. +14
    -3
      lib/network/modules/PhysicsEngine.js
  22. +8
    -4
      lib/network/modules/components/Edge.js
  23. +22
    -0
      lib/network/modules/components/Node.js
  24. +1
    -14
      lib/network/modules/components/edges/BezierEdgeDynamic.js
  25. +62
    -136
      lib/network/modules/components/edges/BezierEdgeStatic.js
  26. +2
    -17
      lib/network/modules/components/edges/CubicBezierEdge.js
  27. +42
    -1
      lib/network/modules/components/edges/util/BezierEdgeBase.js
  28. +5
    -9
      lib/network/modules/components/shared/Label.js
  29. +7
    -2
      lib/timeline/Range.js
  30. +5
    -1
      lib/timeline/TimeStep.js
  31. +1
    -0
      lib/timeline/component/Group.js
  32. +21
    -4
      lib/timeline/component/ItemSet.js
  33. +7
    -2
      lib/timeline/component/item/Item.js
  34. +6
    -5
      lib/timeline/component/item/RangeItem.js
  35. +5
    -1
      lib/timeline/optionsTimeline.js
  36. +12
    -11
      lib/util.js
  37. +1
    -1
      package.json
  38. +46
    -0
      test/TimeStep.test.js
  39. +74
    -0
      test/dotparser.test.js

+ 2
- 1
.gitignore View File

@ -13,5 +13,6 @@ npm-debug.log
.settings/
.directory
# vim temporary files
# temporary files
.*.sw[op]
.commits.tmp

+ 62
- 0
HISTORY.md View File

@ -1,6 +1,68 @@
# vis.js history
http://visjs.org
## 2017-05-21, version 4.20.0
### General
- FIX #2934: Replacing all ES6 imports with CJS require calls (#3063)
- Add command line options to mocha for running tests (#3064)
- Added documentation on how labels are used (#2873)
- FIX: Fix typo in PR template (#2908)
- FIX #2912: updated moment.js (#2925)
- Added @wimrijnders to the support team (#2886)
### Network
- FIX: Fixes for loading images into image nodes (#2964)
- FIX #3025: Added check on mission var 'options', refactoring. (#3055)
- FIX #3057: Use get() to get data from DataSet/View instead of directly accessing member \_data. (#3069)
- FIX #3065: Avoid overriding standard context method ellipse() (#3072)
- FIX #2922: bold label for selected ShapeBase classes (#2924)
- FIX #2952: Pre-render node images for interpolation (#3010)
- FIX #1735: Fix for exploding directed network, first working version; refactored hierarchical state in LayoutEngine.(#3017)
- Refactoring of Label.propagateFonts() (#3052)
- FIX #2894: Set CircleImageBase.imageObjAlt always when options change (#3053)
- FIX #3047: Label.getFormattingValues() fix option fallback to main font for mod-fonts (#3054)
- FIX #2938: Fix handling of node id's in saveAndLoad example (#2943)
- FIX: Refactoring in Canvas.js (#3030)
- FIX #2968: Fix placement label for dot shape (#3018)
- FIX #2994: select edge with id zero (#2996)
- FIX #1847, #2436: Network: use separate refresh indicator in NodeBase, instead of width… (#2885)
- Fix #2914: Use option edges.chosen if present in global options (#2917)
- FIX #2940: Gephi consolidate double assignment of node title (#2962)
- FIX 2936: Fix check for nodes not present in EdgesHandler (#2963)
- FEAT: Reduce the time-complexity of the network initial positioning (#2759)
### Timeline / Graph2D
- FEAT: Add support for multiple class names in utils add/remove class methods (#3079)
- FEAT: Adds 'showTooltips' option to override popups displayed for items with titles (#3046)
- FIX #2818: LineGraph: Add an existingItemsMap to check if items are new or not before skipping (#3075)
- FEAT #2835: Improve timeline stack performance (#2848, #3078)
- FIX #3032: mouseup and mousedown events (#3059)
- FIX #2421: Fix click and doubleclick events on items (#2988)
- FEAT #1405, #1715, #3002: Implementation of a week scale feature (#3009)
- FIX #397: Eliminate repeatedly fired `rangechanged` events on mousewheel (#2989)
- FIX #2939: Add check for parent existence when changing group in Item.setData (#2985)
- FIX #2877: Add check for empty groupIds array and get full list from data set (#2986)
- FIX #2614: Timeline docs border overlaps (#2992)
- FIX: Doubleclick add (#2987)
- FIX #2679: Cannot read property 'hasOwnProperty' of null (#2973)
- FEAT #2863: Drag and drop custom fields (#2872)
- FEAT #2834: Control over the drop event (#2974)
- FIX #2918: Remove usages of elementsCensor (#2947)
- FEAT #2948: Rolling mode offset (#2950)
- FEAT #2805: Add callback functions to moveTo, zoomIn, zoomOut and setWindow (#2870)
- FIX: Do not corrupt class names at high zoom levels (#2909)
- FIX #2888: Fix error in class names (#2911)
- FIX #2835: Visible items bug (#2878)
### Graph3D
- FEAT: Configurable minimum and maximum sizes for dot-size graphs (#2849)
## 2017-03-19, version 4.19.1
### General

+ 126
- 54
docs/graph3d/index.html View File

@ -272,7 +272,7 @@ var options = {
The following options are available.
</p>
<table class="options">
<table class="options" id="optionTable">
<tr>
<th>Name</th>
<th>Type</th>
@ -311,69 +311,83 @@ var options = {
<td>The color of the axis lines and the text along the axis.</td>
</tr>
<tr>
<td>backgroundColor</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','backgroundColor', this);">
<td><span parent="backgroundColor" class="right-caret"></span> backgroundColor</td>
<td>string or Object</td>
<td>{fill:&nbsp;'white', stroke:&nbsp;'gray', strokeWidth:&nbsp;1}</td>
<td>Object</td>
<td>The background color for the main area of the chart.
Can be either a simple HTML color string, for example: 'red' or '#00cc00',
or an object with the following properties.</td>
</tr>
<tr>
<td>backgroundColor.fill</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.fill</td>
<td>string</td>
<td>'white'</td>
<td>The chart fill color, as an HTML color string.</td>
</tr>
<tr>
<td>backgroundColor.stroke</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.stroke</td>
<td>string</td>
<td>'gray'</td>
<td>The color of the chart border, as an HTML color string.</td>
</tr>
<tr>
<td>backgroundColor.strokeWidth</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.strokeWidth</td>
<td>number</td>
<td>1</td>
<td>The border width, in pixels.</td>
</tr>
<tr>
<td>cameraPosition</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','cameraPosition', this);">
<td><span parent="cameraPosition" class="right-caret"></span> cameraPosition</td>
<td>Object</td>
<td>Object</td>
<td>{horizontal:&nbsp;1.0, vertical:&nbsp;0.5, distance:&nbsp;1.7}</td>
<td>Set the initial rotation and position of the camera.
The object <code>cameraPosition</code> contains three parameters:
<code>horizontal</code>, <code>vertical</code>, and <code>distance</code>.
Parameter <code>horizontal</code> is a value in radians and can have any
value (but normally in the range of 0 and 2*Pi).
Parameter <code>vertical</code> is a value in radians between 0 and 0.5*Pi.
Parameter <code>distance</code> is the (normalized) distance from the
camera to the center of the graph, in the range of 0.71 to 5.0. A
larger distance puts the graph further away, making it smaller.
All parameters are optional.
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.horizontal</td>
<td>number</td>
<td>1.0</td>
<td>Value in radians. It can have any
value, but is normally in the range of 0 and 2*Pi.</td>
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.vertical</td>
<td>number</td>
<td>0.5</td>
<td>Value in radians between 0 and 0.5*Pi.</td>
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.distance</td>
<td>number</td>
<td>1.7</td>
<td>The (normalized) distance from the
camera to the center of the graph, in the range of 0.71 to 5.0. A
larger distance puts the graph further away, making it smaller.</p>
</tr>
<tr>
<td>dataColor</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','dataColor', this);">
<td><span parent="dataColor" class="right-caret"></span> dataColor</td>
<td>string or object</td>
<td>{fill:&nbsp;'#7DC1FF', stroke:&nbsp;'#3267D2', strokeWidth:&nbsp;1}</td>
<td>Object</td>
<td>When <code>dataColor</code> is a string, it will set the color for both border and fill color of dots and bars. Applicable for styles <code>dot-size</code>, <code>bar-size</code>, and <code>line</code>. When an object, it can contain the properties descibed below.</td>
</tr>
<tr>
<td>dataColor.fill</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.fill</td>
<td>string</td>
<td>'#7DC1FF'</td>
<td>The fill color of the dots or bars. Applicable when using styles <code>dot-size</code>, <code>bar-size</code>, or <code>line</code>.</td>
</tr>
<tr>
<td>dataColor.stroke</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.stroke</td>
<td>string</td>
<td>'#3267D2'</td>
<td>The border color of the dots or bars. Applicable when using styles <code>dot-size</code> or <code>bar-size</code>.</td>
</tr>
<tr>
<td>dataColor.strokeWidth</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.strokeWidth</td>
<td>number</td>
<td>1</td>
<td>The line width of dots, bars and lines. Applicable for all styles.</td>
@ -516,37 +530,95 @@ var options = {
</td>
</tr>
<tr>
<td>tooltipStyle</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','tooltipStyle', this);">
<td><span parent="tooltipStyle" class="right-caret"></span> tooltipStyle</td>
<td>Object</td>
<td>Object</td>
<td>
<pre class="prettyprint lang-js">
{
content: {
padding: '10px',
border: '1px solid #4d4d4d',
color: '#1a1a1a',
background: 'rgba(255,255,255,0.7)',
borderRadius: '2px',
boxShadow: '5px 5px 10px rgba(128,128,128,0.5)'
},
line: {
height: '40px',
width: '0',
borderLeft: '1px solid #4d4d4d'
},
dot: {
height: '0',
width: '0',
border: '5px solid #4d4d4d',
borderRadius: '5px'
}
}</pre>
</td>
<td>Tooltip style properties.
Provided properties will be merged with the default object.
</td>
</tr>
<!-- Can't define separate entries for content, line and dot objects here,
because toggleTable() can't handle multiple levels of collapsibles -->
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.padding</td>
<td>string</td>
<td>'10px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.border</td>
<td>string</td>
<td>'1px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.color</td>
<td>string</td>
<td>'#1a1a1a'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.background</td>
<td>string</td>
<td>'rgba(255,255,255,0.7)'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.borderRadius</td>
<td>string</td>
<td>'2px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.boxShadow</td>
<td>string</td>
<td>'5px 5px 10px rgba(128,128,128,0.5)'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.height</td>
<td>string</td>
<td>'40px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.width</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.borderLeft</td>
<td>string</td>
<td>'1px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.height</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.width</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.border</td>
<td>string</td>
<td>'5px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.borderRadius</td>
<td>string</td>
<td>'5px'</td>
<td></td>
</tr>
<tr>
<td>valueMax</td>

+ 17
- 6
docs/network/index.html View File

@ -611,13 +611,18 @@ var locales = {
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','findNode', this);">
<td colspan="2"><span parent="findNode" class="right-caret" id="method_findNode"></span> findNode(
<code>String nodeId</code>)
<code>String/Number nodeId</code>)
</tr>
<tr class="hidden" parent="findNode">
<td class="midMethods">Returns: Array</td>
<td>Nodes can be in clusters. Clusters can also be in clusters. This function returns and array of
nodeIds
showing where the node is. <br><br> Example:
nodeIds showing where the node is.
<br><br>
If any nodeId in the chain, especially the first passed in as a parameter, is not present in
the current nodes list, an empty array is returned.
<br><br> Example:
cluster 'A' contains cluster 'B',
cluster 'B' contains cluster 'C',
cluster 'C' contains node 'fred'.
@ -866,13 +871,19 @@ function releaseFunction (clusterPosition, containedNodesPositions) {
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','getConnectedNodes', this);">
<td colspan="2"><span parent="getConnectedNodes" class="right-caret" id="method_getConnectedNodes"></span> getConnectedNodes(<code><i>String
nodeId or edgeId</i></code>)
nodeId or edgeId, [String direction]</i></code>)
</td>
</tr>
<tr class="hidden" parent="getConnectedNodes">
<td class="midMethods">Returns: Array</td>
<td>Returns an array of nodeIds of the all the nodes that are directly connected to this node. If you supply an edgeId,
vis will first match the id to nodes. If no match is found, it will search in the edgelist and return an array: <code>[fromId, toId]</code>.</td>
<td>Returns an array of nodeIds of all the nodes that are directly connected to this node or edge.<br><br>
For a node id, returns an array with the id's of the connected nodes.<br>
If optional parameter <code>direction</code> is set to string <i>'from'</i>, only parent nodes are returned.<br>
If <code>direction</code> is set to <i>'to'</i>, only child nodes are returned.<br>
Any other value or <code>undefined</code> returns both parent and child nodes.
<br><br>
For an edge id, returns an array: <code>[fromId, toId]</code>.
Parameter <i>direction</i> is ignored for edges.</td>
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','getConnectedEdges', this);">
<td colspan="2"><span parent="getConnectedEdges" class="right-caret" id="method_getConnectedEdges"></span> getConnectedEdges(<code><i>String

+ 4
- 1
docs/network/nodes.html View File

@ -809,7 +809,10 @@ network.setOptions(options);
<td>Number</td>
<td><code>1</code></td>
<td>The barnesHut physics model (which is enabled by default) is based on an inverted gravity model. By
increasing the mass of a node, you increase it's repulsion. Values lower than 1 are not recommended.
increasing the mass of a node, you increase it's repulsion.
<br><br>
Values between 0 and 1 are not recommended.<br>
Negative or zero values are not allowed. These will generate a console error and will be set to 1.
</td>
</tr>
<tr>

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

@ -261,6 +261,13 @@ var items = new vis.DataSet([
<a href="#Styles">Styles</a>.
</td>
</tr>
<tr>
<td>align</td>
<td>String</td>
<td>no</td>
<td>This field is optional. If set this overrides the global <code>align</code> configuration option for this item.
</td>
</tr>
<tr>
<td>content</td>
<td>String</td>
@ -729,12 +736,24 @@ function (option, path) {
</td>
</tr>
<tr>
<td>itemsAlwaysDraggable</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','itemsAlwaysDraggable', this);">
<td><span parent="itemsAlwaysDraggable" class="right-caret"></span> itemsAlwaysDraggable</td>
<td>boolean or Object</td>
<td>Object</td>
<td>When a boolean, applies the value only to <code>itemsAlwaysDraggable.item</code>.</td>
</tr>
<tr parent="itemsAlwaysDraggable" class="hidden">
<td class="indent">itemsAlwaysDraggable.item</td>
<td>boolean</td>
<td><code>false</code></td>
<td>If true, all items in the Timeline are draggable without being selected. If false, only the selected item(s) are draggable.</td>
</tr>
<tr parent="itemsAlwaysDraggable" class="hidden">
<td class="indent">itemsAlwaysDraggable.range</td>
<td>boolean</td>
<td><code>false</code></td>
<td>If true, range of all items in the Timeline is draggable without being selected. If false, range is only draggable for the selected item(s). Only applicable when option <code>itemsAlwaysDraggable.item</code> is set <code>true</code>. </td>
</tr>
<tr>
<td>locale</td>

+ 38
- 0
examples/timeline/editing/itemsAlwaysDraggable.html View File

@ -0,0 +1,38 @@
<html>
<head>
<title>Timeline | itemsAlwaysDraggable Option</title>
<meta charset="utf-8">
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>The <code>itemsAlwaysDraggable</code> option allows to drag items around without first selecting them. When <code>itemsAlwaysDraggable.range</code> is set to <code>true</code>, the range can be changed without selection as well.</p>
<div id="mytimeline"></div>
<script>
var container = document.getElementById('mytimeline'),
items = new vis.DataSet();
for (var i = 10; i >= 0; i--) {
var start = new Date(new Date().getTime() + i * 100000);
items.add({
id: i,
content: "item " + i,
start: start,
end: new Date(start.getTime() + 100000)
});
}
var options = {
start: new Date(),
end: new Date(new Date().getTime() + 1000000),
editable: true,
itemsAlwaysDraggable: {
item: true,
range: true
}
};
var timeline = new vis.Timeline(container, items, null, options);
</script>
</body>
</html>

+ 307
- 0
lib/graph3d/DataGroup.js View File

@ -0,0 +1,307 @@
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Point3d = require('./Point3d');
var Range = require('./Range');
/**
* Creates a container for all data of one specific 3D-graph.
*
* On construction, the container is totally empty; the data
* needs to be initialized with method initializeData().
* Failure to do so will result in the following exception begin thrown
* on instantiation of Graph3D:
*
* Error: Array, DataSet, or DataView expected
*
* @constructor
*/
function DataGroup() {
this.dataTable = null; // The original data table
}
/**
* Initializes the instance from the passed data.
*
* Calculates minimum and maximum values and column index values.
*
* The graph3d instance is used internally to access the settings for
* the given instance.
* TODO: Pass settings only instead.
*
* @param {Graph3D} graph3d Reference to the calling Graph3D instance.
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
if (rawData === undefined)
return;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = graph3d.hasBars(style);
// determine barWidth from data
if (withBars) {
if (graph3d.defaultXBarWidth !== undefined) {
this.xBarWidth = graph3d.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (graph3d.defaultYBarWidth !== undefined) {
this.yBarWidth = graph3d.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minima and maxima
this._initializeRange(data, this.colX, graph3d, withBars);
this._initializeRange(data, this.colY, graph3d, withBars);
this._initializeRange(data, this.colZ, graph3d, false);
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data, this.colValue);
this._setRangeDefaults(valueRange, graph3d.defaultValueMin, graph3d.defaultValueMax);
this.valueRange = valueRange;
}
};
/**
* Collect the range settings for the given data column.
*
* This internal method is intended to make the range
* initalization more generic.
*
* TODO: if/when combined settings per axis defined, get rid of this.
*
* @private
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
*/
DataGroup.prototype._collectRangeSettings = function(column, graph3d) {
var index = ['x', 'y', 'z'].indexOf(column);
if (index == -1) {
throw new Error('Column \'' + column + '\' invalid');
}
var upper = column.toUpperCase();
return {
barWidth : this[column + 'BarWidth'],
min : graph3d['default' + upper + 'Min'],
max : graph3d['default' + upper + 'Max'],
step : graph3d['default' + upper + 'Step'],
range_label: column + 'Range', // Name of instance field to write to
step_label : column + 'Step' // Name of instance field to write to
};
}
/**
* Initializes the settings per given column.
*
* TODO: if/when combined settings per axis defined, rewrite this.
*
* @private
*
* @param {DataSet | DataView} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
* @param {Boolean} withBars True if initializing for bar graph
*/
DataGroup.prototype._initializeRange = function(data, column, graph3d, withBars) {
var NUMSTEPS = 5;
var settings = this._collectRangeSettings(column, graph3d);
var range = this.getColumnRange(data, column);
if (withBars && column != 'z') { // Safeguard for 'z'; it doesn't have a bar width
range.expand(settings.barWidth / 2);
}
this._setRangeDefaults(range, settings.min, settings.max);
this[settings.range_label] = range;
this[settings.step_label ] = (settings.step !== undefined) ? settings.step : range.range()/NUMSTEPS;
}
/**
* Creates a list with all the different values in the data for the given column.
*
* If no data passed, use the internal data of this instance.
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
*
* @returns {Array} All distinct values in the given column data, sorted ascending.
*/
DataGroup.prototype.getDistinctValues = function(column, data) {
if (data === undefined) {
data = this.dataTable;
}
var values = [];
for (var i = 0; i < data.length; i++) {
var value = data[i][column] || 0;
if (values.indexOf(value) === -1) {
values.push(value);
}
}
return values.sort(function(a,b) { return a - b; });
};
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
DataGroup.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Range} A Range instance with min/max members properly set.
*/
DataGroup.prototype.getColumnRange = function(data, column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Determines the number of rows in the current data.
*
* @returns {Number}
*/
DataGroup.prototype.getNumberOfRows = function() {
return this.dataTable.length;
}
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
DataGroup.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
DataGroup.prototype.getDataTable = function() {
return this.dataTable;
};
DataGroup.prototype.getDataSet = function() {
return this.dataSet;
};
/**
* Reload the data
*/
DataGroup.prototype.reload = function() {
if (this.dataTable) {
this.setData(this.dataTable);
}
};
module.exports = DataGroup;

+ 4
- 9
lib/graph3d/Filter.js View File

@ -3,12 +3,12 @@ var DataView = require('../DataView');
/**
* @class Filter
*
* @param {DataSet} data The google data table
* @param {DataGroup} dataGroup the data group
* @param {Number} column The index of the column to be filtered
* @param {Graph} graph The graph
*/
function Filter (data, column, graph) {
this.data = data;
function Filter (dataGroup, column, graph) {
this.data = dataGroup.getDataSet();
this.column = column;
this.graph = graph; // the parent graph
@ -16,12 +16,7 @@ function Filter (data, column, graph) {
this.value = undefined;
// read all distinct values and select the first one
this.values = graph.getDistinctValues(data.get(), this.column);
// sort both numeric and string values correctly
this.values.sort(function (a, b) {
return a > b ? 1 : a < b ? -1 : 0;
});
this.values = dataGroup.getDistinctValues(this.column);
if (this.values.length > 0) {
this.selectValue(0);

+ 91
- 277
lib/graph3d/Graph3d.js View File

@ -1,4 +1,5 @@
var Emitter = require('emitter-component'); var DataSet = require('../DataSet');
var Emitter = require('emitter-component');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var util = require('../util');
var Point3d = require('./Point3d');
@ -9,6 +10,7 @@ var Slider = require('./Slider');
var StepNumber = require('./StepNumber');
var Range = require('./Range');
var Settings = require('./Settings');
var DataGroup = require('./DataGroup');
/// enumerate the available styles
@ -148,7 +150,7 @@ function Graph3d(container, data, options) {
// create variables and set default values
this.containerElement = container;
this.dataTable = null; // The original data table
this.dataGroup = new DataGroup();
this.dataPoints = null; // The table with point objects
// create a frame and canvas
@ -303,11 +305,7 @@ Graph3d.prototype._convertTranslationToScreen = function(translation) {
/**
* Calculate the translations and screen positions of all points
*/
Graph3d.prototype._calcTranslations = function(points, sort) {
if (sort === undefined) {
sort = true;
}
Graph3d.prototype._calcTranslations = function(points) {
for (var i = 0; i < points.length; i++) {
var point = points[i];
point.trans = this._convertPointToTranslation(point.point);
@ -318,10 +316,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
point.dist = this.showPerspective ? transBottom.length() : -transBottom.z;
}
if (!sort) {
return;
}
// sort the points on depth of their (x,y) position (not on z)
var sortDepth = function (a, b) {
return b.dist - a.dist;
@ -330,78 +324,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
};
Graph3d.prototype.getNumberOfRows = function(data) {
return data.length;
}
Graph3d.prototype.getNumberOfColumns = function(data) {
var counter = 0;
for (var column in data[0]) {
if (data[0].hasOwnProperty(column)) {
counter++;
}
}
return counter;
}
Graph3d.prototype.getDistinctValues = function(data, column) {
var distinctValues = [];
for (var i = 0; i < data.length; i++) {
if (distinctValues.indexOf(data[i][column]) == -1) {
distinctValues.push(data[i][column]);
}
}
return distinctValues.sort(function(a,b) { return a - b; });
}
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
Graph3d.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
var diffs = [];
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @returns {Range} A Range instance with min/max members properly set.
*/
Graph3d.prototype.getColumnRange = function(data,column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Check if the state is consistent for the use of the value field.
*
@ -418,6 +340,7 @@ Graph3d.prototype._checkValueField = function (data) {
return; // No need to check further
}
// Following field must be present for the current graph style
if (this.colValue === undefined) {
throw new Error('Expected data to have '
@ -437,150 +360,76 @@ Graph3d.prototype._checkValueField = function (data) {
};
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
Graph3d.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
/**
* Initialize the data from the data table. Calculate minimum and maximum values
* and column index values
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
Graph3d.prototype._dataInitialize = function (rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
Graph3d.prototype._initializeData = function(rawData, style) {
this.dataGroup.initializeData(this, rawData, style);
if (rawData === undefined)
return;
// Transfer min/max values to the Graph3d instance.
// TODO: later on, all min/maxes of all datagroups will be combined here
this.xRange = this.dataGroup.xRange;
this.yRange = this.dataGroup.yRange;
this.zRange = this.dataGroup.zRange;
this.valueRange = this.dataGroup.valueRange;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
// Values currently needed but which need to be sorted out for
// the multiple graph case.
this.xStep = this.dataGroup.xStep;
this.yStep = this.dataGroup.yStep;
this.zStep = this.dataGroup.zStep;
this.xBarWidth = this.dataGroup.xBarWidth;
this.yBarWidth = this.dataGroup.yBarWidth;
this.colX = this.dataGroup.colX;
this.colY = this.dataGroup.colY;
this.colZ = this.dataGroup.colZ;
this.colValue = this.dataGroup.colValue;
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
// Check if a filter column is provided
var data = this.dataGroup.getDataTable();
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = this.style == Graph3d.STYLE.BAR ||
this.style == Graph3d.STYLE.BARCOLOR ||
this.style == Graph3d.STYLE.BARSIZE;
// determine barWidth from data
if (withBars) {
if (this.defaultXBarWidth !== undefined) {
this.xBarWidth = this.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (this.defaultYBarWidth !== undefined) {
this.yBarWidth = this.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minimums and maximums
var NUMSTEPS = 5;
var xRange = this.getColumnRange(data, this.colX);
if (withBars) {
xRange.expand(this.xBarWidth / 2);
}
this._setRangeDefaults(xRange, this.defaultXMin, this.defaultXMax);
this.xRange = xRange;
this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : xRange.range()/NUMSTEPS;
var yRange = this.getColumnRange(data, this.colY);
if (withBars) {
yRange.expand(this.yBarWidth / 2);
}
this._setRangeDefaults(yRange, this.defaultYMin, this.defaultYMax);
this.yRange = yRange;
this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : yRange.range()/NUMSTEPS;
var zRange = this.getColumnRange(data, this.colZ);
this._setRangeDefaults(zRange, this.defaultZMin, this.defaultZMax);
this.zRange = zRange;
this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : zRange.range()/NUMSTEPS;
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data,this.colValue);
this._setRangeDefaults(valueRange, this.defaultValueMin, this.defaultValueMax);
this.valueRange = valueRange;
}
// check if a filter column is provided
// Needs to be started after zRange is defined
if (data[0].hasOwnProperty('filter')) {
// Only set this field if it's actually present
this.colFilter = 'filter';
var me = this;
if (this.dataFilter === undefined) {
this.dataFilter = new Filter(rawData, this.colFilter, this);
this.dataFilter = new Filter(this.dataGroup, this.colFilter, this);
this.dataFilter.setOnLoadCallback(function() {me.redraw();});
}
}
// set the scale dependent on the ranges.
this._setScale();
};
/**
* Return all data values as a list of Point3d objects
*/
Graph3d.prototype.getDataPoints = function(data) {
var dataPoints = [];
for (var i = 0; i < data.length; i++) {
var point = new Point3d();
point.x = data[i][this.colX] || 0;
point.y = data[i][this.colY] || 0;
point.z = data[i][this.colZ] || 0;
point.data = data[i];
if (this.colValue !== undefined) {
point.value = data[i][this.colValue] || 0;
}
var obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
dataPoints.push(obj);
}
return dataPoints;
};
/**
* Filter the data based on the current filter
@ -598,60 +447,29 @@ Graph3d.prototype._getDataPoints = function (data) {
if (this.style === Graph3d.STYLE.GRID ||
this.style === Graph3d.STYLE.SURFACE) {
// copy all values from the google data table to a matrix
// copy all values from the data table to a matrix
// the provided values are supposed to form a grid of (x,y) positions
// create two lists with all present x and y values
var dataX = [];
var dataY = [];
for (i = 0; i < this.getNumberOfRows(data); i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
if (dataX.indexOf(x) === -1) {
dataX.push(x);
}
if (dataY.indexOf(y) === -1) {
dataY.push(y);
}
}
var dataX = this.dataGroup.getDistinctValues(this.colX, data);
var dataY = this.dataGroup.getDistinctValues(this.colY, data);
var sortNumber = function (a, b) {
return a - b;
};
dataX.sort(sortNumber);
dataY.sort(sortNumber);
dataPoints = this.getDataPoints(data);
// create a grid, a 2d matrix, with all values.
var dataMatrix = []; // temporary data matrix
for (i = 0; i < data.length; i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
z = data[i][this.colZ] || 0;
for (i = 0; i < dataPoints.length; i++) {
obj = dataPoints[i];
// TODO: implement Array().indexOf() for Internet Explorer
var xIndex = dataX.indexOf(x);
var yIndex = dataY.indexOf(y);
var xIndex = dataX.indexOf(obj.point.x);
var yIndex = dataY.indexOf(obj.point.y);
if (dataMatrix[xIndex] === undefined) {
dataMatrix[xIndex] = [];
}
var point3d = new Point3d();
point3d.x = x;
point3d.y = y;
point3d.z = z;
point3d.data = data[i];
obj = {};
obj.point = point3d;
obj.trans = undefined;
obj.screen = undefined;
obj.bottom = new Point3d(x, y, this.zRange.min);
dataMatrix[xIndex][yIndex] = obj;
dataPoints.push(obj);
}
// fill in the pointers to the neighbors.
@ -670,39 +488,22 @@ Graph3d.prototype._getDataPoints = function (data) {
}
else { // 'dot', 'dot-line', etc.
this._checkValueField(data);
dataPoints = this.getDataPoints(data);
// copy all values from the google data table to a list with Point3d objects
for (i = 0; i < data.length; i++) {
point = new Point3d();
point.x = data[i][this.colX] || 0;
point.y = data[i][this.colY] || 0;
point.z = data[i][this.colZ] || 0;
point.data = data[i];
if (this.colValue !== undefined) {
point.value = data[i][this.colValue] || 0;
}
obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
if (this.style === Graph3d.STYLE.LINE) {
if (this.style === Graph3d.STYLE.LINE) {
// Add next member points for line drawing
for (i = 0; i < dataPoints.length; i++) {
if (i > 0) {
// Add next point for line drawing
dataPoints[i - 1].pointNext = obj;
dataPoints[i - 1].pointNext = dataPoints[i];;
}
}
dataPoints.push(obj);
}
}
return dataPoints;
};
/**
* Create the main frame for the Graph3d.
*
@ -854,7 +655,7 @@ Graph3d.prototype.getCameraPosition = function() {
*/
Graph3d.prototype._readData = function(data) {
// read the data
this._dataInitialize(data, this.style);
this._initializeData(data, this.style);
if (this.dataFilter) {
@ -863,7 +664,7 @@ Graph3d.prototype._readData = function(data) {
}
else {
// no filtering. load all data
this.dataPoints = this._getDataPoints(this.dataTable);
this.dataPoints = this._getDataPoints(this.dataGroup.getDataTable());
}
// draw the filter
@ -901,9 +702,7 @@ Graph3d.prototype.setOptions = function (options) {
this._setSize(this.width, this.height);
// re-load the data
if (this.dataTable) {
this.setData(this.dataTable);
}
this.dataGroup.reload();
// start animation when option is true
if (this.animationAutoStart && this.dataFilter) {
@ -1138,6 +937,7 @@ Graph3d.prototype._redrawLegend = function() {
ctx.fillText(label, right, bottom + this.margin);
};
/**
* Redraw the filter
*/
@ -2335,6 +2135,20 @@ Graph3d.prototype._dataPointFromXY = function (x, y) {
return closestDataPoint;
};
/**
* Determine if the given style has bars
*
* @param {number} style the style to check
* @returns {boolean} true if bar style, false otherwise
*/
Graph3d.prototype.hasBars = function(style) {
return style == Graph3d.STYLE.BAR ||
style == Graph3d.STYLE.BARCOLOR ||
style == Graph3d.STYLE.BARSIZE;
};
/**
* Display a tooltip for given data point
* @param {Object} dataPoint

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

@ -66,6 +66,10 @@ class CachedImage {
* This methods takes the resizing out of the drawing loop, in order to
* reduce performance overhead.
*
* TODO: The code assumes that a 2D context can always be gotten. This is
* not necessarily true! OTOH, if not true then usage of this class
* is senseless.
*
* @private
*/
_fillMipMap() {

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

@ -55,13 +55,27 @@ function Network(container, data, options) {
};
util.extend(this.options, this.defaultOptions);
// containers for nodes and edges
/**
* Containers for nodes and edges.
*
* 'edges' and 'nodes' contain the full definitions of all the network elements.
* 'nodeIndices' and 'edgeIndices' contain the id's of the active elements.
*
* The distinction is important, because a defined node need not be active, i.e.
* visible on the canvas. This happens in particular when clusters are defined, in
* that case there will be nodes and edges not displayed.
* The bottom line is that all code with actions related to visibility, *must* use
* 'nodeIndices' and 'edgeIndices', not 'nodes' and 'edges' directly.
*/
this.body = {
container: container,
// See comment above for following fields
nodes: {},
nodeIndices: [],
edges: {},
edgeIndices: [],
emitter: {
on: this.on.bind(this),
off: this.off.bind(this),

+ 30
- 2
lib/network/dotparser.js View File

@ -10,6 +10,29 @@
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*
* -------------------------------------------
* TODO
* ====
*
* For label handling, this is an incomplete implementation. From docs (quote #3015):
*
* > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
* > left-justified, and right-justified, respectively.
*
* Source: http://www.graphviz.org/content/attrs#kescString
*
* > As another aid for readability, dot allows double-quoted strings to span multiple physical
* > lines using the standard C convention of a backslash immediately preceding a newline
* > character
* > In addition, double-quoted strings can be concatenated using a '+' operator.
* > As HTML strings can contain newline characters, which are used solely for formatting,
* > the language does not allow escaped newlines or concatenation operators to be used
* > within them.
*
* - Currently, only '\\n' is handled
* - Note that text explicitly says 'labels'; the dot parser currently handles escape
* sequences in **all** strings.
*/
function parseDOT (data) {
dot = data;
@ -358,9 +381,14 @@ function getToken() {
if (c === '"') {
next();
while (c != '' && (c != '"' || (c === '"' && nextPreview() === '"'))) {
token += c;
if (c === '"') { // skip the escape character
if (c === '"') { // skip the escape character
token += c;
next();
} else if (c === '\\' && nextPreview() === 'n') { // Honor a newline escape sequence
token += '\n';
next();
} else {
token += c;
}
next();
}

+ 51
- 7
lib/network/modules/Canvas.js View File

@ -188,9 +188,8 @@ class Canvas {
this.frame.canvas.appendChild(noCanvas);
}
else {
let ctx = this.frame.canvas.getContext("2d");
this._setPixelRatio(ctx);
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
this._setPixelRatio();
this.setTransform();
}
// add the frame to the container element
@ -257,9 +256,19 @@ class Canvas {
let oldHeight = this.frame.canvas.height;
// update the pixel ratio
let ctx = this.frame.canvas.getContext("2d");
//
// NOTE: Comment in following is rather inconsistent; this is the ONLY place in the code
// where it is assumed that the pixel ratio could change at runtime.
// The only way I can think of this happening is a rotating screen or tablet; but then
// there should be a mechanism for reloading the data (TODO: check if this is present).
//
// If the assumption is true (i.e. pixel ratio can change at runtime), then *all* usage
// of pixel ratio must be overhauled for this.
//
// For the time being, I will humor the assumption here, and in the rest of the code assume it is
// constant.
let previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value
this._setPixelRatio(ctx);
this._setPixelRatio();
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
this._getCameraState(previousRatio);
@ -324,11 +333,23 @@ class Canvas {
};
getContext() {
return this.frame.canvas.getContext("2d");
}
/**
* Determine the pixel ratio for various browsers.
*
* @private
*/
_setPixelRatio(ctx) {
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
_determinePixelRatio() {
let ctx = this.getContext();
if (ctx === undefined) {
throw "Could not get canvax context";
}
return (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
@ -336,6 +357,29 @@ class Canvas {
}
/**
* Lazy determination of pixel ratio.
*
* @private
*/
_setPixelRatio() {
this.pixelRatio = this._determinePixelRatio();
}
/**
* Set the transform in the contained context, based on its pixelRatio
*/
setTransform() {
let ctx = this.getContext();
if (ctx === undefined) {
throw "Could not get canvax context";
}
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
}
/**
* 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)

+ 9
- 23
lib/network/modules/CanvasRenderer.js View File

@ -16,7 +16,6 @@ class CanvasRenderer {
this.requiresTimeout = true;
this.renderingActive = false;
this.renderRequests = 0;
this.pixelRatio = undefined;
this.allowRedraw = true;
this.dragging = false;
@ -61,7 +60,7 @@ class CanvasRenderer {
clearTimeout(this.renderTimer);
}
else {
cancelAnimationFrame(this.renderTimer);
window.cancelAnimationFrame(this.renderTimer);
}
this.body.emitter.off();
});
@ -138,20 +137,15 @@ class CanvasRenderer {
this.body.emitter.emit("initRedraw");
this.redrawRequested = false;
let ctx = this.canvas.frame.canvas.getContext('2d');
// when the container div was hidden, this fixes it back up!
if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) {
this.canvas.setSize();
}
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
this.canvas.setTransform();
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
let ctx = this.canvas.getContext();
// clear the canvas
let w = this.canvas.frame.canvas.clientWidth;
@ -198,21 +192,14 @@ class CanvasRenderer {
/**
* Redraw all nodes
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
*
* @param {CanvasRenderingContext2D} ctx
* @param {Boolean} [alwaysShow]
* @private
*/
_resizeNodes() {
let ctx = this.canvas.frame.canvas.getContext('2d');
if (this.pixelRatio === undefined) {
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
}
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
this.canvas.setTransform();
let ctx = this.canvas.getContext();
ctx.save();
ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
ctx.scale(this.body.view.scale, this.body.view.scale);
@ -235,8 +222,8 @@ class CanvasRenderer {
/**
* Redraw all nodes
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
*
* @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
* @param {Boolean} [alwaysShow]
* @private
*/
@ -283,8 +270,7 @@ class CanvasRenderer {
/**
* Redraw all edges
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
* @private
*/
_drawEdges(ctx) {

+ 14
- 4
lib/network/modules/Clustering.js View File

@ -689,22 +689,32 @@ class ClusterEngine {
/**
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
*
* If a node can't be found in the chain, return an empty array.
*
* @param {string|number} nodeId
* @returns {Array}
*/
findNode(nodeId) {
let stack = [];
let max = 100;
let counter = 0;
let node;
while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
stack.push(this.body.nodes[nodeId].id);
node = this.body.nodes[nodeId]
if (node === undefined) return [];
stack.push(node.id);
nodeId = this.clusteredNodes[nodeId].clusterId;
counter++;
}
stack.push(this.body.nodes[nodeId].id);
stack.reverse();
node = this.body.nodes[nodeId]
if (node === undefined) return [];
stack.push(node.id);
stack.reverse();
return stack;
}

+ 16
- 4
lib/network/modules/EdgesHandler.js View File

@ -4,6 +4,7 @@ var DataView = require('../../DataView');
var Edge = require("./components/Edge").default;
var Label = require("./components/shared/Label").default;
var LayoutEngine = require("./LayoutEngine").default; // For access to LayoutEngine.getStaticType()
class EdgesHandler {
constructor(body, images, groups) {
@ -115,11 +116,11 @@ class EdgesHandler {
bindEventListeners() {
// this allows external modules to force all dynamic curves to turn static.
this.body.emitter.on("_forceDisableDynamicCurves", (type) => {
this.body.emitter.on("_forceDisableDynamicCurves", (type, emit = true) => {
if (type === 'dynamic') {
type = 'continuous';
}
let emitChange = false;
let dataChanged = false;
for (let edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
let edge = this.body.edges[edgeId];
@ -137,18 +138,25 @@ class EdgesHandler {
else {
edge.setOptions({smooth: {type: type}});
}
emitChange = true;
dataChanged = true;
}
}
}
}
}
if (emitChange === true) {
if (emit === true && dataChanged === true) {
this.body.emitter.emit("_dataChanged");
}
});
// this is called when options of EXISTING nodes or edges have changed.
//
// NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.
// See update() for logic.
// TODO: Verify and examine the consequences of this. It might still trigger when
// non-option fields have changed, but then reconnecting edges is still useless.
// Alternatively, it might also be called when edges are removed.
//
this.body.emitter.on("_dataUpdated", () => {
this.reconnectEdges();
});
@ -247,6 +255,7 @@ class EdgesHandler {
this.add(ids, true);
}
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
if (doNotEmit === false) {
this.body.emitter.emit("_dataChanged");
}
@ -274,6 +283,8 @@ class EdgesHandler {
edges[id] = this.create(data);
}
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
if (doNotEmit === false) {
this.body.emitter.emit("_dataChanged");
}
@ -308,6 +319,7 @@ class EdgesHandler {
}
if (dataChanged === true) {
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
this.body.emitter.emit("_dataChanged");
}
else {

+ 42
- 56
lib/network/modules/InteractionHandler.js View File

@ -170,60 +170,53 @@ class InteractionHandler {
/**
* Select and deselect nodes depending current selection change.
*
* For changing nodes, select/deselect events are fired.
*
* NOTE: For a given edge, if one connecting node is deselected and with the same
* click the other node is selected, no events for the edge will fire.
* It was selected and it will remain selected.
*
* TODO: This is all SelectionHandler calls; the method should be moved to there.
*
* @param pointer
* @param add
*/
checkSelectionChanges(pointer, event, add = false) {
let previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount();
let previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount();
let previousSelection = this.selectionHandler.getSelection();
let selected;
let selected = false;
if (add === true) {
selected = this.selectionHandler.selectAdditionalOnPoint(pointer);
}
else {
selected = this.selectionHandler.selectOnPoint(pointer);
}
let selectedEdgesCount = this.selectionHandler._getSelectedEdgeCount();
let selectedNodesCount = this.selectionHandler._getSelectedNodeCount();
let currentSelection = this.selectionHandler.getSelection();
let {nodesChanged, edgesChanged} = this._determineIfDifferent(previousSelection, currentSelection);
let nodeSelected = false;
// See NOTE in method comment for the reason to do it like this
let deselectedItems = this._determineDifference(previousSelection, currentSelection);
let selectedItems = this._determineDifference(currentSelection , previousSelection);
if (selectedNodesCount - previouslySelectedNodeCount > 0) { // node was selected
this.selectionHandler._generateClickEvent('selectNode', event, pointer);
selected = true;
nodeSelected = true;
}
else if (nodesChanged === true && selectedNodesCount > 0) {
this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
this.selectionHandler._generateClickEvent('selectNode', event, pointer);
nodeSelected = true;
if (deselectedItems.edges.length > 0) {
this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
selected = true;
}
else if (selectedNodesCount - previouslySelectedNodeCount < 0) { // node was deselected
if (deselectedItems.nodes.length > 0) {
this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
selected = true;
}
// handle the selected edges
if (selectedEdgesCount - previouslySelectedEdgeCount > 0 && nodeSelected === false) { // edge was selected
this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
if (selectedItems.nodes.length > 0) {
this.selectionHandler._generateClickEvent('selectNode', event, pointer);
selected = true;
}
else if (selectedEdgesCount > 0 && edgesChanged === true) {
this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
if (selectedItems.edges.length > 0) {
this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
selected = true;
}
else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { // edge was deselected
this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
selected = true;
}
// fire the select event if anything has been selected or deselected
if (selected === true) { // select or unselect
@ -233,38 +226,31 @@ class InteractionHandler {
/**
* This function checks if the nodes and edges previously selected have changed.
* @param previousSelection
* @param currentSelection
* @returns {{nodesChanged: boolean, edgesChanged: boolean}}
* Remove all node and edge id's from the first set that are present in the second one.
*
* @param firstSet
* @param secondSet
* @returns {{nodes: array, edges: array}}
* @private
*/
_determineIfDifferent(previousSelection,currentSelection) {
let nodesChanged = false;
let edgesChanged = false;
for (let i = 0; i < previousSelection.nodes.length; i++) {
if (currentSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
nodesChanged = true;
}
}
for (let i = 0; i < currentSelection.nodes.length; i++) {
if (previousSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
nodesChanged = true;
}
}
for (let i = 0; i < previousSelection.edges.length; i++) {
if (currentSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
edgesChanged = true;
}
}
for (let i = 0; i < currentSelection.edges.length; i++) {
if (previousSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
edgesChanged = true;
_determineDifference(firstSet, secondSet) {
let arrayDiff = function(firstArr, secondArr) {
let result = [];
for (let i = 0; i < firstArr.length; i++) {
let value = firstArr[i];
if (secondArr.indexOf(value) === -1) {
result.push(value);
}
}
}
return {nodesChanged, edgesChanged};
return result;
};
return {
nodes: arrayDiff(firstSet.nodes, secondSet.nodes),
edges: arrayDiff(firstSet.edges, secondSet.edges)
};
}

+ 259
- 147
lib/network/modules/LayoutEngine.js View File

@ -207,35 +207,47 @@ class LayoutEngine {
this.body.emitter.on('_resetHierarchicalLayout', () => {
this.setupHierarchicalLayout();
});
this.body.emitter.on('_adjustEdgesForHierarchicalLayout', () => {
if (this.options.hierarchical.enabled !== true) {
return;
}
// get the type of static smooth curve in case it is required
let type = this.getStaticType();
// force all edges into static smooth curves.
this.body.emitter.emit('_forceDisableDynamicCurves', type, false);
});
}
setOptions(options, allOptions) {
if (options !== undefined) {
let prevHierarchicalState = this.options.hierarchical.enabled;
let hierarchical = this.options.hierarchical;
let prevHierarchicalState = hierarchical.enabled;
util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options);
util.mergeOptions(this.options, options, 'hierarchical');
if (options.randomSeed !== undefined) {this.initialRandomSeed = options.randomSeed;}
if (this.options.hierarchical.enabled === true) {
if (hierarchical.enabled === true) {
if (prevHierarchicalState === true) {
// refresh the overridden options for nodes and edges.
this.body.emitter.emit('refresh', true);
}
// 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;
if (hierarchical.direction === 'RL' || hierarchical.direction === 'DU') {
if (hierarchical.levelSeparation > 0) {
hierarchical.levelSeparation *= -1;
}
}
else {
if (this.options.hierarchical.levelSeparation < 0) {
this.options.hierarchical.levelSeparation *= -1;
if (hierarchical.levelSeparation < 0) {
hierarchical.levelSeparation *= -1;
}
}
this.body.emitter.emit('_resetHierarchicalLayout');
// because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed.
// because the hierarchical system needs it's own physics and smooth curve settings,
// we adapt the other options if needed.
return this.adaptAllOptionsForHierarchicalLayout(allOptions);
}
else {
@ -251,32 +263,32 @@ class LayoutEngine {
adaptAllOptionsForHierarchicalLayout(allOptions) {
if (this.options.hierarchical.enabled === true) {
let backupPhysics = this.optionsBackup.physics;
// set the physics
if (allOptions.physics === undefined || allOptions.physics === true) {
allOptions.physics = {
enabled:this.optionsBackup.physics.enabled === undefined ? true : this.optionsBackup.physics.enabled,
solver:'hierarchicalRepulsion'
enabled: backupPhysics.enabled === undefined ? true : backupPhysics.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';
backupPhysics.enabled = backupPhysics.enabled === undefined ? true : backupPhysics.enabled;
backupPhysics.solver = backupPhysics.solver || 'barnesHut';
}
else if (typeof allOptions.physics === 'object') {
this.optionsBackup.physics.enabled = allOptions.physics.enabled === undefined ? true : allOptions.physics.enabled;
this.optionsBackup.physics.solver = allOptions.physics.solver || 'barnesHut';
backupPhysics.enabled = allOptions.physics.enabled === undefined ? true : allOptions.physics.enabled;
backupPhysics.solver = allOptions.physics.solver || 'barnesHut';
allOptions.physics.solver = 'hierarchicalRepulsion';
}
else if (allOptions.physics !== false) {
this.optionsBackup.physics.solver ='barnesHut';
backupPhysics.solver ='barnesHut';
allOptions.physics = {solver:'hierarchicalRepulsion'};
}
// get the type of static smooth curve in case it is required
let type = 'horizontal';
if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') {
type = 'vertical';
}
let type = this.getStaticType();
// disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves.
// disable smooth curves if nothing is defined. If smooth curves have been turned on,
// turn them into static smooth curves.
if (allOptions.edges === undefined) {
this.optionsBackup.edges = {smooth:{enabled:true, type:'dynamic'}};
allOptions.edges = {smooth: false};
@ -291,27 +303,34 @@ class LayoutEngine {
allOptions.edges.smooth = {enabled: allOptions.edges.smooth, type:type}
}
else {
let smooth = allOptions.edges.smooth;
// allow custom types except for dynamic
if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') {
type = allOptions.edges.smooth.type;
if (smooth.type !== undefined && smooth.type !== 'dynamic') {
type = smooth.type;
}
// TODO: this is options merging; see if the standard routines can be used here.
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,
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
smooth : smooth.enabled === undefined ? true : smooth.enabled,
type : smooth.type === undefined ? 'dynamic': smooth.type,
roundness : smooth.roundness === undefined ? 0.5 : smooth.roundness,
forceDirection: smooth.forceDirection === undefined ? false : smooth.forceDirection
};
// NOTE: Copying an object to self; this is basically setting defaults for undefined variables
allOptions.edges.smooth = {
enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
type:type,
roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
enabled : smooth.enabled === undefined ? true : smooth.enabled,
type : type,
roundness : smooth.roundness === undefined ? 0.5 : smooth.roundness,
forceDirection: smooth.forceDirection === undefined ? false: smooth.forceDirection
}
}
}
// force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth.
// 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);
}
@ -347,22 +366,25 @@ class LayoutEngine {
*/
layoutNetwork() {
if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) {
let indices = this.body.nodeIndices;
// 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++) {
let node = this.body.nodes[this.body.nodeIndices[i]];
for (let i = 0; i < indices.length; i++) {
let node = this.body.nodes[indices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
}
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
if (positionDefined < 0.5 * indices.length) {
let MAX_LEVELS = 10;
let level = 0;
let clusterThreshold = 150;
//Performance enhancement, during clustering edges need only be simple straight lines. These options don't propagate outside the clustering phase.
// Performance enhancement, during clustering edges need only be simple straight lines.
// These options don't propagate outside the clustering phase.
let clusterOptions = {
clusterEdgeProperties:{
smooth: {
@ -372,12 +394,15 @@ class LayoutEngine {
};
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) {
let startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold && level <= MAX_LEVELS) {
// NOTE: this part fails to find clusters for large scale-free networks, which should
// be easily clusterable.
// TODO: examine why this is so
if (indices.length > clusterThreshold) {
let startLength = indices.length;
while (indices.length > clusterThreshold && level <= MAX_LEVELS) {
//console.time("clustering")
level += 1;
let before = this.body.nodeIndices.length;
let before = indices.length;
// if there are many nodes we do a hubsize cluster
if (level % 3 === 0) {
this.body.modules.clustering.clusterBridges(clusterOptions);
@ -385,11 +410,12 @@ class LayoutEngine {
else {
this.body.modules.clustering.clusterOutliers(clusterOptions);
}
let after = this.body.nodeIndices.length;
let after = indices.length;
if (before == after && level % 3 !== 0) {
this._declusterAll();
this.body.emitter.emit("_layoutFailed");
console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance.");
console.info("This network could not be positioned by this version of the improved layout algorithm."
+ " Please disable improvedLayout for better performance.");
return;
}
//console.timeEnd("clustering")
@ -399,22 +425,24 @@ class LayoutEngine {
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)})
}
if (level > MAX_LEVELS){
console.info("The clustering didn't succeed within the amount of interations allowed, progressing with partial result.");
console.info("The clustering didn't succeed within the amount of interations allowed,"
+ " progressing with partial result.");
}
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
this.body.modules.kamadaKawai.solve(indices, this.body.edgeIndices, true);
// shift to center point
this._shiftToCenter();
// perturb the nodes a little bit to force the physics to kick in
let offset = 70;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
for (let i = 0; i < indices.length; i++) {
// Only perturb the nodes that aren't fixed
if (this.body.nodes[this.body.nodeIndices[i]].predefinedPosition === false) {
this.body.nodes[this.body.nodeIndices[i]].x += (0.5 - this.seededRandom())*offset;
this.body.nodes[this.body.nodeIndices[i]].y += (0.5 - this.seededRandom())*offset;
let node = this.body.nodes[indices[i]];
if (node.predefinedPosition === false) {
node.x += (0.5 - this.seededRandom())*offset;
node.y += (0.5 - this.seededRandom())*offset;
}
}
@ -435,8 +463,9 @@ class LayoutEngine {
let range = NetworkUtil.getRangeCore(this.body.nodes, this.body.nodeIndices);
let center = NetworkUtil.findCenter(range);
for (let i = 0; i < this.body.nodeIndices.length; i++) {
this.body.nodes[this.body.nodeIndices[i]].x -= center.x;
this.body.nodes[this.body.nodeIndices[i]].y -= center.y;
let node = this.body.nodes[this.body.nodeIndices[i]];
node.x -= center.x;
node.y -= center.y;
}
}
@ -500,18 +529,20 @@ class LayoutEngine {
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel === true && definedLevel === true) {
throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.');
throw new Error('To use the hierarchical layout, nodes require either no predefined levels'
+ ' or levels have to be defined for all nodes.');
}
else {
// define levels if undefined by the users. Based on hubsize.
if (undefinedLevel === true) {
if (this.options.hierarchical.sortMethod === 'hubsize') {
let sortMethod = this.options.hierarchical.sortMethod;
if (sortMethod === 'hubsize') {
this._determineLevelsByHubsize();
}
else if (this.options.hierarchical.sortMethod === 'directed') {
else if (sortMethod === 'directed') {
this._determineLevelsDirected();
}
else if (this.options.hierarchical.sortMethod === 'custom') {
else if (sortMethod === 'custom') {
this._determineLevelsCustomCallback();
}
}
@ -686,8 +717,9 @@ class LayoutEngine {
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 nodeSpacing = this.options.hierarchical.nodeSpacing;
//console.log("NOW CHECKING:", node1.id, node2.id, diffAbs);
if (diffAbs > nodeSpacing) {
let branchNodes1 = {};
let branchNodes2 = {};
@ -699,12 +731,13 @@ class LayoutEngine {
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);
//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;
if (diffBranch > nodeSpacing) {
let offset = max1 - min2 + nodeSpacing;
if (offset < -minSpace2 + nodeSpacing) {
offset = -minSpace2 + nodeSpacing;
//console.log("RESETTING OFFSET", max1 - min2 + this.options.hierarchical.nodeSpacing, -minSpace2, offset);
}
if (offset < 0) {
@ -939,18 +972,19 @@ class LayoutEngine {
if (level !== undefined) {
let index = this.distributionIndex[node.id];
let position = this._getPositionForHierarchy(node);
let ordering = this.distributionOrdering[level];
let minSpace = 1e9;
let maxSpace = 1e9;
if (index !== 0) {
let prevNode = this.distributionOrdering[level][index - 1];
let prevNode = ordering[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 (index != ordering.length - 1) {
let nextNode = ordering[index + 1];
if ((useMap === true && map[nextNode.id] === undefined) || useMap === false) {
let nextPos = this._getPositionForHierarchy(nextNode);
maxSpace = Math.min(maxSpace, nextPos - position);
@ -975,24 +1009,17 @@ class LayoutEngine {
for (var i = 0; i < parents.length; i++) {
let parentId = parents[i];
let parentNode = this.body.nodes[parentId];
if (this.hierarchical.childrenReference[parentId]) {
let children = this.hierarchical.childrenReference[parentId];
if (children !== undefined) {
// get the range of the children
let minPos = 1e9;
let maxPos = -1e9;
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]];
minPos = Math.min(minPos, this._getPositionForHierarchy(childNode));
maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode));
}
}
let newPosition = this._getCenterPosition(children);
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)) {
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);
}
}
@ -1022,9 +1049,13 @@ class LayoutEngine {
for (let i = 0; i < nodeArray.length; i++) {
let node = nodeArray[i];
if (this.positionedNodes[node.id] === undefined) {
let pos = this.options.hierarchical.nodeSpacing * handledNodeCount;
// 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;}
let spacing = this.options.hierarchical.nodeSpacing;
let pos = spacing * handledNodeCount;
// 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]) + spacing;
}
this._setPositionForHierarchy(node, pos, level);
this._validatePositionAndContinue(node, level, pos);
@ -1045,15 +1076,17 @@ class LayoutEngine {
* @private
*/
_placeBranchNodes(parentId, parentLevel) {
let childRef = this.hierarchical.childrenReference[parentId];
// if this is not a parent, cancel the placing. This can happen with multiple parents to one child.
if (this.hierarchical.childrenReference[parentId] === undefined) {
if (childRef === undefined) {
return;
}
// get a list of childNodes
let childNodes = [];
for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) {
childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]);
for (let i = 0; i < childRef.length; i++) {
childNodes.push(this.body.nodes[childRef[i]]);
}
// use the positions to order the nodes.
@ -1066,11 +1099,13 @@ class LayoutEngine {
// 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 spacing = this.options.hierarchical.nodeSpacing;
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
// 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.options.hierarchical.nodeSpacing;}
else {pos = this._getPositionForHierarchy(childNodes[i-1]) + spacing;}
this._setPositionForHierarchy(childNode, pos, childNodeLevel);
this._validatePositionAndContinue(childNode, childNodeLevel, pos);
}
@ -1080,14 +1115,8 @@ class LayoutEngine {
}
// center the parent nodes.
let minPos = 1e9;
let maxPos = -1e9;
for (let i = 0; i < childNodes.length; i++) {
let childNodeId = childNodes[i].id;
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), parentLevel);
let center = this._getCenterPosition(childNodes);
this._setPositionForHierarchy(this.body.nodes[parentId], center, parentLevel);
}
@ -1123,8 +1152,8 @@ class LayoutEngine {
}
/**
* Receives an array with node indices and returns an array with the actual node references. Used for sorting based on
* node properties.
* Receives an array with node indices and returns an array with the actual node references.
* Used for sorting based on node properties.
* @param idArray
*/
_indexArrayToNodes(idArray) {
@ -1145,13 +1174,14 @@ class LayoutEngine {
let distribution = {};
let nodeId, node;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// we fix Y because the hierarchy is vertical,
// we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId];
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
if(this._isVertical()) {
node.y = this.options.hierarchical.levelSeparation * level;
node.options.fixed.y = true;
}
@ -1170,50 +1200,78 @@ class LayoutEngine {
/**
* Get the hubsize from all remaining unlevelled nodes.
* Return the active (i.e. visible) edges for this node
*
* @returns {number}
* @returns {array} Array of edge instances
* @private
*/
_getHubSize() {
let hubSize = 0;
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (this.hierarchical.levels[nodeId] === undefined) {
hubSize = node.edges.length < hubSize ? hubSize : node.edges.length;
}
_getActiveEdges(node) {
let result = [];
for (let j in node.edges) {
let edge = node.edges[j];
if (this.body.edgeIndices.indexOf(edge.id) !== -1) {
result.push(edge);
}
}
return hubSize;
return result;
}
/**
* Get the hubsizes for all active nodes.
*
* @returns {number}
* @private
*/
_getHubSizes() {
let hubSizes = {};
let nodeIds = this.body.nodeIndices;
for (let i in nodeIds) {
let nodeId = nodeIds[i];
let node = this.body.nodes[nodeId];
let hubSize = this._getActiveEdges(node).length;
hubSizes[hubSize] = true;
}
// Make an array of the size sorted descending
let result = [];
for (let size in hubSizes) {
result.push(Number(size));
}
result.sort(function(a, b) {
return b - a;
});
return result;
}
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
_determineLevelsByHubsize() {
let hubSize = 1;
let levelDownstream = (nodeA, nodeB) => {
this.hierarchical.levelDownstream(nodeA, nodeB);
}
while (hubSize > 0) {
// determine hubs
hubSize = this._getHubSize();
if (hubSize === 0)
break;
let hubSizes = this._getHubSizes();
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (node.edges.length === hubSize) {
this._crawlNetwork(levelDownstream,nodeId);
}
for (let i = 0; i < hubSizes.length; ++i ) {
let hubSize = hubSizes[i];
if (hubSize === 0) break;
let nodeIds = this.body.nodeIndices;
for (let j in nodeIds) {
let nodeId = nodeIds[j];
let node = this.body.nodes[nodeId];
if (hubSize === this._getActiveEdges(node).length) {
this._crawlNetwork(levelDownstream, nodeId);
}
}
}
@ -1239,7 +1297,7 @@ class LayoutEngine {
let levelByDirection = (nodeA, nodeB, edge) => {
let levelA = this.hierarchical.levels[nodeA.id];
// set initial level
if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;}
if (levelA === undefined) { levelA = this.hierarchical.levels[nodeA.id] = minLevel;}
let diff = customCallback(
NetworkUtil.cloneOptions(nodeA,'node'),
@ -1247,7 +1305,7 @@ class LayoutEngine {
NetworkUtil.cloneOptions(edge,'edge')
);
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + diff;
this.hierarchical.levels[nodeB.id] = levelA + diff;
};
this._crawlNetwork(levelByDirection);
@ -1266,12 +1324,12 @@ class LayoutEngine {
let levelByDirection = (nodeA, nodeB, edge) => {
let levelA = this.hierarchical.levels[nodeA.id];
// set initial level
if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;}
if (levelA === undefined) { levelA = this.hierarchical.levels[nodeA.id] = minLevel;}
if (edge.toId == nodeB.id) {
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + 1;
this.hierarchical.levels[nodeB.id] = levelA + 1;
}
else {
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] - 1;
this.hierarchical.levels[nodeB.id] = levelA - 1;
}
};
@ -1298,7 +1356,7 @@ class LayoutEngine {
/**
* 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 callback | will receive nodeA, nodeB and the connecting edge. A and B are distinct.
* @param startingNodeId
* @private
*/
@ -1316,17 +1374,19 @@ class LayoutEngine {
progress[node.id] = true;
let childNode;
for (let i = 0; i < node.edges.length; i++) {
if (node.edges[i].connected === true) {
if (node.edges[i].toId === node.id) {
childNode = node.edges[i].from;
let edges = this._getActiveEdges(node);
for (let i = 0; i < edges.length; i++) {
let edge = edges[i];
if (edge.connected === true) {
if (edge.toId == node.id) { // '==' because id's can be string and numeric
childNode = edge.from;
}
else {
childNode = node.edges[i].to;
childNode = edge.to;
}
if (node.id !== childNode.id) {
callback(node, childNode, node.edges[i]);
if (node.id != childNode.id) { // '!=' because id's can be string and numeric
callback(node, childNode, edge);
crawler(childNode, tree);
}
}
@ -1369,15 +1429,17 @@ class LayoutEngine {
return;
}
progress[parentId] = true;
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
if(this._isVertical()) {
this.body.nodes[parentId].x += diff;
}
else {
this.body.nodes[parentId].y += diff;
}
if (this.hierarchical.childrenReference[parentId] !== undefined) {
for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) {
shifter(this.hierarchical.childrenReference[parentId][i]);
let childRef = this.hierarchical.childrenReference[parentId];
if (childRef !== undefined) {
for (let i = 0; i < childRef.length; i++) {
shifter(childRef[i]);
}
}
};
@ -1395,18 +1457,20 @@ class LayoutEngine {
_findCommonParent(childA,childB) {
let parents = {};
let iterateParents = (parents,child) => {
if (this.hierarchical.parentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) {
let parent = this.hierarchical.parentReference[child][i];
let parentRef = this.hierarchical.parentReference[child];
if (parentRef !== undefined) {
for (let i = 0; i < parentRef.length; i++) {
let parent = parentRef[i];
parents[parent] = true;
iterateParents(parents, parent)
}
}
};
let findParent = (parents, child) => {
if (this.hierarchical.parentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) {
let parent = this.hierarchical.parentReference[child][i];
let parentRef = this.hierarchical.parentReference[child];
if (parentRef !== undefined) {
for (let i = 0; i < parentRef.length; i++) {
let parent = parentRef[i];
if (parents[parent] !== undefined) {
return {foundParent:parent, withChild:child};
}
@ -1445,7 +1509,7 @@ class LayoutEngine {
this.distributionOrderingPresence[level][node.id] = true;
}
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
if(this._isVertical()) {
node.x = position;
}
else {
@ -1472,7 +1536,7 @@ class LayoutEngine {
* @private
*/
_getPositionForHierarchy(node) {
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
if(this._isVertical()) {
return node.x;
}
else {
@ -1487,7 +1551,7 @@ class LayoutEngine {
*/
_sortNodeArray(nodeArray) {
if (nodeArray.length > 1) {
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
if(this._isVertical()) {
nodeArray.sort(function (a, b) {
return a.x - b.x;
})
@ -1499,6 +1563,54 @@ class LayoutEngine {
}
}
}
/**
* Get the type of static smooth curve in case it is required.
*
* The return value is the type to use to translate dynamic curves to
* another type, in the case of hierarchical layout. Dynamic curves do
* not work for that layout type.
*/
getStaticType() {
// Node that 'type' is the edge type, and therefore 'orthogonal' to the layout type.
let type = 'horizontal';
if (!this._isVertical()) {
type = 'vertical';
}
return type;
}
/**
* Determine the center position of a branch from the passed list of child nodes
*
* This takes into account the positions of all the child nodes.
* @param childNodes {array} Array of either child nodes or node id's
* @return {number}
* @private
*/
_getCenterPosition(childNodes) {
let minPos = 1e9;
let maxPos = -1e9;
for (let i = 0; i < childNodes.length; i++) {
let childNode;
if (childNodes[i].id !== undefined) {
childNode = childNodes[i];
} else {
let childNodeId = childNodes[i];
childNode = this.body.nodes[childNodeId];
}
let position = this._getPositionForHierarchy(childNode);
minPos = Math.min(minPos, position);
maxPos = Math.max(maxPos, position);
}
return 0.5 * (minPos + maxPos);
}
}
export default LayoutEngine;

+ 11
- 3
lib/network/modules/NodesHandler.js View File

@ -129,6 +129,12 @@ class NodesHandler {
x: undefined,
y: undefined
};
// Protect from idiocy
if (this.defaultOptions.mass <= 0) {
throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative';
}
util.extend(this.options, this.defaultOptions);
this.bindEventListeners();
@ -408,22 +414,24 @@ class NodesHandler {
/**
* Get the Ids of nodes connected to this node.
* @param nodeId
* @param direction {String|undefined} values 'from' and 'to' select respectively parent and child nodes only.
* Any other value returns both parent and child nodes.
* @returns {Array}
*/
getConnectedNodes(nodeId) {
getConnectedNodes(nodeId, direction) {
let nodeList = [];
if (this.body.nodes[nodeId] !== undefined) {
let node = this.body.nodes[nodeId];
let nodeObj = {}; // used to quickly check if node already exists
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
if (edge.toId == node.id) { // these are double equals since ids can be numeric or string
if (direction !== 'from' && edge.toId == node.id) { // these are double equals since ids can be numeric or string
if (nodeObj[edge.fromId] === undefined) {
nodeList.push(edge.fromId);
nodeObj[edge.fromId] = true;
}
}
else if (edge.fromId == node.id) { // these are double equals since ids can be numeric or string
else if (direction !== 'to' && edge.fromId == node.id) { // these are double equals since ids can be numeric or string
if (nodeObj[edge.toId] === undefined) {
nodeList.push(edge.toId);
nodeObj[edge.toId] = true;

+ 14
- 3
lib/network/modules/PhysicsEngine.js View File

@ -638,20 +638,31 @@ class PhysicsEngine {
* @private
*/
_stabilizationBatch() {
var self = this;
var running = () => (self.stabilized === false && self.stabilizationIterations < self.targetIterations);
var sendProgress = () => {
self.body.emitter.emit('stabilizationProgress', {
iterations: self.stabilizationIterations,
total: self.targetIterations
});
};
// this is here to ensure that there is at least one start event.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
sendProgress();
}
var count = 0;
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
while (running() && count < this.options.stabilization.updateInterval) {
this.physicsTick();
count++;
}
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) {
this.body.emitter.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations});
sendProgress();
if (running()) {
setTimeout(this._stabilizationBatch.bind(this),0);
}
else {

+ 8
- 4
lib/network/modules/components/Edge.js View File

@ -528,7 +528,11 @@ class Edge {
// set style
var node1 = this.from;
var node2 = this.to;
var selected = (this.from.selected || this.to.selected || this.selected);
if (this.labelModule.differentState(this.selected, this.hover)) {
this.labelModule.getTextSize(ctx, this.selected, this.hover);
}
if (node1.id != node2.id) {
this.labelModule.pointToSelf = false;
var point = this.edgeType.getPoint(0.5, viaNode);
@ -536,13 +540,13 @@ class Edge {
// if the label has to be rotated:
if (this.options.font.align !== "horizontal") {
this.labelModule.calculateLabelSize(ctx, selected, this.hover, point.x, point.y);
this.labelModule.calculateLabelSize(ctx, this.selected, this.hover, point.x, point.y);
ctx.translate(point.x, this.labelModule.size.yLine);
this._rotateForLabelAlignment(ctx);
}
// draw the label
this.labelModule.draw(ctx, point.x, point.y, selected, this.hover);
this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
ctx.restore();
}
else {
@ -559,7 +563,7 @@ class Edge {
y = node1.y - node1.shape.height * 0.5;
}
point = this._pointOnCircle(x, y, radius, 0.125);
this.labelModule.draw(ctx, point.x, point.y, selected, this.hover);
this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
}
}
}

+ 22
- 0
lib/network/modules/components/Node.js View File

@ -115,6 +115,8 @@ class Node {
throw "Node must have an id";
}
Node.checkMass(options, this.id);
// set these options locally
// clear x and y positions
if (options.x !== undefined) {
@ -210,6 +212,8 @@ class Node {
];
util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
Node.checkMass(newOptions);
// merge the shadow options into the parent.
util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
@ -538,6 +542,24 @@ class Node {
this.shape.boundingBox.bottom > obj.top
);
}
/**
* Check valid values for mass
*
* The mass may not be negative or zero. If it is, reset to 1
*/
static checkMass(options, id) {
if (options.mass !== undefined && options.mass <= 0) {
let strId = '';
if (id !== undefined) {
strId = ' in node id: ' + id;
}
console.log('%cNegative or zero mass disallowed' + strId +
', setting mass to 1.' , printStyle);
options.mass = 1;
}
}
}
export default Node;

+ 1
- 14
lib/network/modules/components/edges/BezierEdgeDynamic.js View File

@ -103,20 +103,7 @@ class BezierEdgeDynamic extends BezierEdgeBase {
* @private
*/
_line(ctx, values, viaNode) {
// draw a straight line
ctx.beginPath();
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, values);
ctx.stroke();
this.disableShadow(ctx, values);
this._bezierCurve(ctx, values, viaNode);
}
getViaNode() {

+ 62
- 136
lib/network/modules/components/edges/BezierEdgeStatic.js View File

@ -11,21 +11,7 @@ class BezierEdgeStatic extends BezierEdgeBase {
* @private
*/
_line(ctx, values, viaNode) {
// draw a straight line
ctx.beginPath();
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, values);
ctx.stroke();
this.disableShadow(ctx, values);
this._bezierCurve(ctx, values, viaNode);
}
getViaNode() {
@ -39,6 +25,7 @@ class BezierEdgeStatic extends BezierEdgeBase {
* @private
*/
_getViaCoordinates() {
// Assumption: x/y coordinates in from/to always defined
let xVia = undefined;
let yVia = undefined;
let factor = this.options.smooth.roundness;
@ -46,94 +33,55 @@ class BezierEdgeStatic extends BezierEdgeBase {
let dx = Math.abs(this.from.x - this.to.x);
let dy = Math.abs(this.from.y - this.to.y);
if (type === 'discrete' || type === 'diagonalCross') {
if (Math.abs(this.from.x - this.to.x) <= Math.abs(this.from.y - this.to.y)) {
if (this.from.y >= this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dy;
yVia = this.from.y - factor * dy;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dy;
yVia = this.from.y - factor * dy;
}
}
else if (this.from.y < this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dy;
yVia = this.from.y + factor * dy;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dy;
yVia = this.from.y + factor * dy;
}
}
if (type === "discrete") {
xVia = dx < factor * dy ? this.from.x : xVia;
}
let stepX;
let stepY;
if (dx <= dy) {
stepX = stepY = factor * dy;
} else {
stepX = stepY = factor * dx;
}
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
if (this.from.y >= this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dx;
yVia = this.from.y - factor * dx;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dx;
yVia = this.from.y - factor * dx;
}
}
else if (this.from.y < this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dx;
yVia = this.from.y + factor * dx;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dx;
yVia = this.from.y + factor * dx;
}
}
if (type === "discrete") {
if (this.from.x > this.to.x) stepX = -stepX;
if (this.from.y >= this.to.y) stepY = -stepY;
xVia = this.from.x + stepX;
yVia = this.from.y + stepY;
if (type === "discrete") {
if (dx <= dy) {
xVia = dx < factor * dy ? this.from.x : xVia;
} else {
yVia = dy < factor * dx ? this.from.y : yVia;
}
}
}
else if (type === "straightCross") {
if (Math.abs(this.from.x - this.to.x) <= Math.abs(this.from.y - this.to.y)) { // up - down
xVia = this.from.x;
if (this.from.y < this.to.y) {
yVia = this.to.y - (1 - factor) * dy;
}
else {
yVia = this.to.y + (1 - factor) * dy;
}
let stepX = (1 - factor) * dx;
let stepY = (1 - factor) * dy;
if (dx <= dy) { // up - down
stepX = 0;
if (this.from.y < this.to.y) stepY = -stepY;
}
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
if (this.from.x < this.to.x) {
xVia = this.to.x - (1 - factor) * dx;
}
else {
xVia = this.to.x + (1 - factor) * dx;
}
yVia = this.from.y;
else { // left - right
if (this.from.x < this.to.x) stepX = -stepX;
stepY = 0;
}
xVia = this.to.x + stepX;
yVia = this.to.y + stepY;
}
else if (type === 'horizontal') {
if (this.from.x < this.to.x) {
xVia = this.to.x - (1 - factor) * dx;
}
else {
xVia = this.to.x + (1 - factor) * dx;
}
let stepX = (1 - factor) * dx;
if (this.from.x < this.to.x) stepX = -stepX;
xVia = this.to.x + stepX;
yVia = this.from.y;
}
else if (type === 'vertical') {
let stepY = (1 - factor) * dy;
if (this.from.y < this.to.y) stepY = -stepY;
xVia = this.from.x;
if (this.from.y < this.to.y) {
yVia = this.to.y - (1 - factor) * dy;
}
else {
yVia = this.to.y + (1 - factor) * dy;
}
yVia = this.to.y + stepY;
}
else if (type === 'curvedCW') {
dx = this.to.x - this.from.x;
@ -160,56 +108,34 @@ class BezierEdgeStatic extends BezierEdgeBase {
yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
}
else { // continuous
if (Math.abs(this.from.x - this.to.x) <= Math.abs(this.from.y - this.to.y)) {
if (this.from.y >= this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dy;
yVia = this.from.y - factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dy;
yVia = this.from.y - factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia;
}
let stepX;
let stepY;
if (dx <= dy) {
stepX = stepY = factor * dy;
} else {
stepX = stepY = factor * dx;
}
if (this.from.x > this.to.x) stepX = -stepX;
if (this.from.y >= this.to.y) stepY = -stepY;
xVia = this.from.x + stepX;
yVia = this.from.y + stepY;
if (dx <= dy) {
if (this.from.x <= this.to.x) {
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else if (this.from.y < this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dy;
yVia = this.from.y + factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dy;
yVia = this.from.y + factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia;
}
else {
xVia = this.to.x > xVia ? this.to.x : xVia;
}
}
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
else {
if (this.from.y >= this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dx;
yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dx;
yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia;
}
}
else if (this.from.y < this.to.y) {
if (this.from.x <= this.to.x) {
xVia = this.from.x + factor * dx;
yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia;
}
else if (this.from.x > this.to.x) {
xVia = this.from.x - factor * dx;
yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia;
}
yVia = this.to.y > yVia ? this.to.y : yVia;
} else {
yVia = this.to.y < yVia ? this.to.y : yVia;
}
}
}
@ -241,4 +167,4 @@ class BezierEdgeStatic extends BezierEdgeBase {
}
export default BezierEdgeStatic;
export default BezierEdgeStatic;

+ 2
- 17
lib/network/modules/components/edges/CubicBezierEdge.js View File

@ -14,22 +14,7 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
// get the coordinates of the support points.
let via1 = viaNodes[0];
let via2 = viaNodes[1];
// start drawing the line.
ctx.beginPath();
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
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.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
this.disableShadow(ctx, values);
this._bezierCurve(ctx, values, via1, via2);
}
_getViaCoordinates() {
@ -90,4 +75,4 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
}
export default CubicBezierEdge;
export default CubicBezierEdge;

+ 42
- 1
lib/network/modules/components/edges/util/BezierEdgeBase.js View File

@ -101,6 +101,47 @@ class BezierEdgeBase extends EdgeBase {
return minDistance;
}
/**
* Draw a bezier curve between two nodes
*
* The method accepts zero, one or two control points.
* Passing zero control points just draws a straight line
*
* @param {CanvasRenderingContext2D} ctx
* @param {Object} values | options for shadow drawing
* @param {Object|undefined} viaNode1 | first control point for curve drawing
* @param {Object|undefined} viaNode2 | second control point for curve drawing
*
* @protected
*/
_bezierCurve(ctx, values, viaNode1, viaNode2) {
var hasNode1 = (viaNode1 !== undefined && viaNode1.x !== undefined);
var hasNode2 = (viaNode2 !== undefined && viaNode2.x !== undefined);
ctx.beginPath();
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
if (hasNode1 && hasNode2) {
ctx.bezierCurveTo(viaNode1.x, viaNode1.y, viaNode2.x, viaNode2.y, this.toPoint.x, this.toPoint.y);
} else if (hasNode1) {
ctx.quadraticCurveTo(viaNode1.x, viaNode1.y, this.toPoint.x, this.toPoint.y);
} else {
// fallback to normal straight edge
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
this.disableShadow(ctx, values);
}
getViaNode() {
return this._getViaCoordinates();
}
}
export default BezierEdgeBase;
export default BezierEdgeBase;

+ 5
- 9
lib/network/modules/components/shared/Label.js View File

@ -778,18 +778,14 @@ class Label {
strokeWidth: this.fontOptions.strokeWidth,
strokeColor: this.fontOptions.strokeColor
};
if (mod === "normal") {
if (selected || hover) {
if ((this.fontOptions.chooser === true) && (this.elementOptions.labelHighlightBold)) {
if (selected || hover) {
if (mod === "normal" && (this.fontOptions.chooser === true) && (this.elementOptions.labelHighlightBold)) {
values.mod = 'bold';
} else if (typeof this.fontOptions.chooser === 'function') {
this.fontOptions.chooser(ctx, values, this.elementOptions.id, selected, hover);
} else {
if (typeof this.fontOptions.chooser === 'function') {
this.fontOptions.chooser(values, this.elementOptions.id, selected, hover);
}
}
} else {
if ((selected || hover) && (typeof this.fontOptions.chooser === 'function')) {
this.fontOptions.chooser(ctx, values, this.elementOptions.id, selected, hover);
}
}
ctx.font = (values.mod + " " + values.size + "px " + values.face).replace(/"/g, "");
values.font = ctx.font;

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

@ -16,7 +16,8 @@ function Range(body, options) {
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
var start = now.clone().add(-3, 'days').valueOf();
var end = now.clone().add(3, 'days').valueOf();
this.millisecondsPerPixelCache = undefined;
if(options === undefined) {
this.start = start;
this.end = end;
@ -200,6 +201,7 @@ Range.prototype.setRange = function(start, end, options, callback) {
var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null;
var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null;
this._cancelAnimation();
this.millisecondsPerPixelCache = undefined;
if (options.animation) { // true or an Object
var initStart = this.start;
@ -280,7 +282,10 @@ Range.prototype.setRange = function(start, end, options, callback) {
* Get the number of milliseconds per pixel.
*/
Range.prototype.getMillisecondsPerPixel = function() {
return (this.end - this.start) / this.body.dom.center.clientWidth;
if (this.millisecondsPerPixelCache === undefined) {
this.millisecondsPerPixelCache = (this.end - this.start) / this.body.dom.center.clientWidth;
}
return this.millisecondsPerPixelCache;
}
/**

+ 5
- 1
lib/timeline/TimeStep.js View File

@ -148,6 +148,10 @@ TimeStep.prototype.start = function() {
*/
TimeStep.prototype.roundToMinor = function() {
// round to floor
// to prevent year & month scales rounding down to the first day of week we perform this separately
if (this.scale == 'week') {
this.current.weekday(0);
}
// IMPORTANT: we have no breaks in this switch! (this is no bug)
// noinspection FallThroughInSwitchStatementJS
switch (this.scale) {
@ -155,7 +159,7 @@ TimeStep.prototype.roundToMinor = function() {
this.current.year(this.step * Math.floor(this.current.year() / this.step));
this.current.month(0);
case 'month': this.current.date(1);
case 'week': this.current.weekday(0);
case 'week': // intentional fall through
case 'day': // intentional fall through
case 'weekday': this.current.hours(0);
case 'hour': this.current.minutes(0);

+ 1
- 0
lib/timeline/component/Group.js View File

@ -534,6 +534,7 @@ Group.prototype.resetSubgroups = function() {
for (var subgroup in this.subgroups) {
if (this.subgroups.hasOwnProperty(subgroup)) {
this.subgroups[subgroup].visible = false;
this.subgroups[subgroup].height = 0;
}
}
};

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

@ -44,7 +44,10 @@ function ItemSet(body, options) {
selectable: true,
multiselect: false,
itemsAlwaysDraggable: false,
itemsAlwaysDraggable: {
item: false,
range: false,
},
editable: {
updateTime: false,
@ -336,12 +339,26 @@ ItemSet.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
var fields = [
'type', 'rtl', 'align', 'order', 'stack', 'stackSubgroups', 'selectable', 'multiselect', 'itemsAlwaysDraggable',
'type', 'rtl', 'align', 'order', 'stack', 'stackSubgroups', 'selectable', 'multiselect',
'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate',
'hide', 'snap', 'groupOrderSwap', 'showTooltips', 'tooltip', 'tooltipOnItemUpdateTime'
];
util.selectiveExtend(fields, this.options, options);
if ('itemsAlwaysDraggable' in options) {
if (typeof options.itemsAlwaysDraggable === 'boolean') {
this.options.itemsAlwaysDraggable.item = options.itemsAlwaysDraggable;
this.options.itemsAlwaysDraggable.range = false;
}
else if (typeof options.itemsAlwaysDraggable === 'object') {
util.selectiveExtend(['item', 'range'], this.options.itemsAlwaysDraggable, options.itemsAlwaysDraggable);
// only allow range always draggable when item is always draggable as well
if (! this.options.itemsAlwaysDraggable.item) {
this.options.itemsAlwaysDraggable.range = false;
}
}
}
if ('orientation' in options) {
if (typeof options.orientation === 'string') {
this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom';
@ -1285,7 +1302,7 @@ ItemSet.prototype._onDragStart = function (event) {
var me = this;
var props;
if (item && (item.selected || this.options.itemsAlwaysDraggable)) {
if (item && (item.selected || this.options.itemsAlwaysDraggable.item)) {
if (this.options.editable.overrideItems &&
!this.options.editable.updateTime &&
@ -1327,7 +1344,7 @@ ItemSet.prototype._onDragStart = function (event) {
else {
var baseGroupIndex = this._getGroupIndex(item.data.group);
var itemsToDrag = (this.options.itemsAlwaysDraggable && !item.selected) ? [item.id] : this.getSelection();
var itemsToDrag = (this.options.itemsAlwaysDraggable.item && !item.selected) ? [item.id] : this.getSelection();
this.touchParams.itemProps = itemsToDrag.map(function (id) {
var item = me.items[id];

+ 7
- 2
lib/timeline/component/item/Item.js View File

@ -169,8 +169,13 @@ Item.prototype._repaintDragCenter = function () {
});
if (this.dom.box) {
this.dom.box.appendChild(dragCenter);
}
if (this.dom.dragLeft) {
this.dom.box.insertBefore(dragCenter, this.dom.dragLeft);
}
else {
this.dom.box.appendChild(dragCenter);
}
}
else if (this.dom.point) {
this.dom.point.appendChild(dragCenter);
}

+ 6
- 5
lib/timeline/component/item/RangeItem.js View File

@ -171,6 +171,7 @@ RangeItem.prototype.repositionX = function(limitSize) {
var parentWidth = this.parent.width;
var start = this.conversion.toScreen(this.data.start);
var end = this.conversion.toScreen(this.data.end);
var align = this.data.align === undefined ? this.options.align : this.data.align;
var contentStartPosition;
var contentWidth;
@ -217,7 +218,7 @@ RangeItem.prototype.repositionX = function(limitSize) {
}
this.dom.box.style.width = boxWidth + 'px';
switch (this.options.align) {
switch (align) {
case 'left':
if (this.options.rtl) {
this.dom.content.style.right = '0';
@ -291,7 +292,7 @@ RangeItem.prototype.repositionY = function() {
* @protected
*/
RangeItem.prototype._repaintDragLeft = function () {
if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragLeft) {
// create and show drag area
var dragLeft = document.createElement('div');
dragLeft.className = 'vis-drag-left';
@ -300,7 +301,7 @@ RangeItem.prototype._repaintDragLeft = function () {
this.dom.box.appendChild(dragLeft);
this.dom.dragLeft = dragLeft;
}
else if (!this.selected && this.dom.dragLeft) {
else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragLeft) {
// delete drag area
if (this.dom.dragLeft.parentNode) {
this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
@ -314,7 +315,7 @@ RangeItem.prototype._repaintDragLeft = function () {
* @protected
*/
RangeItem.prototype._repaintDragRight = function () {
if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragRight) {
// create and show drag area
var dragRight = document.createElement('div');
dragRight.className = 'vis-drag-right';
@ -323,7 +324,7 @@ RangeItem.prototype._repaintDragRight = function () {
this.dom.box.appendChild(dragRight);
this.dom.dragRight = dragRight;
}
else if (!this.selected && this.dom.dragRight) {
else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragRight) {
// delete drag area
if (this.dom.dragRight.parentNode) {
this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);

+ 5
- 1
lib/timeline/optionsTimeline.js View File

@ -89,7 +89,11 @@ let allOptions = {
repeat: {string},
__type__: {object, array}
},
itemsAlwaysDraggable: { 'boolean': bool},
itemsAlwaysDraggable: {
item: { 'boolean': bool, 'undefined': 'undefined'},
range: { 'boolean': bool, 'undefined': 'undefined'},
__type__: { 'boolean': bool, object}
},
locale:{string},
locales:{
__any__: {any},

+ 12
- 11
lib/util.js View File

@ -621,12 +621,13 @@ exports.getAbsoluteTop = function (elem) {
* @param {Element} elem
* @param {String} className
*/
exports.addClassName = function (elem, className) {
exports.addClassName = function (elem, classNames) {
var classes = elem.className.split(' ');
if (classes.indexOf(className) == -1) {
classes.push(className); // add the class to the array
elem.className = classes.join(' ');
}
var newClasses = classNames.split(' ');
classes = classes.concat(newClasses.filter(function(className) {
return classes.indexOf(className) < 0;
}));
elem.className = classes.join(' ');
};
/**
@ -634,13 +635,13 @@ exports.addClassName = function (elem, className) {
* @param {Element} elem
* @param {String} className
*/
exports.removeClassName = function (elem, className) {
exports.removeClassName = function (elem, classNames) {
var classes = elem.className.split(' ');
var index = classes.indexOf(className);
if (index != -1) {
classes.splice(index, 1); // remove the class from the array
elem.className = classes.join(' ');
}
var oldClasses = classNames.split(' ');
classes = classes.filter(function(className) {
return oldClasses.indexOf(className) < 0;
});
elem.className = classes.join(' ');
};
/**

+ 1
- 1
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "4.19.1-SNAPSHOT",
"version": "4.20.1-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"license": "(Apache-2.0 OR MIT)",

+ 46
- 0
test/TimeStep.test.js View File

@ -41,4 +41,50 @@ describe('TimeStep', function () {
assert.equal(timestep.scale, "second", "should have right scale");
assert.equal(timestep.step, 10, "should have right step size");
});
it('should perform the step with a specified scale (1 year)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
timestep.setScale({ scale: 'year', step: 1 });
timestep.start();
assert.equal(timestep.getCurrent().unix(), moment("2017-01-01T00:00:00.000").unix(), "should have the right initial value");
timestep.next();
assert.equal(timestep.getCurrent().unix(), moment("2018-01-01T00:00:00.000").unix(), "should have the right value after a step");
});
it('should perform the step with a specified scale (1 month)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
timestep.setScale({ scale: 'month', step: 1 });
timestep.start();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-01T00:00:00.000").unix(), "should have the right initial value");
timestep.next();
assert.equal(timestep.getCurrent().unix(), moment("2017-05-01T00:00:00.000").unix(), "should have the right value after a step");
});
it('should perform the step with a specified scale (1 week)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
timestep.setScale({ scale: 'week', step: 1 });
timestep.start();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-02T00:00:00.000").unix(), "should have the right initial value");
timestep.next();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-09T00:00:00.000").unix(), "should have the right value after a step");
});
it('should perform the step with a specified scale (1 day)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
timestep.setScale({ scale: 'day', step: 1 });
timestep.start();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-03T00:00:00.000").unix(), "should have the right initial value");
timestep.next();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-04T00:00:00.000").unix(), "should have the right value after a step");
});
it('should perform the step with a specified scale (1 hour)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
timestep.setScale({ scale: 'hour', step: 1 });
timestep.start();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-03T00:00:00.000").unix(), "should have the right initial value");
timestep.next();
assert.equal(timestep.getCurrent().unix(), moment("2017-04-03T01:00:00.000").unix(), "should have the right value after a step");
});
});

+ 74
- 0
test/dotparser.test.js View File

@ -183,4 +183,78 @@ describe('dotparser', function () {
});
});
/**
* DOT-format examples taken from #3015
*/
it('properly handles newline escape sequences in strings', function (done) {
var data = 'dinetwork {1 [label="new\\nline"];}';
data = String(data);
var graph = dot.parseDOT(data);
assert.deepEqual(graph, {
"id": "dinetwork",
"nodes": [
{
"id": 1,
"attr": {
"label": "new\nline", // And not "new\\nline"
}
}
]
});
// Note the double backslashes
var data2 = 'digraph {' + "\n" +
' 3 [color="#0d2b7c", label="query:1230:add_q\\n0.005283\\n6.83%\\n(0.0001)\\n(0.13%)\\n17×"];' + "\n" +
' 3 -> 7 [color="#0d2a7b", fontcolor="#0d2a7b", label="0.005128\\n6.63%\\n17×"];' + "\n" +
' 5 [color="#0d1976", label="urlresolvers:537:reverse\\n0.00219\\n2.83%\\n(0.000193)\\n(0.25%)\\n29×"];' + "\n" +
"}"
data2 = String(data2);
var graph2 = dot.parseDOT(data2);
//console.log(JSON.stringify(graph, null, 2));
assert.deepEqual(graph2, {
"type": "digraph",
"nodes": [
{
"id": 3,
"attr": {
"color": "#0d2b7c",
"label": "query:1230:add_q\n0.005283\n6.83%\n(0.0001)\n(0.13%)\n17×"
}
},
{
"id": 7
},
{
"id": 5,
"attr": {
"color": "#0d1976",
"label": "urlresolvers:537:reverse\n0.00219\n2.83%\n(0.000193)\n(0.25%)\n29×"
}
}
],
"edges": [
{
"from": 3,
"to": 7,
"type": "->",
"attr": {
"color": "#0d2a7b",
"fontcolor": "#0d2a7b",
"label": "0.005128\n6.63%\n17×"
}
}
]
});
done();
});
});

Loading…
Cancel
Save