Browse Source

started creation of the edges handler and the nodes handler. currently broken.

flowchartTest
Alex de Mulder 9 years ago
parent
commit
333ec38c3c
20 changed files with 2281 additions and 2311 deletions
  1. +1236
    -825
      dist/vis.js
  2. +1
    -1
      examples/network/02_random_nodes.html
  3. +2
    -2
      index.js
  4. +2
    -11
      lib/network/Images.js
  5. +99
    -692
      lib/network/Network.js
  6. +0
    -707
      lib/network/mixins/SelectionMixin.js
  7. +2
    -1
      lib/network/modules/Canvas.js
  8. +1
    -1
      lib/network/modules/CanvasRenderer.js
  9. +24
    -32
      lib/network/modules/Clustering.js
  10. +241
    -0
      lib/network/modules/EdgesHandler.js
  11. +3
    -3
      lib/network/modules/InteractionHandler.js
  12. +421
    -0
      lib/network/modules/LayoutEngine.js
  13. +200
    -0
      lib/network/modules/NodesHandler.js
  14. +13
    -4
      lib/network/modules/PhysicsEngine.js
  15. +9
    -4
      lib/network/modules/SelectionHandler.js
  16. +2
    -7
      lib/network/modules/View.js
  17. +5
    -4
      lib/network/modules/components/NavigationHandler.js
  18. +7
    -8
      lib/network/modules/components/edges/EdgeMain.js
  19. +6
    -9
      lib/network/modules/components/nodes/NodeMain.js
  20. +7
    -0
      lib/util.js

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


+ 1
- 1
examples/network/02_random_nodes.html View File

@ -84,7 +84,7 @@
nodes: nodes, nodes: nodes,
edges: edges edges: edges
}; };
var options = {stabilize:false};
var options = {physics:{stabilization:false}};
network = new vis.Network(container, data, options); network = new vis.Network(container, data, options);
// add event listeners // add event listeners

+ 2
- 2
index.js View File

@ -54,10 +54,10 @@ exports.timeline = {
// Network // Network
exports.Network = require('./lib/network/Network'); exports.Network = require('./lib/network/Network');
exports.network = { exports.network = {
Edge: require('./lib/network/Edge'),
//Edge: require('./lib/network/Edge'),
Groups: require('./lib/network/Groups'), Groups: require('./lib/network/Groups'),
Images: require('./lib/network/Images'), Images: require('./lib/network/Images'),
Node: require('./lib/network/Node'),
//Node: require('./lib/network/Node'),
Popup: require('./lib/network/Popup'), Popup: require('./lib/network/Popup'),
dotparser: require('./lib/network/dotparser'), dotparser: require('./lib/network/dotparser'),
gephiParser: require('./lib/network/gephiParser') gephiParser: require('./lib/network/gephiParser')

+ 2
- 11
lib/network/Images.js View File

@ -2,20 +2,11 @@
* @class Images * @class Images
* This class loads images and keeps them stored. * This class loads images and keeps them stored.
*/ */
function Images() {
function Images(callback) {
this.images = {}; this.images = {};
this.imageBroken = {}; this.imageBroken = {};
this.callback = undefined;
}
/**
* Set an onload callback function. This will be called each time an image
* is loaded
* @param {function} callback
*/
Images.prototype.setOnloadCallback = function(callback) {
this.callback = callback; this.callback = callback;
};
}
/** /**
* *

+ 99
- 692
lib/network/Network.js View File

@ -1,31 +1,31 @@
var Emitter = require('emitter-component'); var Emitter = require('emitter-component');
var Hammer = require('../module/hammer'); var Hammer = require('../module/hammer');
var keycharm = require('keycharm');
var util = require('../util'); var util = require('../util');
var hammerUtil = require('../hammerUtil');
var DataSet = require('../DataSet'); var DataSet = require('../DataSet');
var DataView = require('../DataView'); var DataView = require('../DataView');
var dotparser = require('./dotparser'); var dotparser = require('./dotparser');
var gephiParser = require('./gephiParser'); var gephiParser = require('./gephiParser');
var Groups = require('./Groups'); var Groups = require('./Groups');
var Images = require('./Images'); var Images = require('./Images');
var Node = require('./Node');
var Edge = require('./Edge');
var Node = require('./modules/components/nodes/NodeMain');
var Edge = require('./modules/components/edges/EdgeMain');
var Popup = require('./Popup'); var Popup = require('./Popup');
var MixinLoader = require('./mixins/MixinLoader');
var Activator = require('../shared/Activator'); var Activator = require('../shared/Activator');
var locales = require('./locales'); var locales = require('./locales');
// Load custom shapes into CanvasRenderingContext2D // Load custom shapes into CanvasRenderingContext2D
require('./shapes'); require('./shapes');
import { PhysicsEngine } from './modules/PhysicsEngine'
import { ClusterEngine } from './modules/Clustering'
import { CanvasRenderer } from './modules/CanvasRenderer'
import { Canvas } from './modules/Canvas'
import { View } from './modules/View'
import { InteractionHandler } from './modules/InteractionHandler'
import { SelectionHandler } from "./modules/SelectionHandler"
import NodesHandler from './modules/NodesHandler';
import EdgesHandler from './modules/EdgesHandler';
import PhysicsEngine from './modules/PhysicsEngine';
import ClusterEngine from './modules/Clustering';
import CanvasRenderer from './modules/CanvasRenderer';
import Canvas from './modules/Canvas';
import View from './modules/View';
import InteractionHandler from './modules/InteractionHandler';
import SelectionHandler from "./modules/SelectionHandler";
import LayoutEngine from "./modules/LayoutEngine";
/** /**
* @constructor Network * @constructor Network
@ -43,96 +43,8 @@ function Network (container, data, options) {
throw new SyntaxError('Constructor must be called with the new operator'); throw new SyntaxError('Constructor must be called with the new operator');
} }
this._initializeMixinLoaders();
// render and calculation settings
this.initializing = true;
this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
var customScalingFunction = function (min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
};
// set constant values // set constant values
this.defaultOptions = {
nodes: {
customScalingFunction: customScalingFunction,
mass: 1,
radiusMin: 10,
radiusMax: 30,
radius: 10,
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
widthMax: 64, // px
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
fontFill: undefined,
fontStrokeWidth: 0, // px
fontStrokeColor: '#ffffff',
fontDrawThreshold: 3,
scaleFontWithValue: false,
fontSizeMin: 14,
fontSizeMax: 30,
fontSizeMaxVisible: 30,
value: 1,
level: -1,
color: {
border: '#2B7CE9',
background: '#97C2FC',
highlight: {
border: '#2B7CE9',
background: '#D2E5FF'
},
hover: {
border: '#2B7CE9',
background: '#D2E5FF'
}
},
group: undefined,
borderWidth: 1,
borderWidthSelected: undefined
},
edges: {
customScalingFunction: customScalingFunction,
widthMin: 1, //
widthMax: 15,//
width: 1,
widthSelectionMultiplier: 2,
hoverWidth: 1.5,
value:1,
style: 'line',
color: {
color:'#848484',
highlight:'#848484',
hover: '#848484'
},
opacity:1.0,
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
fontFill: 'white',
fontStrokeWidth: 0, // px
fontStrokeColor: 'white',
labelAlignment:'horizontal',
arrowScaleFactor: 1,
dash: {
length: 10,
gap: 5,
altLength: undefined
},
inheritColor: "from", // to, from, false, true (== from)
useGradients: false // release in 4.0
},
this.remainingOptions = {
dataManipulation: { dataManipulation: {
enabled: false, enabled: false,
initiallyVisible: false initiallyVisible: false
@ -144,43 +56,10 @@ function Network (container, data, options) {
direction: "UD", // UD, DU, LR, RL direction: "UD", // UD, DU, LR, RL
layout: "hubsize" // hubsize, directed layout: "hubsize" // hubsize, directed
}, },
interaction: {
dragNodes:true,
dragView: true,
zoomView: true,
hoverEnabled: false,
showNavigationIcons: false,
tooltip: {
delay: 300,
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
color: {
border: '#666',
background: '#FFFFC6'
}
},
keyboard: {
enabled: false,
speed: {x: 10, y: 10, zoom: 0.02},
bindToWindow: true
}
},
selection: {
enabled: true,
selectConnectedEdges: true
},
smoothCurves: {
enabled: true,
dynamic: true,
type: "continuous",
roundness: 0.5
},
locale: 'en', locale: 'en',
locales: locales, locales: locales,
useDefaultGroups: true useDefaultGroups: true
}; };
this.constants = util.extend({}, this.defaultOptions);
// containers for nodes and edges // containers for nodes and edges
this.body = { this.body = {
@ -223,121 +102,67 @@ function Network (container, data, options) {
} }
}; };
// modules
this.canvas = new Canvas(this.body);
this.selectionHandler = new SelectionHandler(this.body, this.canvas);
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler);
this.view = new View(this.body, this.canvas);
this.renderer = new CanvasRenderer(this.body, this.canvas);
this.clustering = new ClusterEngine(this.body);
this.physics = new PhysicsEngine(this.body);
// todo think of good comment for this set
var groups = new Groups(); // object with groups
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
// create the DOM elements
this.canvas.create();
// data handling modules
this.canvas = new Canvas(this.body); // DOM handler
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
this.layoutEngine = new LayoutEngine(this.body); // TODO: layout engine for initial positioning and hierarchical positioning
this.clustering = new ClusterEngine(this.body); // clustering api
this.hoverObj = {nodes:{},edges:{}};
this.controlNodesActive = false;
this.navigationHammers = [];
this.manipulationHammers = [];
// Node variables
var me = this;
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function (status) {
me._requestRedraw();
});
this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
// keyboard navigation variables
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
// loading all the mixins:
// load the force calculation functions, grouped under the physics system.
//this._loadPhysicsSystem();
// create a frame and canvas
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
// load the selection system. (mandatory, required by Network)
this._loadSelectionSystem();
// load the selection system. (mandatory, required by Network)
//this._loadHierarchySystem();
// apply options
this.setOptions(options);
// position and scale variables and objects
this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
this.scale = 1; // defining the global scale variable in the constructor
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
this.body.emitter.on("_dataChanged", (params) => {
var t0 = new Date().valueOf();
// update shortcut lists
this._updateNodeIndexList();
this.physics._updateCalculationNodes();
// update values
this._updateValueRange(this.body.nodes);
this._updateValueRange(this.body.edges);
// update edges
this._reconnectEdges();
this.edgesHandler.createBezierNodes(params);
this._markAllEdgesAsDirty();
// start simulation (can be called safely, even if already running)
this.body.emitter.emit("startSimulation");
console.log("_dataChanged took:", new Date().valueOf() - t0);
})
// create event listeners used to subscribe on the DataSets of the nodes and edges
this.nodesListeners = {
'add': function (event, params) {
me._addNodes(params.items);
me.start();
},
'update': function (event, params) {
me._updateNodes(params.items, params.data);
me.start();
},
'remove': function (event, params) {
me._removeNodes(params.items);
me.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
me._addEdges(params.items);
me.start();
},
'update': function (event, params) {
me._updateEdges(params.items);
me.start();
},
'remove': function (event, params) {
me._removeEdges(params.items);
me.start();
}
};
// this is called when options of EXISTING nodes or edges have changed.
this.body.emitter.on("_dataUpdated", () => {
var t0 = new Date().valueOf();
// update values
this._updateValueRange(this.body.nodes);
this._updateValueRange(this.body.edges);
// update edges
this._reconnectEdges();
this.edgesHandler.createBezierNodes(params);
this._markAllEdgesAsDirty();
// start simulation (can be called safely, even if already running)
this.body.emitter.emit("startSimulation");
console.log("_dataUpdated took:", new Date().valueOf() - t0);
});
// create the DOM elements
this.canvas.create();
// properties for the animation
this.moving = true;
this.renderTimer = undefined; // Scheduling function. Is definded in this.start();
// apply options
this.setOptions(options);
// load data (the disable start variable will be the same as the enabled clustering) // load data (the disable start variable will be the same as the enabled clustering)
this.setData(data, this.constants.hierarchicalLayout.enabled);
this.setData(data);
// hierarchical layout
if (this.constants.hierarchicalLayout.enabled == true) {
this._setupHierarchicalLayout();
}
else {
// zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
if (this.constants.stabilize == false) {
this.zoomExtent({duration:0}, true, this.constants.clustering.enabled);
}
}
if (this.constants.stabilize == false) {
this.initializing = false;
}
var me = this;
// this event will trigger a rebuilding of the cache of colors, nodes etc.
this.on("_dataChanged", function () {
me._updateNodeIndexList();
me.physics._updateCalculationNodes();
me._markAllEdgesAsDirty();
if (me.initializing !== true) {
me.moving = true;
me.start();
}
})
this.on("_newEdgesCreated", this._createBezierNodes.bind(this));
//this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this));
} }
// Extend Network with an Emitter mixin // Extend Network with an Emitter mixin
@ -352,9 +177,6 @@ Network.prototype._createEdge = function(properties) {
return new Edge(properties, this.body, this.constants) return new Edge(properties, this.body, this.constants)
} }
/** /**
* Update the this.body.nodeIndices with the most recent node index list * Update the this.body.nodeIndices with the most recent node index list
* @private * @private
@ -376,10 +198,10 @@ Network.prototype._updateNodeIndexList = function() {
* {Options} [options] Object with options * {Options} [options] Object with options
* @param {Boolean} [disableStart] | optional: disable the calling of the start function. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
*/ */
Network.prototype.setData = function(data, disableStart) {
if (disableStart === undefined) {
disableStart = false;
}
Network.prototype.setData = function(data) {
// reset the physics engine.
this.body.emitter.emit("resetPhysics");
this.body.emitter.emit("_resetData");
// unselect all to ensure no selections from old data are carried over. // unselect all to ensure no selections from old data are carried over.
this.selectionHandler.unselectAll(); this.selectionHandler.unselectAll();
@ -393,9 +215,9 @@ Network.prototype.setData = function(data, disableStart) {
} }
// clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button. // clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button.
if (this.constants.dataManipulation.enabled == true) {
this._createManipulatorBar();
}
//if (this.constants.dataManipulation.enabled == true) {
// this._createManipulatorBar();
//}
// set options // set options
this.setOptions(data && data.options); this.setOptions(data && data.options);
@ -417,23 +239,12 @@ Network.prototype.setData = function(data, disableStart) {
} }
} }
else { else {
this._setNodes(data && data.nodes);
this._setEdges(data && data.edges);
this.nodesHandler.setData(data && data.nodes);
this.edgesHandler.setData(data && data.edges);
} }
if (disableStart == false) {
if (this.constants.hierarchicalLayout.enabled == true) {
this._resetLevels();
this._setupHierarchicalLayout();
}
else {
// find a stable position or start animating to a stable position
this.body.emitter.emit("stabilize");
}
}
else {
this.initializing = false;
}
// find a stable position or start animating to a stable position
this.body.emitter.emit("initPhysics");
}; };
/** /**
@ -442,41 +253,38 @@ Network.prototype.setData = function(data, disableStart) {
*/ */
Network.prototype.setOptions = function (options) { Network.prototype.setOptions = function (options) {
if (options) { if (options) {
var prop;
var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
];
//var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
// 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
//];
// extend all but the values in fields // extend all but the values in fields
util.selectiveNotDeepExtend(fields,this.constants, options);
util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
//util.selectiveNotDeepExtend(fields,this.constants, options);
//util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
//util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
this.groups.useDefaultGroups = this.constants.useDefaultGroups;
//this.groups.useDefaultGroups = this.constants.useDefaultGroups;
this.nodesHandler.setOptions(options.nodes);
this.edgesHandler.setOptions(options.edges);
this.physics.setOptions(options.physics); this.physics.setOptions(options.physics);
this.canvas.setOptions(options.canvas); this.canvas.setOptions(options.canvas);
this.renderer.setOptions(options.rendering); this.renderer.setOptions(options.rendering);
this.view.setOptions(options.view);
this.interactionHandler.setOptions(options.interaction); this.interactionHandler.setOptions(options.interaction);
this.selectionHandler.setOptions(options.selection); this.selectionHandler.setOptions(options.selection);
this.layoutEngine.setOptions(options.layout);
//this.clustering.setOptions(options.clustering);
//util.mergeOptions(this.constants, options,'smoothCurves');
//util.mergeOptions(this.constants, options,'hierarchicalLayout');
//util.mergeOptions(this.constants, options,'clustering');
//util.mergeOptions(this.constants, options,'navigation');
//util.mergeOptions(this.constants, options,'keyboard');
//util.mergeOptions(this.constants, options,'dataManipulation');
if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
util.mergeOptions(this.constants, options,'smoothCurves');
util.mergeOptions(this.constants, options,'hierarchicalLayout');
util.mergeOptions(this.constants, options,'clustering');
util.mergeOptions(this.constants, options,'navigation');
util.mergeOptions(this.constants, options,'keyboard');
util.mergeOptions(this.constants, options,'dataManipulation');
if (options.dataManipulation) {
this.editMode = this.constants.dataManipulation.initiallyVisible;
}
//if (options.dataManipulation) {
// this.editMode = this.constants.dataManipulation.initiallyVisible;
//}
// TODO: work out these options and document them // TODO: work out these options and document them
@ -554,76 +362,11 @@ Network.prototype.setOptions = function (options) {
this.body.emitter.emit("activate"); this.body.emitter.emit("activate");
} }
this._markAllEdgesAsDirty();
this.canvas.setSize(); this.canvas.setSize();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
if (this.initializing !== true) {
this.moving = true;
this.start();
}
} }
}; };
/**
* Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
* @private
*/
Network.prototype._createKeyBinds = function() {
return;
//var me = this;
//if (this.keycharm !== undefined) {
// this.keycharm.destroy();
//}
//
//if (this.constants.keyboard.bindToWindow == true) {
// this.keycharm = keycharm({container: window, preventDefault: false});
//}
//else {
// this.keycharm = keycharm({container: this.frame, preventDefault: false});
//}
//
//this.keycharm.reset();
//
//if (this.constants.keyboard.enabled && this.isActive()) {
// this.keycharm.bind("up", this._moveUp.bind(me) , "keydown");
// this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup");
// this.keycharm.bind("down", this._moveDown.bind(me) , "keydown");
// this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup");
// this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown");
// this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup");
// this.keycharm.bind("right",this._moveRight.bind(me), "keydown");
// this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup");
// this.keycharm.bind("=", this._zoomIn.bind(me), "keydown");
// this.keycharm.bind("=", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown");
// this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown");
// this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("-", this._zoomOut.bind(me), "keydown");
// this.keycharm.bind("-", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("[", this._zoomIn.bind(me), "keydown");
// this.keycharm.bind("[", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("]", this._zoomOut.bind(me), "keydown");
// this.keycharm.bind("]", this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown");
// this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup");
// this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown");
// this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup");
//}
//
//if (this.constants.dataManipulation.enabled == true) {
// this.keycharm.bind("esc",this._createManipulatorBar.bind(me));
// this.keycharm.bind("delete",this._deleteSelected.bind(me));
//}
};
/** /**
* Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
* var network = new vis.Network(..); * var network = new vis.Network(..);
@ -631,31 +374,15 @@ Network.prototype._createKeyBinds = function() {
* network = null; * network = null;
*/ */
Network.prototype.destroy = function() { Network.prototype.destroy = function() {
this.start = function () {};
this.redraw = function () {};
this.renderTimer = false;
// cleanup physicsConfiguration if it exists
this._cleanupPhysicsConfiguration();
// remove keybindings
this.keycharm.reset();
// clear hammer bindings
this.hammer.destroy();
this.body.emitter.emit("destroy");
// clear events // clear events
this.off();
this.body.emitter.off();
this._recursiveDOMDelete(this.containerElement);
// remove the container and everything inside it recursively
this.util.recursiveDOMDelete(this.body.container);
}; };
Network.prototype._recursiveDOMDelete = function(DOMobject) {
while (DOMobject.hasChildNodes() == true) {
this._recursiveDOMDelete(DOMobject.firstChild);
DOMobject.removeChild(DOMobject.firstChild);
}
};
/** /**
* Check if there is an element on the given position in the network * Check if there is an element on the given position in the network
@ -784,113 +511,6 @@ Network.prototype._checkHidePopup = function (pointer) {
}; };
/**
* Set a data set with nodes for the network
* @param {Array | DataSet | DataView} nodes The data containing the nodes.
* @private
*/
Network.prototype._setNodes = function(nodes) {
var oldNodesData = this.body.data.nodes;
if (nodes instanceof DataSet || nodes instanceof DataView) {
this.body.data.nodes = nodes;
}
else if (Array.isArray(nodes)) {
this.body.data.nodes = new DataSet();
this.body.data.nodes.add(nodes);
}
else if (!nodes) {
this.body.data.nodes = new DataSet();
}
else {
throw new TypeError('Array or DataSet expected');
}
if (oldNodesData) {
// unsubscribe from old dataset
util.forEach(this.nodesListeners, function (callback, event) {
oldNodesData.off(event, callback);
});
}
// remove drawn nodes
this.body.nodes = {};
if (this.body.data.nodes) {
// subscribe to new dataset
var me = this;
util.forEach(this.nodesListeners, function (callback, event) {
me.body.data.nodes.on(event, callback);
});
// draw all new nodes
var ids = this.body.data.nodes.getIds();
this._addNodes(ids);
}
this._updateSelection();
};
/**
* Add nodes
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._addNodes = function(ids) {
var id;
for (var i = 0, len = ids.length; i < len; i++) {
id = ids[i];
var data = this.body.data.nodes.get(id);
var node = new Node(data, this.images, this.groups, this.constants);
this.body.nodes[id] = node; // note: this may replace an existing node
if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
var radius = 10 * 0.1*ids.length + 10;
var angle = 2 * Math.PI * Math.random();
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
}
this.moving = true;
}
this._updateNodeIndexList();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this.physics._updateCalculationNodes();
this._reconnectEdges();
this._updateValueRange(this.body.nodes);
};
/**
* Update existing nodes, or create them when not yet existing
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._updateNodes = function(ids,changedData) {
var nodes = this.body.nodes;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
var node = nodes[id];
var data = changedData[i];
if (node) {
// update node
node.setProperties(data, this.constants);
}
else {
// create node
node = new Node(properties, this.images, this.groups, this.constants);
nodes[id] = node;
}
}
this.moving = true;
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this._updateNodeIndexList();
this._updateValueRange(nodes);
this._markAllEdgesAsDirty();
};
Network.prototype._markAllEdgesAsDirty = function() { Network.prototype._markAllEdgesAsDirty = function() {
@ -899,190 +519,8 @@ Network.prototype._markAllEdgesAsDirty = function() {
} }
} }
/**
* Remove existing nodes. If nodes do not exist, the method will just ignore it.
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._removeNodes = function(ids) {
var nodes = this.body.nodes;
// remove from selection
for (var i = 0, len = ids.length; i < len; i++) {
if (this.selectionObj.nodes[ids[i]] !== undefined) {
this.body.nodes[ids[i]].unselect();
this._removeFromSelection(this.body.nodes[ids[i]]);
}
}
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
delete nodes[id];
}
this._updateNodeIndexList();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this.physics._updateCalculationNodes();
this._reconnectEdges();
this._updateSelection();
this._updateValueRange(nodes);
};
/**
* Load edges by reading the data table
* @param {Array | DataSet | DataView} edges The data containing the edges.
* @private
* @private
*/
Network.prototype._setEdges = function(edges) {
var oldEdgesData = this.body.data.edges;
if (edges instanceof DataSet || edges instanceof DataView) {
this.body.data.edges = edges;
}
else if (Array.isArray(edges)) {
this.body.data.edges = new DataSet();
this.body.data.edges.add(edges);
}
else if (!edges) {
this.body.data.edges = new DataSet();
}
else {
throw new TypeError('Array or DataSet expected');
}
if (oldEdgesData) {
// unsubscribe from old dataset
util.forEach(this.edgesListeners, function (callback, event) {
oldEdgesData.off(event, callback);
});
}
// remove drawn edges
this.body.edges = {};
if (this.body.data.edges) {
// subscribe to new dataset
var me = this;
util.forEach(this.edgesListeners, function (callback, event) {
me.body.data.edges.on(event, callback);
});
// draw all new nodes
var ids = this.body.data.edges.getIds();
this._addEdges(ids);
}
this._reconnectEdges();
};
/**
* Add edges
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._addEdges = function (ids) {
var edges = this.body.edges,
edgesData = this.body.data.edges;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
var oldEdge = edges[id];
if (oldEdge) {
oldEdge.disconnect();
}
var data = edgesData.get(id, {"showInternalIds" : true});
edges[id] = new Edge(data, this.body, this.constants);
}
this.moving = true;
this._updateValueRange(edges);
this._createBezierNodes();
this.physics._updateCalculationNodes();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
};
/**
* Update existing edges, or create them when not yet existing
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._updateEdges = function (ids) {
var edges = this.body.edges;
var edgesData = this.body.data.edges;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
var data = edgesData.get(id);
var edge = edges[id];
if (edge) {
// update edge
edge.disconnect();
edge.setProperties(data);
edge.connect();
}
else {
// create edge
edge = new Edge(data, this.body, this.constants);
this.body.edges[id] = edge;
}
}
this._createBezierNodes();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this.moving = true;
this._updateValueRange(edges);
};
/**
* Remove existing edges. Non existing ids will be ignored
* @param {Number[] | String[]} ids
* @private
*/
Network.prototype._removeEdges = function (ids) {
var edges = this.body.edges;
// remove from selection
for (var i = 0, len = ids.length; i < len; i++) {
if (this.selectionObj.edges[ids[i]] !== undefined) {
edges[ids[i]].unselect();
this._removeFromSelection(edges[ids[i]]);
}
}
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
var edge = edges[id];
if (edge) {
if (edge.via != null) {
delete this.body.supportNodes[edge.via.id];
}
edge.disconnect();
delete edges[id];
}
}
this.moving = true;
this._updateValueRange(edges);
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this.physics._updateCalculationNodes();
};
/** /**
* Reconnect all edges * Reconnect all edges
* @private * @private
@ -1273,37 +711,6 @@ Network.prototype._configureSmoothCurves = function(disableStart = true) {
}; };
/**
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
* are used for the force calculation.
*
* @private
*/
Network.prototype._createBezierNodes = function(specificEdges = this.body.edges) {
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
for (var edgeId in specificEdges) {
if (specificEdges.hasOwnProperty(edgeId)) {
var edge = specificEdges[edgeId];
if (edge.via == null) {
var nodeId = "edgeId:".concat(edge.id);
var node = new Node(
{id:nodeId,
mass:1,
shape:'circle',
image:"",
internalMultiplier:1
},{},{},this.constants);
this.body.supportNodes[nodeId] = node;
edge.via = node;
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
}
}
this._updateNodeIndexList();
}
};
/** /**
* load the functions that load the mixins into the prototype. * load the functions that load the mixins into the prototype.
* *

+ 0
- 707
lib/network/mixins/SelectionMixin.js View File

@ -1,707 +0,0 @@
var Node = require('../Node');
/**
*
* @param object
* @param overlappingNodes
* @private
*/
exports._getNodesOverlappingWith = function(object, overlappingNodes) {
var nodes = this.body.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
}
};
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getAllNodesOverlappingWith = function (object) {
var overlappingNodes = [];
this._getNodesOverlappingWith(object,overlappingNodes);
return overlappingNodes;
};
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
exports._pointerToPositionObject = function(pointer) {
var x = this._XconvertDOMtoCanvas(pointer.x);
var y = this._YconvertDOMtoCanvas(pointer.y);
return {
left: x,
top: y,
right: x,
bottom: y
};
};
/**
* Get the top node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
exports._getNodeAt = function (pointer) {
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
};
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
var edges = this.body.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
};
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getAllEdgesOverlappingWith = function (object) {
var overlappingEdges = [];
this._getEdgesOverlappingWith(object,overlappingEdges);
return overlappingEdges;
};
/**
* Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call
* getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
* @param pointer
* @returns {null}
* @private
*/
exports._getEdgeAt = function(pointer) {
var positionObject = this._pointerToPositionObject(pointer);
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
}
};
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
exports._addToSelection = function(obj) {
if (obj instanceof Node) {
this.selectionObj.nodes[obj.id] = obj;
}
else {
this.selectionObj.edges[obj.id] = obj;
}
};
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
exports._addToHover = function(obj) {
if (obj instanceof Node) {
this.hoverObj.nodes[obj.id] = obj;
}
else {
this.hoverObj.edges[obj.id] = obj;
}
};
/**
* Remove a single option from selection.
*
* @param {Object} obj
* @private
*/
exports._removeFromSelection = function(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
}
else {
delete this.selectionObj.edges[obj.id];
}
};
/**
* Unselect all. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._unselectAll = function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
this.selectionObj.nodes[nodeId].unselect();
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();
}
}
this.selectionObj = {nodes:{},edges:{}};
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* Unselect all clusters. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._unselectClusters = function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
this.selectionObj.nodes[nodeId].unselect();
this._removeFromSelection(this.selectionObj.nodes[nodeId]);
}
}
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
exports._getSelectedNodeCount = function() {
var count = 0;
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
return count;
};
/**
* return the selected node
*
* @returns {number}
* @private
*/
exports._getSelectedNode = function() {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return this.selectionObj.nodes[nodeId];
}
}
return null;
};
/**
* return the selected edge
*
* @returns {number}
* @private
*/
exports._getSelectedEdge = function() {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
return this.selectionObj.edges[edgeId];
}
}
return null;
};
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
exports._getSelectedEdgeCount = function() {
var count = 0;
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
};
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
exports._getSelectedObjectCount = function() {
var count = 0;
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
};
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
exports._selectionIsEmpty = function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return false;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
return false;
}
}
return true;
};
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
exports._clusterInSelection = function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
return true;
}
}
}
return false;
};
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._selectConnectedEdges = function(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.select();
this._addToSelection(edge);
}
};
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._hoverConnectedEdges = function(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.hover = true;
this._addToHover(edge);
}
};
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._unselectConnectedEdges = function(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.unselect();
this._removeFromSelection(edge);
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @param {Boolean} append
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._selectObject = function(object, append, doNotTrigger, highlightEdges, overrideSelectable) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
if (highlightEdges === undefined) {
highlightEdges = true;
}
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
}
// selectable allows the object to be selected. Override can be used if needed to bypass this.
if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) {
object.select();
this._addToSelection(object);
if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
this._selectConnectedEdges(object);
}
}
// do not select the object if selectable is false, only add it to selection to allow drag to work
else if (object.selected == false) {
this._addToSelection(object);
doNotTrigger = true;
}
else {
object.unselect();
this._removeFromSelection(object);
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
exports._blurObject = function(object) {
if (object.hover == true) {
object.hover = false;
this.emit("blurNode",{node:object.id});
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
exports._hoverObject = function(object) {
if (object.hover == false) {
object.hover = true;
this._addToHover(object);
if (object instanceof Node) {
this.emit("hoverNode",{node:object.id});
}
}
if (object instanceof Node) {
this._hoverConnectedEdges(object);
}
};
/**
* handles the selection part of the touch, only for navigation controls elements;
* Touch is triggered before tap, also before hold. Hold triggers after a while.
* This is the most responsive solution
*
* @param {Object} pointer
* @private
*/
exports._handleTouch = function(pointer) {
};
/**
* handles the selection part of the tap;
*
* @param {Object} pointer
* @private
*/
exports._handleTap = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node, false);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge, false);
}
else {
this._unselectAll();
}
}
var properties = this.getSelection();
properties['pointer'] = {
DOM: {x: pointer.x, y: pointer.y},
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
}
this.emit("click", properties);
this._requestRedraw();
};
/**
* handles the selection part of the double tap and opens a cluster if needed
*
* @param {Object} pointer
* @private
*/
exports._handleDoubleTap = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null && node !== undefined) {
// we reset the areaCenter here so the opening of the node will occur
this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
"y" : this._YconvertDOMtoCanvas(pointer.y)};
this.openCluster(node);
}
var properties = this.getSelection();
properties['pointer'] = {
DOM: {x: pointer.x, y: pointer.y},
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
}
this.emit("doubleClick", properties);
};
/**
* Handle the onHold selection part
*
* @param pointer
* @private
*/
exports._handleOnHold = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,true);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,true);
}
}
this._requestRedraw();
};
/**
* handle the onRelease event. These functions are here for the navigation controls module
* and data manipulation module.
*
* @private
*/
exports._handleOnRelease = function(pointer) {
this._manipulationReleaseOverload(pointer);
this._navigationReleaseOverload(pointer);
};
exports._manipulationReleaseOverload = function (pointer) {};
exports._navigationReleaseOverload = function (pointer) {};
/**
*
* retrieve the currently selected objects
* @return {{nodes: Array.<String>, edges: Array.<String>}} selection
*/
exports.getSelection = function() {
var nodeIds = this.getSelectedNodes();
var edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
};
/**
*
* retrieve the currently selected nodes
* @return {String[]} selection An array with the ids of the
* selected nodes.
*/
exports.getSelectedNodes = function() {
var idArray = [];
if (this.constants.selectable == true) {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(nodeId);
}
}
}
return idArray
};
/**
*
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
exports.getSelectedEdges = function() {
var idArray = [];
if (this.constants.selectable == true) {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(edgeId);
}
}
}
return idArray;
};
/**
* select zero or more nodes DEPRICATED
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
exports.setSelection = function() {
console.log("setSelection is deprecated. Please use selectNodes instead.")
};
/**
* select zero or more nodes with the option to highlight edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
* @param {boolean} [highlightEdges]
*/
exports.selectNodes = function(selection, highlightEdges) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true,highlightEdges,true);
}
this.redraw();
};
/**
* select zero or more edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
exports.selectEdges = function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this._selectObject(edge,true,true,false,true);
}
this.redraw();
};
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
exports._updateSelection = function () {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.body.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.body.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}
}
};

+ 2
- 1
lib/network/modules/Canvas.js View File

@ -22,6 +22,7 @@ class Canvas {
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.body.emitter.once("resize", (obj) => {this.body.view.translation.x = obj.width * 0.5; this.body.view.translation.y = obj.height * 0.5;}); this.body.emitter.once("resize", (obj) => {this.body.view.translation.x = obj.width * 0.5; this.body.view.translation.y = obj.height * 0.5;});
this.body.emitter.on("destroy", () => this.hammer.destroy());
this.pixelRatio = 1; this.pixelRatio = 1;
} }
@ -227,4 +228,4 @@ class Canvas {
} }
export {Canvas};
export default Canvas;

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

@ -265,4 +265,4 @@ class CanvasRenderer {
} }
export {CanvasRenderer};
export default CanvasRenderer;

+ 24
- 32
lib/network/modules/Clustering.js View File

@ -35,7 +35,7 @@ class ClusterEngine {
for (var i = 0; i < nodesToCluster.length; i++) { for (var i = 0; i < nodesToCluster.length; i++) {
var node = this.body.nodes[nodesToCluster[i]]; var node = this.body.nodes[nodesToCluster[i]];
this.clusterByConnection(node,options,{},{},true);
this.clusterByConnection(node,options,{},{},false);
} }
this.body.emitter.emit('_dataChanged'); this.body.emitter.emit('_dataChanged');
} }
@ -44,9 +44,9 @@ class ClusterEngine {
/** /**
* loop over all nodes, check if they adhere to the condition and cluster if needed. * loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options * @param options
* @param doNotUpdateCalculationNodes
* @param refreshData
*/ */
clusterByNodeData(options = {}, doNotUpdateCalculationNodes = false) {
clusterByNodeData(options = {}, refreshData = true) {
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");} if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed // check if the options object is fine, append if needed
@ -64,16 +64,16 @@ class ClusterEngine {
} }
} }
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
this._cluster(childNodesObj, childEdgesObj, options, refreshData);
} }
/** /**
* Cluster all nodes in the network that have only 1 edge * Cluster all nodes in the network that have only 1 edge
* @param options * @param options
* @param doNotUpdateCalculationNodes
* @param refreshData
*/ */
clusterOutliers(options, doNotUpdateCalculationNodes) {
clusterOutliers(options, refreshData = true) {
options = this._checkOptions(options); options = this._checkOptions(options);
var clusters = []; var clusters = [];
@ -106,10 +106,10 @@ class ClusterEngine {
} }
for (var i = 0; i < clusters.length; i++) { for (var i = 0; i < clusters.length; i++) {
this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
this._cluster(clusters[i].nodes, clusters[i].edges, options, false)
} }
if (doNotUpdateCalculationNodes !== true) {
if (refreshData === true) {
this.body.emitter.emit('_dataChanged'); this.body.emitter.emit('_dataChanged');
} }
} }
@ -118,9 +118,9 @@ class ClusterEngine {
* *
* @param nodeId * @param nodeId
* @param options * @param options
* @param doNotUpdateCalculationNodes
* @param refreshData
*/ */
clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) {
clusterByConnection(nodeId, options, refreshData = true) {
// kill conditions // kill conditions
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
@ -160,7 +160,7 @@ class ClusterEngine {
} }
} }
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
this._cluster(childNodesObj, childEdgesObj, options, refreshData);
} }
@ -256,10 +256,10 @@ class ClusterEngine {
* @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
* @param {Object} childEdgesObj | object with edge objects, id as keys * @param {Object} childEdgesObj | object with edge objects, id as keys
* @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
* @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
* @param {Boolean} refreshData | when true, do not wrap up
* @private * @private
*/ */
_cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes = false) {
_cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
// kill condition: no children so cant cluster // kill condition: no children so cant cluster
if (Object.keys(childNodesObj).length == 0) {return;} if (Object.keys(childNodesObj).length == 0) {return;}
@ -362,17 +362,12 @@ class ClusterEngine {
this.body.edges[newEdges[i].id].connect(); this.body.edges[newEdges[i].id].connect();
} }
// create bezier nodes for smooth curves if needed
this.body.emitter.emit("_newEdgesCreated");
// set ID to undefined so no duplicates arise // set ID to undefined so no duplicates arise
clusterNodeProperties.id = undefined; clusterNodeProperties.id = undefined;
// wrap up // wrap up
if (doNotUpdateCalculationNodes !== true) {
if (refreshData === true) {
this.body.emitter.emit('_dataChanged'); this.body.emitter.emit('_dataChanged');
} }
} }
@ -384,13 +379,13 @@ class ClusterEngine {
* @returns {*} * @returns {*}
*/ */
isCluster(nodeId) { isCluster(nodeId) {
if (this.body.nodes[nodeId] !== undefined) {
return this.body.nodes[nodeId].isCluster;
}
if (this.body.nodes[nodeId] !== undefined) {
return this.body.nodes[nodeId].isCluster;
}
else { else {
console.log("Node does not exist.")
return false;
}
console.log("Node does not exist.")
return false;
}
} }
/** /**
@ -420,9 +415,9 @@ class ClusterEngine {
/** /**
* Open a cluster by calling this function. * Open a cluster by calling this function.
* @param {String} clusterNodeId | the ID of the cluster node * @param {String} clusterNodeId | the ID of the cluster node
* @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
* @param {Boolean} refreshData | wrap up afterwards if not true
*/ */
openCluster(clusterNodeId, doNotUpdateCalculationNodes) {
openCluster(clusterNodeId, refreshData = true) {
// kill conditions // kill conditions
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
@ -465,9 +460,6 @@ class ClusterEngine {
} }
} }
this.body.emitter.emit("_newEdgesCreated",containedEdges);
var edgeIds = []; var edgeIds = [];
for (var i = 0; i < node.edges.length; i++) { for (var i = 0; i < node.edges.length; i++) {
edgeIds.push(node.edges[i].id); edgeIds.push(node.edges[i].id);
@ -505,7 +497,7 @@ class ClusterEngine {
// remove clusterNode // remove clusterNode
delete this.body.nodes[clusterNodeId]; delete this.body.nodes[clusterNodeId];
if (doNotUpdateCalculationNodes !== true) {
if (refreshData === true) {
this.body.emitter.emit('_dataChanged'); this.body.emitter.emit('_dataChanged');
} }
} }
@ -617,4 +609,4 @@ class ClusterEngine {
} }
export { ClusterEngine };
export default ClusterEngine;

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

@ -0,0 +1,241 @@
/**
* Created by Alex on 3/4/2015.
*/
var util = require("../../util");
var DataSet = require('../../DataSet');
var DataView = require('../../DataView');
var Edge = require("./components/edges/EdgeMain");
class EdgesHandler {
constructor(body, images, groups) {
this.body = body;
this.images = images;
this.groups = groups;
this.edgesListeners = {
'add': (event, params) => {this.add(params.items);},
'update': (event, params) => {this.update(params.items);},
'remove': (event, params) => {this.remove(params.items);}
};
var customScalingFunction = function (min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
};
this.options = {};
this.defaultOptions = {
customScalingFunction: customScalingFunction,
widthMin: 1, //
widthMax: 15,//
width: 1,
widthSelectionMultiplier: 2,
hoverWidth: 1.5,
value:1,
style: 'line',
color: {
color:'#848484',
highlight:'#848484',
hover: '#848484'
},
opacity:1.0,
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
fontFill: 'white',
fontStrokeWidth: 0, // px
fontStrokeColor: 'white',
labelAlignment:'horizontal',
arrowScaleFactor: 1,
dash: {
length: 10,
gap: 5,
altLength: undefined
},
inheritColor: "from", // to, from, false, true (== from)
useGradients: false, // release in 4.0
smoothEdges: {
enabled: true,
dynamic: true,
type: "continuous",
roundness: 0.5
}
};
util.extend(this.options, this.defaultOptions);
}
setOptions(options) {
}
/**
* Load edges by reading the data table
* @param {Array | DataSet | DataView} edges The data containing the edges.
* @private
* @private
*/
setData(edges) {
var oldEdgesData = this.body.data.edges;
if (edges instanceof DataSet || edges instanceof DataView) {
this.body.data.edges = edges;
}
else if (Array.isArray(edges)) {
this.body.data.edges = new DataSet();
this.body.data.edges.add(edges);
}
else if (!edges) {
this.body.data.edges = new DataSet();
}
else {
throw new TypeError('Array or DataSet expected');
}
// TODO: is this null or undefined or false?
if (oldEdgesData) {
// unsubscribe from old dataset
util.forEach(this.edgesListeners, (callback, event) => {oldEdgesData.off(event, callback);});
}
// remove drawn edges
this.body.edges = {};
// TODO: is this null or undefined or false?
if (this.body.data.edges) {
// subscribe to new dataset
util.forEach(this.edgesListeners, (callback, event) => {this.body.data.edges.on(event, callback);});
// draw all new nodes
var ids = this.body.data.edges.getIds();
this.add(ids);
}
this.body.emitter.emit("_dataChanged");
}
/**
* Add edges
* @param {Number[] | String[]} ids
* @private
*/
add(ids) {
var edges = this.body.edges,
edgesData = this.body.data.edges;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var oldEdge = edges[id];
if (oldEdge) {
oldEdge.disconnect();
}
var data = edgesData.get(id, {"showInternalIds" : true});
edges[id] = new Edge(data, this.body, this.options);
}
this.body.emitter.emit("_dataChanged");
}
/**
* Update existing edges, or create them when not yet existing
* @param {Number[] | String[]} ids
* @private
*/
update(ids) {
var edges = this.body.edges;
var edgesData = this.body.data.edges;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var data = edgesData.get(id);
var edge = edges[id];
if (edge) {
// update edge
edge.disconnect();
edge.setProperties(data);
edge.connect();
}
else {
// create edge
edge = new Edge(data, this.body, this.options);
this.body.edges[id] = edge;
}
}
this.body.emitter.emit("_dataUpdated")
}
/**
* Remove existing edges. Non existing ids will be ignored
* @param {Number[] | String[]} ids
* @private
*/
remove(ids) {
var edges = this.body.edges;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var edge = edges[id];
if (edge !== undefined) {
if (edge.via != null) {
delete this.body.supportNodes[edge.via.id];
}
edge.disconnect();
delete edges[id];
}
}
this.body.emitter.emit("_dataChanged");
}
/**
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
* are used for the force calculation.
*
* @private
*/
createBezierNodes(specificEdges = this.body.edges) {
var changedData = false;
if (this.options.smoothEdges.enabled == true && this.options.smoothEdges.dynamic == true) {
for (var edgeId in specificEdges) {
if (specificEdges.hasOwnProperty(edgeId)) {
var edge = specificEdges[edgeId];
if (edge.via == null) {
changedData = true;
// TODO: move to nodes
var nodeId = "edgeId:".concat(edge.id);
var node = new Node(
{id:nodeId,
mass:1,
shape:'circle',
image:"",
internalMultiplier:1
},{},{},this.constants);
this.body.supportNodes[nodeId] = node;
edge.via = node;
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
}
}
}
if (changedData === true) {
this.body.emitter.emit("_dataChanged");
}
}
}
export default EdgesHandler;

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

@ -41,7 +41,7 @@ class InteractionHandler {
dragView: true, dragView: true,
zoomView: true, zoomView: true,
hoverEnabled: false, hoverEnabled: false,
showNavigationIcons: true,
showNavigationIcons: false,
tooltip: { tooltip: {
delay: 300, delay: 300,
fontColor: 'black', fontColor: 'black',
@ -53,7 +53,7 @@ class InteractionHandler {
} }
}, },
keyboard: { keyboard: {
enabled: true,
enabled: false,
speed: {x: 10, y: 10, zoom: 0.02}, speed: {x: 10, y: 10, zoom: 0.02},
bindToWindow: true bindToWindow: true
} }
@ -486,4 +486,4 @@ class InteractionHandler {
} }
} }
export {InteractionHandler};
export default InteractionHandler;

+ 421
- 0
lib/network/modules/LayoutEngine.js View File

@ -2,3 +2,424 @@
* Created by Alex on 3/3/2015. * Created by Alex on 3/3/2015.
*/ */
class LayoutEngine {
constructor(body) {
this.body = body;
}
setOptions(options) {
}
positionInitially(nodesArray) {
for (var i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i];
if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
var radius = 10 * 0.1*nodesArray.length + 10;
var angle = 2 * Math.PI * Math.random();
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
}
}
}
_resetLevels() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
node.hierarchyEnumerated = false;
}
}
}
}
/**
* This is the main function to layout the nodes in a hierarchical way.
* It checks if the node details are supplied correctly
*
* @private
*/
_setupHierarchicalLayout() {
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
}
}
// 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.");
this.zoomExtent({duration:0},true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
if (this.constants.hierarchicalLayout.layout == "hubsize") {
this._determineLevels(hubsize);
}
else {
this._determineLevelsDirected(false);
}
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
this._placeNodesByHierarchy(distribution);
}
}
}
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
* @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
_placeNodesByHierarchy(distribution) {
var nodeId, node;
// start placing all the level 0 nodes first. Then recursively position their branches.
for (var level in distribution) {
if (distribution.hasOwnProperty(level)) {
for (nodeId in distribution[level].nodes) {
if (distribution[level].nodes.hasOwnProperty(nodeId)) {
node = distribution[level].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[level].minPos;
node.xFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
}
else {
if (node.yFixed) {
node.y = distribution[level].minPos;
node.yFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
}
/**
* This function get the distribution of levels based on hubsize
*
* @returns {Object}
* @private
*/
_getDistribution() {
var distribution = {};
var nodeId, node, level;
// 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];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
if (distribution[node.level] === undefined) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[nodeId] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
}
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
_determineLevels(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
}
}
// branch from hubs
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
}
/**
* this function allocates nodes in levels based on the direction of the edges
*
* @param hubsize
* @private
*/
_determineLevelsDirected() {
var nodeId, node, firstNode;
var minLevel = 10000;
// set first node to source
firstNode = this.body.nodes[this.nodeIndices[0]];
firstNode.level = minLevel;
this._setLevelDirected(minLevel,firstNode.edges,firstNode.id);
// get the minimum level
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
minLevel = node.level < minLevel ? node.level : minLevel;
}
}
// subtract the minimum from the set so we have a range starting from 0
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.level -= minLevel;
}
}
}
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
*
* @private
*/
_changeConstants() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.dynamic = false;
}
this._configureSmoothCurves();
var config = this.constants.hierarchicalLayout;
config.levelSeparation = Math.abs(config.levelSeparation);
if (config.direction == "RL" || config.direction == "DU") {
config.levelSeparation *= -1;
}
if (config.direction == "RL" || config.direction == "LR") {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "vertical";
}
}
else {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "horizontal";
}
}
}
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
*
* @param edges
* @param parentId
* @param distribution
* @param parentLevel
* @private
*/
_placeBranchNodes(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
}
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevel(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (childNode.edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
}
/**
* this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevelDirected(level, edges, parentId) {
this.body.nodes[parentId].hierarchyEnumerated = true;
var childNode, direction;
for (var i = 0; i < edges.length; i++) {
direction = 1;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
direction = -1;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1) {
childNode.level = level + direction;
}
}
for (var i = 0; i < edges.length; i++) {
if (edges[i].toId == parentId) {childNode = edges[i].from;}
else {childNode = edges[i].to;}
if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
}
}
}
/**
* Unfix nodes
*
* @private
*/
_restoreNodes() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].xFixed = false;
this.body.nodes[nodeId].yFixed = false;
}
}
}
}
export default LayoutEngine;

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

@ -0,0 +1,200 @@
/**
* Created by Alex on 3/4/2015.
*/
var util = require("../../util");
var DataSet = require('../../DataSet');
var DataView = require('../../DataView');
var Node = require("./components/nodes/NodeMain");
class NodesHandler {
constructor(body, images, groups, layoutEngine) {
this.body = body;
this.images = images;
this.groups = groups;
this.layoutEngine = layoutEngine;
this.nodesListeners = {
'add': (event, params) => {this.add(params.items);},
'update': (event, params) => {this.update(params.items, params.data);},
'remove': (event, params) => {this.remove(params.items);}
};
var customScalingFunction = function (min,max,total,value) {
if (max == min) {
return 0.5;
}
else {
var scale = 1 / (max - min);
return Math.max(0,(value - min)*scale);
}
};
this.options = {};
this.defaultOptions = {
customScalingFunction: customScalingFunction,
mass: 1,
radiusMin: 10,
radiusMax: 30,
radius: 10,
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
widthMax: 64, // px
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
fontFill: undefined,
fontStrokeWidth: 0, // px
fontStrokeColor: '#ffffff',
fontDrawThreshold: 3,
scaleFontWithValue: false,
fontSizeMin: 14,
fontSizeMax: 30,
fontSizeMaxVisible: 30,
value: 1,
level: -1,
color: {
border: '#2B7CE9',
background: '#97C2FC',
highlight: {
border: '#2B7CE9',
background: '#D2E5FF'
},
hover: {
border: '#2B7CE9',
background: '#D2E5FF'
}
},
group: undefined,
borderWidth: 1,
borderWidthSelected: undefined
};
util.extend(this.options, this.defaultOptions);
}
setOptions(options) {
}
/**
* Set a data set with nodes for the network
* @param {Array | DataSet | DataView} nodes The data containing the nodes.
* @private
*/
setData(nodes) {
var oldNodesData = this.body.data.nodes;
if (nodes instanceof DataSet || nodes instanceof DataView) {
this.body.data.nodes = nodes;
}
else if (Array.isArray(nodes)) {
this.body.data.nodes = new DataSet();
this.body.data.nodes.add(nodes);
}
else if (!nodes) {
this.body.data.nodes = new DataSet();
}
else {
throw new TypeError('Array or DataSet expected');
}
if (oldNodesData) {
// unsubscribe from old dataset
util.forEach(this.nodesListeners, function (callback, event) {
oldNodesData.off(event, callback);
});
}
// remove drawn nodes
this.body.nodes = {};
if (this.body.data.nodes) {
// subscribe to new dataset
var me = this;
util.forEach(this.nodesListeners, function (callback, event) {
me.body.data.nodes.on(event, callback);
});
// draw all new nodes
var ids = this.body.data.nodes.getIds();
this.add(ids);
}
this.body.emitter.emit("_dataChanged");
}
/**
* Add nodes
* @param {Number[] | String[]} ids
* @private
*/
add(ids) {
var id;
var newNodes = [];
for (var i = 0; i < ids.length; i++) {
id = ids[i];
var data = this.body.data.nodes.get(id);
var node = new Node(data, this.images, this.groups, this.options);
newNodes.push(node);
this.body.nodes[id] = node; // note: this may replace an existing node
}
this.layoutEngine.positionInitially(newNodes);
this.body.emitter.emit("_dataChanged");
}
/**
* Update existing nodes, or create them when not yet existing
* @param {Number[] | String[]} ids
* @private
*/
update(ids, changedData) {
var nodes = this.body.nodes;
var dataChanged = false;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var node = nodes[id];
var data = changedData[i];
if (node !== undefined) {
// update node
node.setProperties(data, this.constants);
}
else {
dataChanged = true;
// create node
node = new Node(properties, this.images, this.groups, this.constants);
nodes[id] = node;
}
}
if (dataChanged) {
this.body.emitter.emit("_dataChanged");
}
else {
this.body.emitter.emit("_dataUpdated");
}
}
/**
* Remove existing nodes. If nodes do not exist, the method will just ignore it.
* @param {Number[] | String[]} ids
* @private
*/
remove(ids) {
var nodes = this.body.nodes;
for (let i = 0; i < ids.length; i++) {
var id = ids[i];
delete nodes[id];
}
this.body.emitter.emit("_dataChanged");
}
}
export default NodesHandler;

+ 13
- 4
lib/network/modules/PhysicsEngine.js View File

@ -26,6 +26,7 @@ class PhysicsEngine {
this.stabilized = false; this.stabilized = false;
this.stabilizationIterations = 0; this.stabilizationIterations = 0;
this.ready = false; // will be set to true if the stabilize
// default options // default options
this.options = {}; this.options = {};
@ -66,8 +67,14 @@ class PhysicsEngine {
} }
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.body.emitter.on("stabilize", () => {this.startSimulation();});
this.body.emitter.on("startSimulation", () => {this.stabilized = false; this.runSimulation();});
this.body.emitter.on("initPhysics", () => {this.initPhysics();});
this.body.emitter.on("resetPhysics", () => {this.stopSimulation(); this.ready = false;});
this.body.emitter.on("startSimulation", () => {
if (this.ready === true) {
this.stabilized = false;
this.runSimulation();
}
})
this.body.emitter.on("stopSimulation", () => {this.stopSimulation();}); this.body.emitter.on("stopSimulation", () => {this.stopSimulation();});
} }
@ -106,12 +113,14 @@ class PhysicsEngine {
this.modelOptions = options; this.modelOptions = options;
} }
startSimulation() {
initPhysics() {
this.stabilized = false; this.stabilized = false;
this.ready = true;
if (this.options.stabilization.enabled === true) { if (this.options.stabilization.enabled === true) {
this.stabilize(); this.stabilize();
} }
else { else {
this.body.emitter.emit("zoomExtent", {duration:0}, true)
this.runSimulation(); this.runSimulation();
} }
} }
@ -425,4 +434,4 @@ class PhysicsEngine {
} }
export {PhysicsEngine};
export default PhysicsEngine;

+ 9
- 4
lib/network/modules/SelectionHandler.js View File

@ -2,14 +2,14 @@
* Created by Alex on 2/27/2015. * Created by Alex on 2/27/2015.
*/ */
var Node = require("../Node");
var Node = require("./components/nodes/NodeMain");
var util = require('../../util'); var util = require('../../util');
class SelectionHandler { class SelectionHandler {
constructor(body, canvas) { constructor(body, canvas) {
this.body = body; this.body = body;
this.canvas = canvas; this.canvas = canvas;
this.selectionObj = {nodes:[], edges:[]};
this.selectionObj = {nodes: [], edges: []};
this.options = {}; this.options = {};
this.defaultOptions = { this.defaultOptions = {
@ -17,8 +17,13 @@ class SelectionHandler {
selectConnectedEdges: true selectConnectedEdges: true
} }
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.body.emitter.on("_dataChanged", () => {
this.updateSelection()
});
} }
setOptions(options) { setOptions(options) {
if (options !== undefined) { if (options !== undefined) {
util.deepExtend(this.options, options); util.deepExtend(this.options, options);
@ -592,7 +597,7 @@ class SelectionHandler {
* Validate the selection: remove ids of nodes which no longer exist * Validate the selection: remove ids of nodes which no longer exist
* @private * @private
*/ */
_updateSelection() {
updateSelection() {
for (var nodeId in this.selectionObj.nodes) { for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.body.nodes.hasOwnProperty(nodeId)) { if (!this.body.nodes.hasOwnProperty(nodeId)) {
@ -610,4 +615,4 @@ class SelectionHandler {
} }
} }
export {SelectionHandler};
export default SelectionHandler;

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

@ -124,12 +124,7 @@ class View {
range = this._getRange(options.nodes); range = this._getRange(options.nodes);
var numberOfNodes = this.body.nodeIndices.length; var numberOfNodes = this.body.nodeIndices.length;
if (this.options.smoothCurves == true) {
zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
else {
zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
// correct for larger canvasses. // correct for larger canvasses.
var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
@ -331,4 +326,4 @@ class View {
} }
export {View};
export default View;

+ 5
- 4
lib/network/modules/components/NavigationHandler.js View File

@ -18,6 +18,7 @@ class NavigationHandler {
this.body.emitter.on("release", this._stopMovement.bind(this)); this.body.emitter.on("release", this._stopMovement.bind(this));
this.body.emitter.on("activate", () => {this.activated = true; this.configureKeyboardBindings();}); this.body.emitter.on("activate", () => {this.activated = true; this.configureKeyboardBindings();});
this.body.emitter.on("deactivate", () => {this.activated = false; this.configureKeyboardBindings();}); this.body.emitter.on("deactivate", () => {this.activated = false; this.configureKeyboardBindings();});
this.body.emitter.on("destroy", () => {if (this.keycharm !== undefined) {this.keycharm.destroy();}});
this.options = {} this.options = {}
} }
@ -141,10 +142,10 @@ class NavigationHandler {
this.boundFunctions = {}; this.boundFunctions = {};
} }
_moveUp() {this.body.view.translation.y -= this.options.keyboard.speed.y;}
_moveDown() {this.body.view.translation.y += this.options.keyboard.speed.y;}
_moveLeft() {this.body.view.translation.x -= this.options.keyboard.speed.x;}
_moveRight(){this.body.view.translation.x += this.options.keyboard.speed.x;}
_moveUp() {this.body.view.translation.y += this.options.keyboard.speed.y;}
_moveDown() {this.body.view.translation.y -= this.options.keyboard.speed.y;}
_moveLeft() {this.body.view.translation.x += this.options.keyboard.speed.x;}
_moveRight(){this.body.view.translation.x -= this.options.keyboard.speed.x;}
_zoomIn() {this.body.view.scale += this.options.keyboard.speed.zoom;} _zoomIn() {this.body.view.scale += this.options.keyboard.speed.zoom;}
_zoomOut() {this.body.view.scale -= this.options.keyboard.speed.zoom;} _zoomOut() {this.body.view.scale -= this.options.keyboard.speed.zoom;}

lib/network/Edge.js → lib/network/modules/components/edges/EdgeMain.js View File

@ -1,5 +1,5 @@
var util = require('../util');
var Node = require('./Node');
var util = require('../../../../util');
var Node = require('../nodes/NodeMain');
/** /**
* @class Edge * @class Edge
@ -16,15 +16,14 @@ var Node = require('./Node');
* @param {Object} constants An object with default values for * @param {Object} constants An object with default values for
* example for the color * example for the color
*/ */
function Edge (properties, body, networkConstants) {
function Edge (properties, body, globalOptions) {
if (body === undefined) { if (body === undefined) {
throw "No body provided"; throw "No body provided";
} }
var fields = ['edges'];
var constants = util.selectiveBridgeObject(fields,networkConstants);
this.options = constants.edges;
this.options['smoothCurves'] = networkConstants['smoothCurves'];
this.options = util.bridgeObject(globalOptions);
console.log(this.options)
// TODO: fix this
//this.options['smoothCurves'] = networkConstants['smoothCurves'];
this.body = body; this.body = body;
// initialize variables // initialize variables

lib/network/Node.js → lib/network/modules/components/nodes/NodeMain.js View File

@ -1,4 +1,4 @@
var util = require('../util');
var util = require('../../../../util');
/** /**
* @class Node * @class Node
@ -25,9 +25,8 @@ var util = require('../util');
* example for the color * example for the color
* *
*/ */
function Node(properties, imagelist, grouplist, networkConstants) {
var constants = util.selectiveBridgeObject(['nodes'],networkConstants);
this.options = constants.nodes;
function Node(properties, imagelist, grouplist, globalOptions) {
this.options = util.bridgeObject(globalOptions);
this.selected = false; this.selected = false;
this.hover = false; this.hover = false;
@ -42,7 +41,7 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.yFixed = false; this.yFixed = false;
this.horizontalAlignLeft = true; // these are for the navigation controls this.horizontalAlignLeft = true; // these are for the navigation controls
this.verticalAlignTop = true; // these are for the navigation controls this.verticalAlignTop = true; // these are for the navigation controls
this.baseRadiusValue = networkConstants.nodes.radius;
this.baseRadiusValue = this.options.radius;
this.radiusFixed = false; this.radiusFixed = false;
this.level = -1; this.level = -1;
this.preassignedLevel = false; this.preassignedLevel = false;
@ -58,12 +57,10 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.y = null; this.y = null;
this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate
// used for reverting to previous position on stabilization
this.previousState = {vx:0,vy:0,x:0,y:0};
this.fixedData = {x:null,y:null}; this.fixedData = {x:null,y:null};
this.setProperties(properties, constants);
// TODO: global options should not be needed here.
this.setProperties(properties, globalOptions);
// variables to tell the node about the network. // variables to tell the node about the network.
this.networkScaleInv = 1; this.networkScaleInv = 1;

+ 7
- 0
lib/util.js View File

@ -14,6 +14,13 @@ exports.isNumber = function(object) {
}; };
exports.recursiveDOMDelete = function(DOMobject) {
while (DOMobject.hasChildNodes() == true) {
exports.recursiveDOMDelete(DOMobject.firstChild);
DOMobject.removeChild(DOMobject.firstChild);
}
};
/** /**
* this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value. * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
* *

Loading…
Cancel
Save