visjs-network
Version:
A dynamic, browser-based network visualization library.
501 lines (452 loc) • 13.1 kB
JavaScript
var util = require('../../util')
var DataSet = require('../../DataSet')
var DataView = require('../../DataView')
var Edge = require('./components/Edge').default
/**
* Handler for Edges
*/
class EdgesHandler {
/**
* @param {Object} body
* @param {Array.<Image>} images
* @param {Array.<Group>} groups
*/
constructor(body, images, groups) {
this.body = body
this.images = images
this.groups = groups
// create the edge API in the body container
this.body.functions.createEdge = this.create.bind(this)
this.edgesListeners = {
add: (event, params) => {
this.add(params.items)
},
update: (event, params) => {
this.update(params.items)
},
remove: (event, params) => {
this.remove(params.items)
}
}
this.options = {}
this.defaultOptions = {
arrows: {
to: { enabled: false, scaleFactor: 1, type: 'arrow' }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}
middle: { enabled: false, scaleFactor: 1, type: 'arrow' },
from: { enabled: false, scaleFactor: 1, type: 'arrow' }
},
arrowStrikethrough: true,
color: {
color: '#848484',
highlight: '#848484',
hover: '#848484',
inherit: 'from',
opacity: 1.0
},
dashes: false,
font: {
color: '#343434',
size: 14, // px
face: 'arial',
background: 'none',
strokeWidth: 2, // px
strokeColor: '#ffffff',
align: 'horizontal',
multi: false,
vadjust: 0,
bold: {
mod: 'bold'
},
boldital: {
mod: 'bold italic'
},
ital: {
mod: 'italic'
},
mono: {
mod: '',
size: 15, // px
face: 'courier new',
vadjust: 2
}
},
hidden: false,
hoverWidth: 1.5,
label: undefined,
labelHighlightBold: true,
length: undefined,
physics: true,
scaling: {
min: 1,
max: 15,
label: {
enabled: true,
min: 14,
max: 30,
maxVisible: 30,
drawThreshold: 5
},
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)
}
}
},
selectionWidth: 1.5,
selfReferenceSize: 20,
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: 10,
x: 5,
y: 5
},
background: {
enabled: false,
color: 'rgba(111,111,111,1)',
size: 10,
dashes: false
},
smooth: {
enabled: true,
type: 'dynamic',
forceDirection: 'none',
roundness: 0.5
},
title: undefined,
width: 1,
value: undefined
}
util.deepExtend(this.options, this.defaultOptions)
this.bindEventListeners()
}
/**
* Binds event listeners
*/
bindEventListeners() {
// this allows external modules to force all dynamic curves to turn static.
this.body.emitter.on('_forceDisableDynamicCurves', (type, emit = true) => {
if (type === 'dynamic') {
type = 'continuous'
}
let dataChanged = false
for (let edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
let edge = this.body.edges[edgeId]
let edgeData = this.body.data.edges._data[edgeId]
// only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.
// this is because a change in the global would not affect these curves.
if (edgeData !== undefined) {
let smoothOptions = edgeData.smooth
if (smoothOptions !== undefined) {
if (
smoothOptions.enabled === true &&
smoothOptions.type === 'dynamic'
) {
if (type === undefined) {
edge.setOptions({ smooth: false })
} else {
edge.setOptions({ smooth: { type: type } })
}
dataChanged = true
}
}
}
}
}
if (emit === true && dataChanged === true) {
this.body.emitter.emit('_dataChanged')
}
})
// this is called when options of EXISTING nodes or edges have changed.
//
// NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.
// See update() for logic.
// TODO: Verify and examine the consequences of this. It might still trigger when
// non-option fields have changed, but then reconnecting edges is still useless.
// Alternatively, it might also be called when edges are removed.
//
this.body.emitter.on('_dataUpdated', () => {
this.reconnectEdges()
})
// refresh the edges. Used when reverting from hierarchical layout
this.body.emitter.on('refreshEdges', this.refresh.bind(this))
this.body.emitter.on('refresh', this.refresh.bind(this))
this.body.emitter.on('destroy', () => {
util.forEach(this.edgesListeners, (callback, event) => {
if (this.body.data.edges) this.body.data.edges.off(event, callback)
})
delete this.body.functions.createEdge
delete this.edgesListeners.add
delete this.edgesListeners.update
delete this.edgesListeners.remove
delete this.edgesListeners
})
}
/**
*
* @param {Object} options
*/
setOptions(options) {
if (options !== undefined) {
// use the parser from the Edge class to fill in all shorthand notations
Edge.parseOptions(this.options, options, true, this.defaultOptions, true)
// update smooth settings in all edges
let dataChanged = false
if (options.smooth !== undefined) {
for (let edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
dataChanged =
this.body.edges[edgeId].updateEdgeType() || dataChanged
}
}
}
// update fonts in all edges
if (options.font !== undefined) {
for (let edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
this.body.edges[edgeId].updateLabelModule()
}
}
}
// update the state of the variables if needed
if (
options.hidden !== undefined ||
options.physics !== undefined ||
dataChanged === true
) {
this.body.emitter.emit('_dataChanged')
}
}
}
/**
* Load edges by reading the data table
* @param {Array | DataSet | DataView} edges The data containing the edges.
* @param {boolean} [doNotEmit=false]
* @private
*/
setData(edges, doNotEmit = false) {
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, true)
}
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout')
if (doNotEmit === false) {
this.body.emitter.emit('_dataChanged')
}
}
/**
* Add edges
* @param {number[] | string[]} ids
* @param {boolean} [doNotEmit=false]
* @private
*/
add(ids, doNotEmit = false) {
var edges = this.body.edges
var edgesData = this.body.data.edges
for (let 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] = this.create(data)
}
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout')
if (doNotEmit === false) {
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
var dataChanged = false
for (var i = 0; i < ids.length; i++) {
var id = ids[i]
var data = edgesData.get(id)
var edge = edges[id]
if (edge !== undefined) {
// update edge
edge.disconnect()
dataChanged = edge.setOptions(data) || dataChanged // if a support node is added, data can be changed.
edge.connect()
} else {
// create edge
this.body.edges[id] = this.create(data)
dataChanged = true
}
}
if (dataChanged === true) {
this.body.emitter.emit('_adjustEdgesForHierarchicalLayout')
this.body.emitter.emit('_dataChanged')
} else {
this.body.emitter.emit('_dataUpdated')
}
}
/**
* Remove existing edges. Non existing ids will be ignored
* @param {number[] | string[]} ids
* @param {boolean} [emit=true]
* @private
*/
remove(ids, emit = true) {
if (ids.length === 0) return // early out
var edges = this.body.edges
util.forEach(ids, id => {
var edge = edges[id]
if (edge !== undefined) {
edge.remove()
}
})
if (emit) {
this.body.emitter.emit('_dataChanged')
}
}
/**
* Refreshes Edge Handler
*/
refresh() {
util.forEach(this.body.edges, (edge, edgeId) => {
let data = this.body.data.edges._data[edgeId]
if (data !== undefined) {
edge.setOptions(data)
}
})
}
/**
*
* @param {Object} properties
* @returns {Edge}
*/
create(properties) {
return new Edge(properties, this.body, this.options, this.defaultOptions)
}
/**
* Reconnect all edges
* @private
*/
reconnectEdges() {
var id
var nodes = this.body.nodes
var edges = this.body.edges
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].edges = []
}
}
for (id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id]
edge.from = null
edge.to = null
edge.connect()
}
}
}
/**
*
* @param {Edge.id} edgeId
* @returns {Array}
*/
getConnectedNodes(edgeId) {
let nodeList = []
if (this.body.edges[edgeId] !== undefined) {
let edge = this.body.edges[edgeId]
if (edge.fromId !== undefined) {
nodeList.push(edge.fromId)
}
if (edge.toId !== undefined) {
nodeList.push(edge.toId)
}
}
return nodeList
}
/**
* There is no direct relation between the nodes and the edges DataSet,
* so the right place to do call this is in the handler for event `_dataUpdated`.
*/
_updateState() {
this._addMissingEdges()
this._removeInvalidEdges()
}
/**
* Scan for missing nodes and remove corresponding edges, if any.
* @private
*/
_removeInvalidEdges() {
let edgesToDelete = []
util.forEach(this.body.edges, (edge, id) => {
let toNode = this.body.nodes[edge.toId]
let fromNode = this.body.nodes[edge.fromId]
// Skip clustering edges here, let the Clustering module handle those
if (
(toNode !== undefined && toNode.isCluster === true) ||
(fromNode !== undefined && fromNode.isCluster === true)
) {
return
}
if (toNode === undefined || fromNode === undefined) {
edgesToDelete.push(id)
}
})
this.remove(edgesToDelete, false)
}
/**
* add all edges from dataset that are not in the cached state
* @private
*/
_addMissingEdges() {
let edgesData = this.body.data.edges
if (edgesData === undefined || edgesData === null) {
return // No edges DataSet yet; can happen on startup
}
let edges = this.body.edges
let addIds = []
if (edgesData instanceof DataView) {
edgesData = edgesData.getDataSet()
}
edgesData.forEach((edgeData, edgeId) => {
let edge = edges[edgeId]
if (edge === undefined) {
addIds.push(edgeId)
}
})
this.add(addIds, true)
}
}
export default EdgesHandler