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.
 
 
 

401 lines
11 KiB

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});
}
};
/**
* 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;