visjs-network
Version:
A dynamic, browser-based network visualization library.
404 lines (356 loc) • 11.1 kB
JavaScript
var util = require('./util')
var DataSet = require('./DataSet')
/**
* DataView
*
* a dataview offers a filtered view on a dataset or an other dataview.
*
* @param {DataSet | DataView} data
* @param {Object} [options] Available options: see method get
*
* @constructor DataView
*/
function DataView(data, options) {
this._data = null
this._ids = {} // ids of the items currently in memory (just contains a boolean true)
this.length = 0 // number of items in the DataView
this._options = options || {}
this._fieldId = 'id' // name of the field containing id
this._subscribers = {} // event subscribers
var me = this
this.listener = function() {
me._onEvent.apply(me, arguments)
}
this.setData(data)
}
// TODO: implement a function .config() to dynamically update things like configured filter
// and trigger changes accordingly
/**
* Set a data source for the view
* @param {DataSet | DataView} data
*/
DataView.prototype.setData = function(data) {
var ids, id, i, len, items
if (this._data) {
// unsubscribe from current dataset
if (this._data.off) {
this._data.off('*', this.listener)
}
// trigger a remove of all items in memory
ids = this._data.getIds({ filter: this._options && this._options.filter })
items = []
for (i = 0, len = ids.length; i < len; i++) {
items.push(this._data._data[ids[i]])
}
this._ids = {}
this.length = 0
this._trigger('remove', { items: ids, oldData: items })
}
this._data = data
if (this._data) {
// update fieldId
this._fieldId =
this._options.fieldId ||
(this._data && this._data.options && this._data.options.fieldId) ||
'id'
// trigger an add of all added items
ids = this._data.getIds({ filter: this._options && this._options.filter })
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i]
this._ids[id] = true
}
this.length = ids.length
this._trigger('add', { items: ids })
// subscribe to new dataset
if (this._data.on) {
this._data.on('*', this.listener)
}
}
}
/**
* Refresh the DataView. Useful when the DataView has a filter function
* containing a variable parameter.
*/
DataView.prototype.refresh = function() {
var id, i, len
var ids = this._data.getIds({
filter: this._options && this._options.filter
}),
oldIds = Object.keys(this._ids),
newIds = {},
addedIds = [],
removedIds = [],
removedItems = []
// check for additions
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i]
newIds[id] = true
if (!this._ids[id]) {
addedIds.push(id)
this._ids[id] = true
}
}
// check for removals
for (i = 0, len = oldIds.length; i < len; i++) {
id = oldIds[i]
if (!newIds[id]) {
removedIds.push(id)
removedItems.push(this._data._data[id])
delete this._ids[id]
}
}
this.length += addedIds.length - removedIds.length
// trigger events
if (addedIds.length) {
this._trigger('add', { items: addedIds })
}
if (removedIds.length) {
this._trigger('remove', { items: removedIds, oldData: removedItems })
}
}
// prettier-ignore
/**
* Get data from the data view
*
* Usage:
*
* get()
* get(options: Object)
* get(options: Object, data: Array | DataTable)
*
* get(id: Number)
* get(id: Number, options: Object)
* get(id: Number, options: Object, data: Array | DataTable)
*
* get(ids: Number[])
* get(ids: Number[], options: Object)
* get(ids: Number[], options: Object, data: Array | DataTable)
*
* Where:
*
* {number | string} id The id of an item
* {number[] | string{}} ids An array with ids of items
* {Object} options An Object with options. Available options:
* {string} [type] Type of data to be returned. Can
* be 'DataTable' or 'Array' (default)
* {Object.<string, string>} [convert]
* {string[]} [fields] field names to be returned
* {function} [filter] filter items
* {string | function} [order] Order the items by
* a field name or custom sort function.
* {Array | DataTable} [data] If provided, items will be appended to this
* array or table. Required in case of Google
* DataTable.
* @param {Array} args
* @return {DataSet|DataView}
*/
DataView.prototype.get = function(args) { // eslint-disable-line no-unused-vars
var me = this
// parse the arguments
var ids, options, data
var firstType = util.getType(arguments[0])
if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
// get(id(s) [, options] [, data])
ids = arguments[0] // can be a single id or an array with ids
options = arguments[1]
data = arguments[2]
} else {
// get([, options] [, data])
options = arguments[0]
data = arguments[1]
}
// extend the options with the default options and provided options
var viewOptions = util.extend({}, this._options, options)
// create a combined filter method when needed
if (this._options.filter && options && options.filter) {
viewOptions.filter = function(item) {
return me._options.filter(item) && options.filter(item)
}
}
// build up the call to the linked data set
var getArguments = []
if (ids != undefined) {
getArguments.push(ids)
}
getArguments.push(viewOptions)
getArguments.push(data)
return this._data && this._data.get.apply(this._data, getArguments)
}
/**
* Get ids of all items or from a filtered set of items.
* @param {Object} [options] An Object with options. Available options:
* {function} [filter] filter items
* {string | function} [order] Order the items by
* a field name or custom sort function.
* @return {Array.<string|number>} ids
*/
DataView.prototype.getIds = function(options) {
var ids
if (this._data) {
var defaultFilter = this._options.filter
var filter
if (options && options.filter) {
if (defaultFilter) {
filter = function(item) {
return defaultFilter(item) && options.filter(item)
}
} else {
filter = options.filter
}
} else {
filter = defaultFilter
}
ids = this._data.getIds({
filter: filter,
order: options && options.order
})
} else {
ids = []
}
return ids
}
/**
* Map every item in the dataset.
* @param {function} callback
* @param {Object} [options] Available options:
* {Object.<string, string>} [type]
* {string[]} [fields] filter fields
* {function} [filter] filter items
* {string | function} [order] Order the items by
* a field name or custom sort function.
* @return {Object[]} mappedItems
*/
DataView.prototype.map = function(callback, options) {
var mappedItems = []
if (this._data) {
var defaultFilter = this._options.filter
var filter
if (options && options.filter) {
if (defaultFilter) {
filter = function(item) {
return defaultFilter(item) && options.filter(item)
}
} else {
filter = options.filter
}
} else {
filter = defaultFilter
}
mappedItems = this._data.map(callback, {
filter: filter,
order: options && options.order
})
} else {
mappedItems = []
}
return mappedItems
}
/**
* Get the DataSet to which this DataView is connected. In case there is a chain
* of multiple DataViews, the root DataSet of this chain is returned.
* @return {DataSet} dataSet
*/
DataView.prototype.getDataSet = function() {
var dataSet = this
while (dataSet instanceof DataView) {
dataSet = dataSet._data
}
return dataSet || null
}
/**
* Event listener. Will propagate all events from the connected data set to
* the subscribers of the DataView, but will filter the items and only trigger
* when there are changes in the filtered data set.
* @param {string} event
* @param {Object | null} params
* @param {string} senderId
* @private
*/
DataView.prototype._onEvent = function(event, params, senderId) {
var i, len, id, item
var ids = params && params.items
var addedIds = [],
updatedIds = [],
removedIds = [],
oldItems = [],
updatedItems = [],
removedItems = []
if (ids && this._data) {
switch (event) {
case 'add':
// filter the ids of the added items
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i]
item = this.get(id)
if (item) {
this._ids[id] = true
addedIds.push(id)
}
}
break
case 'update':
// determine the event from the views viewpoint: an updated
// item can be added, updated, or removed from this view.
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i]
item = this.get(id)
if (item) {
if (this._ids[id]) {
updatedIds.push(id)
updatedItems.push(params.data[i])
oldItems.push(params.oldData[i])
} else {
this._ids[id] = true
addedIds.push(id)
}
} else {
if (this._ids[id]) {
delete this._ids[id]
removedIds.push(id)
removedItems.push(params.oldData[i])
} else {
// nothing interesting for me :-(
}
}
}
break
case 'remove':
// filter the ids of the removed items
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i]
if (this._ids[id]) {
delete this._ids[id]
removedIds.push(id)
removedItems.push(params.oldData[i])
}
}
break
}
this.length += addedIds.length - removedIds.length
if (addedIds.length) {
this._trigger('add', { items: addedIds }, senderId)
}
if (updatedIds.length) {
this._trigger(
'update',
{ items: updatedIds, oldData: oldItems, data: updatedItems },
senderId
)
}
if (removedIds.length) {
this._trigger(
'remove',
{ items: removedIds, oldData: removedItems },
senderId
)
}
}
}
// copy subscription functionality from DataSet
DataView.prototype.on = DataSet.prototype.on
DataView.prototype.off = DataSet.prototype.off
DataView.prototype._trigger = DataSet.prototype._trigger
// TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
DataView.prototype.subscribe = DataView.prototype.on
DataView.prototype.unsubscribe = DataView.prototype.off
module.exports = DataView