vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

514 lines
14 KiB

let util = require("../../util");
let DataSet = require('../../DataSet');
let DataView = require('../../DataView');
var Node = require("./components/Node").default;
/**
* Handler for Nodes
*/
class NodesHandler {
/**
* @param {Object} body
* @param {Images} images
* @param {Array.<Group>} groups
* @param {LayoutEngine} layoutEngine
*/
constructor(body, images, groups, layoutEngine) {
this.body = body;
this.images = images;
this.groups = groups;
this.layoutEngine = layoutEngine;
// create the node API in the body container
this.body.functions.createNode = this.create.bind(this);
this.nodesListeners = {
add: (event, params) => { this.add(params.items); },
update: (event, params) => { this.update(params.items, params.data, params.oldData); },
remove: (event, params) => { this.remove(params.items); }
};
this.defaultOptions = {
borderWidth: 1,
borderWidthSelected: 2,
brokenImage: undefined,
color: {
border: '#2B7CE9',
background: '#97C2FC',
highlight: {
border: '#2B7CE9',
background: '#D2E5FF'
},
hover: {
border: '#2B7CE9',
background: '#D2E5FF'
}
},
fixed: {
x: false,
y: false
},
font: {
color: '#343434',
size: 14, // px
face: 'arial',
background: 'none',
strokeWidth: 0, // px
strokeColor: '#ffffff',
align: 'center',
vadjust: 0,
multi: false,
bold: {
mod: 'bold'
},
boldital: {
mod: 'bold italic'
},
ital: {
mod: 'italic'
},
mono: {
mod: '',
size: 15, // px
face: 'monospace',
vadjust: 2
}
},
group: undefined,
hidden: false,
icon: {
face: 'FontAwesome', //'FontAwesome',
code: undefined, //'\uf007',
size: 50, //50,
color: '#2B7CE9' //'#aa00ff'
},
image: undefined, // --> URL
label: undefined,
labelHighlightBold: true,
level: undefined,
margin: {
top: 5,
right: 5,
bottom: 5,
left: 5
},
mass: 1,
physics: true,
scaling: {
min: 10,
max: 30,
label: {
enabled: false,
min: 14,
max: 30,
maxVisible: 30,
drawThreshold: 5
},
customScalingFunction: function (min, max, total, value) {
if (max === min) {
return 0.5;
}
else {
let scale = 1 / (max - min);
return Math.max(0, (value - min) * scale);
}
}
},
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: 10,
x: 5,
y: 5
},
shape: 'ellipse',
shapeProperties: {
borderDashes: false, // only for borders
borderRadius: 6, // only for box shape
interpolation: true, // only for image and circularImage shapes
useImageSize: false, // only for image and circularImage shapes
useBorderWithImage: false // only for image shape
},
size: 25,
title: undefined,
value: undefined,
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';
}
this.options = util.bridgeObject(this.defaultOptions);
this.bindEventListeners();
}
/**
* Binds event listeners
*/
bindEventListeners() {
// refresh the nodes. Used when reverting from hierarchical layout
this.body.emitter.on('refreshNodes', this.refresh.bind(this));
this.body.emitter.on('refresh', this.refresh.bind(this));
this.body.emitter.on('destroy', () => {
util.forEach(this.nodesListeners, (callback, event) => {
if (this.body.data.nodes)
this.body.data.nodes.off(event, callback);
});
delete this.body.functions.createNode;
delete this.nodesListeners.add;
delete this.nodesListeners.update;
delete this.nodesListeners.remove;
delete this.nodesListeners;
});
}
/**
*
* @param {Object} options
*/
setOptions(options) {
if (options !== undefined) {
Node.parseOptions(this.options, options);
// update the shape in all nodes
if (options.shape !== undefined) {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].updateShape();
}
}
}
// update the font in all nodes
if (options.font !== undefined) {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].updateLabelModule();
this.body.nodes[nodeId].needsRefresh();
}
}
}
// update the shape size in all nodes
if (options.size !== undefined) {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].needsRefresh();
}
}
}
// update the state of the variables if needed
if (options.hidden !== undefined || options.physics !== undefined) {
this.body.emitter.emit('_dataChanged');
}
}
}
/**
* Set a data set with nodes for the network
* @param {Array | DataSet | DataView} nodes The data containing the nodes.
* @param {boolean} [doNotEmit=false]
* @private
*/
setData(nodes, doNotEmit = false) {
let 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
let me = this;
util.forEach(this.nodesListeners, function (callback, event) {
me.body.data.nodes.on(event, callback);
});
// draw all new nodes
let ids = this.body.data.nodes.getIds();
this.add(ids, true);
}
if (doNotEmit === false) {
this.body.emitter.emit("_dataChanged");
}
}
/**
* Add nodes
* @param {number[] | string[]} ids
* @param {boolean} [doNotEmit=false]
* @private
*/
add(ids, doNotEmit = false) {
let id;
let newNodes = [];
for (let i = 0; i < ids.length; i++) {
id = ids[i];
let properties = this.body.data.nodes.get(id);
let node = this.create(properties);
newNodes.push(node);
this.body.nodes[id] = node; // note: this may replace an existing node
}
this.layoutEngine.positionInitially(newNodes);
if (doNotEmit === false) {
this.body.emitter.emit("_dataChanged");
}
}
/**
* Update existing nodes, or create them when not yet existing
* @param {number[] | string[]} ids id's of changed nodes
* @param {Array} changedData array with changed data
* @param {Array|undefined} oldData optional; array with previous data
* @private
*/
update(ids, changedData, oldData) {
let nodes = this.body.nodes;
let dataChanged = false;
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let node = nodes[id];
let data = changedData[i];
if (node !== undefined) {
// update node
if (node.setOptions(data)) {
dataChanged = true;
}
}
else {
dataChanged = true;
// create node
node = this.create(data);
nodes[id] = node;
}
}
if (!dataChanged && oldData !== undefined) {
// Check for any changes which should trigger a layout recalculation
// For now, this is just 'level' for hierarchical layout
// Assumption: old and new data arranged in same order; at time of writing, this holds.
dataChanged = changedData.some(function(newValue, index) {
let oldValue = oldData[index];
return (oldValue && oldValue.level !== newValue.level);
});
}
if (dataChanged === true) {
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) {
let nodes = this.body.nodes;
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
delete nodes[id];
}
this.body.emitter.emit("_dataChanged");
}
/**
* create a node
* @param {Object} properties
* @param {class} [constructorClass=Node.default]
* @returns {*}
*/
create(properties, constructorClass = Node) {
return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions)
}
/**
*
* @param {boolean} [clearPositions=false]
*/
refresh(clearPositions = false) {
util.forEach(this.body.nodes, (node, nodeId) => {
let data = this.body.data.nodes.get(nodeId);
if (data !== undefined) {
if (clearPositions === true) {
node.setOptions({x:null, y:null});
}
node.setOptions({ fixed: false });
node.setOptions(data);
}
});
}
/**
* Returns the positions of the nodes.
* @param {Array.<Node.id>|String} [ids] --> optional, can be array of nodeIds, can be string
* @returns {{}}
*/
getPositions(ids) {
let dataArray = {};
if (ids !== undefined) {
if (Array.isArray(ids) === true) {
for (let i = 0; i < ids.length; i++) {
if (this.body.nodes[ids[i]] !== undefined) {
let node = this.body.nodes[ids[i]];
dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
}
}
}
else {
if (this.body.nodes[ids] !== undefined) {
let node = this.body.nodes[ids];
dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
}
}
}
else {
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let node = this.body.nodes[this.body.nodeIndices[i]];
dataArray[this.body.nodeIndices[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
}
}
return dataArray;
}
/**
* Load the XY positions of the nodes into the dataset.
*/
storePositions() {
// todo: add support for clusters and hierarchical.
let dataArray = [];
var dataset = this.body.data.nodes.getDataSet();
for (let nodeId in dataset._data) {
if (dataset._data.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
dataArray.push({ id: node.id, x: Math.round(node.x), y: Math.round(node.y) });
}
}
}
dataset.update(dataArray);
}
/**
* get the bounding box of a node.
* @param {Node.id} nodeId
* @returns {j|*}
*/
getBoundingBox(nodeId) {
if (this.body.nodes[nodeId] !== undefined) {
return this.body.nodes[nodeId].shape.boundingBox;
}
}
/**
* Get the Ids of nodes connected to this node.
* @param {Node.id} nodeId
* @param {'to'|'from'|undefined} direction values 'from' and 'to' select respectively parent and child nodes only.
* Any other value returns both parent and child nodes.
* @returns {Array}
*/
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 (direction !== 'to' && 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 (direction !== 'from' && 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;
}
}
}
}
return nodeList;
}
/**
* Get the ids of the edges connected to this node.
* @param {Node.id} nodeId
* @returns {*}
*/
getConnectedEdges(nodeId) {
let edgeList = [];
if (this.body.nodes[nodeId] !== undefined) {
let node = this.body.nodes[nodeId];
for (let i = 0; i < node.edges.length; i++) {
edgeList.push(node.edges[i].id)
}
}
else {
console.log("NodeId provided for getConnectedEdges does not exist. Provided: ", nodeId);
}
return edgeList;
}
/**
* Move a node.
*
* @param {Node.id} nodeId
* @param {number} x
* @param {number} y
*/
moveNode(nodeId, x, y) {
if (this.body.nodes[nodeId] !== undefined) {
this.body.nodes[nodeId].x = Number(x);
this.body.nodes[nodeId].y = Number(y);
setTimeout(() => {this.body.emitter.emit("startSimulation")},0);
}
else {
console.log("Node id supplied to moveNode does not exist. Provided: ", nodeId);
}
}
}
export default NodesHandler;