vis-data
Version:
Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data.
1,428 lines (1,422 loc) • 72.3 kB
JavaScript
/**
* vis-data
* http://visjs.org/
*
* Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data.
*
* @version 7.0.0
* @date 2020-08-02T17:48:43.502Z
*
* @copyright (c) 2011-2017 Almende B.V, http://almende.com
* @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
*
* @license
* vis.js is dual licensed under both
*
* 1. The Apache 2.0 License
* http://www.apache.org/licenses/LICENSE-2.0
*
* and
*
* 2. The MIT License
* http://opensource.org/licenses/MIT
*
* vis.js may be distributed under either license.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('uuid'), require('vis-util/esnext/umd/vis-util.js')) :
typeof define === 'function' && define.amd ? define(['exports', 'uuid', 'vis-util/esnext/umd/vis-util.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vis = global.vis || {}, global.uuidv4, global.vis));
}(this, (function (exports, uuid, esnext) {
/* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */
/**
* Create new data pipe.
*
* @param from - The source data set or data view.
*
* @remarks
* Example usage:
* ```typescript
* interface AppItem {
* whoami: string;
* appData: unknown;
* visData: VisItem;
* }
* interface VisItem {
* id: number;
* label: string;
* color: string;
* x: number;
* y: number;
* }
*
* const ds1 = new DataSet<AppItem, "whoami">([], { fieldId: "whoami" });
* const ds2 = new DataSet<VisItem, "id">();
*
* const pipe = createNewDataPipeFrom(ds1)
* .filter((item): boolean => item.enabled === true)
* .map<VisItem, "id">((item): VisItem => item.visData)
* .to(ds2);
*
* pipe.start();
* ```
*
* @returns A factory whose methods can be used to configure the pipe.
*/
function createNewDataPipeFrom(from) {
return new DataPipeUnderConstruction(from);
}
/**
* Internal implementation of the pipe. This should be accessible only through
* `createNewDataPipeFrom` from the outside.
*
* @typeparam SI - Source item type.
* @typeparam SP - Source item type's id property name.
* @typeparam TI - Target item type.
* @typeparam TP - Target item type's id property name.
*/
class SimpleDataPipe {
/**
* Create a new data pipe.
*
* @param _source - The data set or data view that will be observed.
* @param _transformers - An array of transforming functions to be used to
* filter or transform the items in the pipe.
* @param _target - The data set or data view that will receive the items.
*/
constructor(_source, _transformers, _target) {
this._source = _source;
this._transformers = _transformers;
this._target = _target;
/**
* Bound listeners for use with `DataInterface['on' | 'off']`.
*/
this._listeners = {
add: this._add.bind(this),
remove: this._remove.bind(this),
update: this._update.bind(this),
};
}
/** @inheritdoc */
all() {
this._target.update(this._transformItems(this._source.get()));
return this;
}
/** @inheritdoc */
start() {
this._source.on("add", this._listeners.add);
this._source.on("remove", this._listeners.remove);
this._source.on("update", this._listeners.update);
return this;
}
/** @inheritdoc */
stop() {
this._source.off("add", this._listeners.add);
this._source.off("remove", this._listeners.remove);
this._source.off("update", this._listeners.update);
return this;
}
/**
* Apply the transformers to the items.
*
* @param items - The items to be transformed.
*
* @returns The transformed items.
*/
_transformItems(items) {
return this._transformers.reduce((items, transform) => {
return transform(items);
}, items);
}
/**
* Handle an add event.
*
* @param _name - Ignored.
* @param payload - The payload containing the ids of the added items.
*/
_add(_name, payload) {
if (payload == null) {
return;
}
this._target.add(this._transformItems(this._source.get(payload.items)));
}
/**
* Handle an update event.
*
* @param _name - Ignored.
* @param payload - The payload containing the ids of the updated items.
*/
_update(_name, payload) {
if (payload == null) {
return;
}
this._target.update(this._transformItems(this._source.get(payload.items)));
}
/**
* Handle a remove event.
*
* @param _name - Ignored.
* @param payload - The payload containing the data of the removed items.
*/
_remove(_name, payload) {
if (payload == null) {
return;
}
this._target.remove(this._transformItems(payload.oldData));
}
}
/**
* Internal implementation of the pipe factory. This should be accessible
* only through `createNewDataPipeFrom` from the outside.
*
* @typeparam TI - Target item type.
* @typeparam TP - Target item type's id property name.
*/
class DataPipeUnderConstruction {
/**
* Create a new data pipe factory. This is an internal constructor that
* should never be called from outside of this file.
*
* @param _source - The source data set or data view for this pipe.
*/
constructor(_source) {
this._source = _source;
/**
* Array transformers used to transform items within the pipe. This is typed
* as any for the sake of simplicity.
*/
this._transformers = [];
}
/**
* Filter the items.
*
* @param callback - A filtering function that returns true if given item
* should be piped and false if not.
*
* @returns This factory for further configuration.
*/
filter(callback) {
this._transformers.push((input) => input.filter(callback));
return this;
}
/**
* Map each source item to a new type.
*
* @param callback - A mapping function that takes a source item and returns
* corresponding mapped item.
*
* @typeparam TI - Target item type.
* @typeparam TP - Target item type's id property name.
*
* @returns This factory for further configuration.
*/
map(callback) {
this._transformers.push((input) => input.map(callback));
return this;
}
/**
* Map each source item to zero or more items of a new type.
*
* @param callback - A mapping function that takes a source item and returns
* an array of corresponding mapped items.
*
* @typeparam TI - Target item type.
* @typeparam TP - Target item type's id property name.
*
* @returns This factory for further configuration.
*/
flatMap(callback) {
this._transformers.push((input) => input.flatMap(callback));
return this;
}
/**
* Connect this pipe to given data set.
*
* @param target - The data set that will receive the items from this pipe.
*
* @returns The pipe connected between given data sets and performing
* configured transformation on the processed items.
*/
to(target) {
return new SimpleDataPipe(this._source, this._transformers, target);
}
}
/**
* Determine whether a value can be used as an id.
*
* @param value - Input value of unknown type.
*
* @returns True if the value is valid id, false otherwise.
*/
function isId(value) {
return typeof value === "string" || typeof value === "number";
}
/* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */
/**
* A queue.
*
* @typeParam T - The type of method names to be replaced by queued versions.
*/
class Queue {
/**
* Construct a new Queue.
*
* @param options - Queue configuration.
*/
constructor(options) {
this._queue = [];
this._timeout = null;
this._extended = null;
// options
this.delay = null;
this.max = Infinity;
this.setOptions(options);
}
/**
* Update the configuration of the queue.
*
* @param options - Queue configuration.
*/
setOptions(options) {
if (options && typeof options.delay !== "undefined") {
this.delay = options.delay;
}
if (options && typeof options.max !== "undefined") {
this.max = options.max;
}
this._flushIfNeeded();
}
/**
* Extend an object with queuing functionality.
* The object will be extended with a function flush, and the methods provided in options.replace will be replaced with queued ones.
*
* @param object - The object to be extended.
* @param options - Additional options.
*
* @returns The created queue.
*/
static extend(object, options) {
const queue = new Queue(options);
if (object.flush !== undefined) {
throw new Error("Target object already has a property flush");
}
object.flush = () => {
queue.flush();
};
const methods = [
{
name: "flush",
original: undefined,
},
];
if (options && options.replace) {
for (let i = 0; i < options.replace.length; i++) {
const name = options.replace[i];
methods.push({
name: name,
// @TODO: better solution?
original: object[name],
});
// @TODO: better solution?
queue.replace(object, name);
}
}
queue._extended = {
object: object,
methods: methods,
};
return queue;
}
/**
* Destroy the queue. The queue will first flush all queued actions, and in case it has extended an object, will restore the original object.
*/
destroy() {
this.flush();
if (this._extended) {
const object = this._extended.object;
const methods = this._extended.methods;
for (let i = 0; i < methods.length; i++) {
const method = methods[i];
if (method.original) {
// @TODO: better solution?
object[method.name] = method.original;
}
else {
// @TODO: better solution?
delete object[method.name];
}
}
this._extended = null;
}
}
/**
* Replace a method on an object with a queued version.
*
* @param object - Object having the method.
* @param method - The method name.
*/
replace(object, method) {
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
const me = this;
const original = object[method];
if (!original) {
throw new Error("Method " + method + " undefined");
}
object[method] = function (...args) {
// add this call to the queue
me.queue({
args: args,
fn: original,
context: this,
});
};
}
/**
* Queue a call.
*
* @param entry - The function or entry to be queued.
*/
queue(entry) {
if (typeof entry === "function") {
this._queue.push({ fn: entry });
}
else {
this._queue.push(entry);
}
this._flushIfNeeded();
}
/**
* Check whether the queue needs to be flushed.
*/
_flushIfNeeded() {
// flush when the maximum is exceeded.
if (this._queue.length > this.max) {
this.flush();
}
// flush after a period of inactivity when a delay is configured
if (this._timeout != null) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (this.queue.length > 0 && typeof this.delay === "number") {
this._timeout = setTimeout(() => {
this.flush();
}, this.delay);
}
}
/**
* Flush all queued calls
*/
flush() {
this._queue.splice(0).forEach((entry) => {
entry.fn.apply(entry.context || entry.fn, entry.args || []);
});
}
}
/* eslint-disable @typescript-eslint/member-ordering */
/**
* [[DataSet]] code that can be reused in [[DataView]] or other similar implementations of [[DataInterface]].
*
* @typeParam Item - Item type that may or may not have an id.
* @typeParam IdProp - Name of the property that contains the id.
*/
class DataSetPart {
constructor() {
this._subscribers = {
"*": [],
add: [],
remove: [],
update: [],
};
/**
* @deprecated Use on instead (PS: DataView.subscribe === DataView.on).
*/
this.subscribe = DataSetPart.prototype.on;
/**
* @deprecated Use off instead (PS: DataView.unsubscribe === DataView.off).
*/
this.unsubscribe = DataSetPart.prototype.off;
}
/**
* Trigger an event
*
* @param event - Event name.
* @param payload - Event payload.
* @param senderId - Id of the sender.
*/
_trigger(event, payload, senderId) {
if (event === "*") {
throw new Error("Cannot trigger event *");
}
[...this._subscribers[event], ...this._subscribers["*"]].forEach((subscriber) => {
subscriber(event, payload, senderId != null ? senderId : null);
});
}
/**
* Subscribe to an event, add an event listener.
*
* @remarks Non-function callbacks are ignored.
*
* @param event - Event name.
* @param callback - Callback method.
*/
on(event, callback) {
if (typeof callback === "function") {
this._subscribers[event].push(callback);
}
// @TODO: Maybe throw for invalid callbacks?
}
/**
* Unsubscribe from an event, remove an event listener.
*
* @remarks If the same callback was subscribed more than once **all** occurences will be removed.
*
* @param event - Event name.
* @param callback - Callback method.
*/
off(event, callback) {
this._subscribers[event] = this._subscribers[event].filter((subscriber) => subscriber !== callback);
}
}
/**
* Data stream
*
* @remarks
* [[DataStream]] offers an always up to date stream of items from a [[DataSet]] or [[DataView]].
* That means that the stream is evaluated at the time of iteration, conversion to another data type or when [[cache]] is called, not when the [[DataStream]] was created.
* Multiple invocations of for example [[toItemArray]] may yield different results (if the data source like for example [[DataSet]] gets modified).
*
* @typeparam Item - The item type this stream is going to work with.
*/
class DataStream {
/**
* Create a new data stream.
*
* @param _pairs - The id, item pairs.
*/
constructor(_pairs) {
this._pairs = _pairs;
}
/**
* Return an iterable of key, value pairs for every entry in the stream.
*/
*[Symbol.iterator]() {
for (const [id, item] of this._pairs) {
yield [id, item];
}
}
/**
* Return an iterable of key, value pairs for every entry in the stream.
*/
*entries() {
for (const [id, item] of this._pairs) {
yield [id, item];
}
}
/**
* Return an iterable of keys in the stream.
*/
*keys() {
for (const [id] of this._pairs) {
yield id;
}
}
/**
* Return an iterable of values in the stream.
*/
*values() {
for (const [, item] of this._pairs) {
yield item;
}
}
/**
* Return an array containing all the ids in this stream.
*
* @remarks
* The array may contain duplicities.
*
* @returns The array with all ids from this stream.
*/
toIdArray() {
return [...this._pairs].map((pair) => pair[0]);
}
/**
* Return an array containing all the items in this stream.
*
* @remarks
* The array may contain duplicities.
*
* @returns The array with all items from this stream.
*/
toItemArray() {
return [...this._pairs].map((pair) => pair[1]);
}
/**
* Return an array containing all the entries in this stream.
*
* @remarks
* The array may contain duplicities.
*
* @returns The array with all entries from this stream.
*/
toEntryArray() {
return [...this._pairs];
}
/**
* Return an object map containing all the items in this stream accessible by ids.
*
* @remarks
* In case of duplicate ids (coerced to string so `7 == '7'`) the last encoutered appears in the returned object.
*
* @returns The object map of all id → item pairs from this stream.
*/
toObjectMap() {
const map = Object.create(null);
for (const [id, item] of this._pairs) {
map[id] = item;
}
return map;
}
/**
* Return a map containing all the items in this stream accessible by ids.
*
* @returns The map of all id → item pairs from this stream.
*/
toMap() {
return new Map(this._pairs);
}
/**
* Return a set containing all the (unique) ids in this stream.
*
* @returns The set of all ids from this stream.
*/
toIdSet() {
return new Set(this.toIdArray());
}
/**
* Return a set containing all the (unique) items in this stream.
*
* @returns The set of all items from this stream.
*/
toItemSet() {
return new Set(this.toItemArray());
}
/**
* Cache the items from this stream.
*
* @remarks
* This method allows for items to be fetched immediatelly and used (possibly multiple times) later.
* It can also be used to optimize performance as [[DataStream]] would otherwise reevaluate everything upon each iteration.
*
* ## Example
* ```javascript
* const ds = new DataSet([…])
*
* const cachedStream = ds.stream()
* .filter(…)
* .sort(…)
* .map(…)
* .cached(…) // Data are fetched, processed and cached here.
*
* ds.clear()
* chachedStream // Still has all the items.
* ```
*
* @returns A new [[DataStream]] with cached items (detached from the original [[DataSet]]).
*/
cache() {
return new DataStream([...this._pairs]);
}
/**
* Get the distinct values of given property.
*
* @param callback - The function that picks and possibly converts the property.
*
* @typeparam T - The type of the distinct value.
*
* @returns A set of all distinct properties.
*/
distinct(callback) {
const set = new Set();
for (const [id, item] of this._pairs) {
set.add(callback(item, id));
}
return set;
}
/**
* Filter the items of the stream.
*
* @param callback - The function that decides whether an item will be included.
*
* @returns A new data stream with the filtered items.
*/
filter(callback) {
const pairs = this._pairs;
return new DataStream({
*[Symbol.iterator]() {
for (const [id, item] of pairs) {
if (callback(item, id)) {
yield [id, item];
}
}
},
});
}
/**
* Execute a callback for each item of the stream.
*
* @param callback - The function that will be invoked for each item.
*/
forEach(callback) {
for (const [id, item] of this._pairs) {
callback(item, id);
}
}
/**
* Map the items into a different type.
*
* @param callback - The function that does the conversion.
*
* @typeparam Mapped - The type of the item after mapping.
*
* @returns A new data stream with the mapped items.
*/
map(callback) {
const pairs = this._pairs;
return new DataStream({
*[Symbol.iterator]() {
for (const [id, item] of pairs) {
yield [id, callback(item, id)];
}
},
});
}
/**
* Get the item with the maximum value of given property.
*
* @param callback - The function that picks and possibly converts the property.
*
* @returns The item with the maximum if found otherwise null.
*/
max(callback) {
const iter = this._pairs[Symbol.iterator]();
let curr = iter.next();
if (curr.done) {
return null;
}
let maxItem = curr.value[1];
let maxValue = callback(curr.value[1], curr.value[0]);
while (!(curr = iter.next()).done) {
const [id, item] = curr.value;
const value = callback(item, id);
if (value > maxValue) {
maxValue = value;
maxItem = item;
}
}
return maxItem;
}
/**
* Get the item with the minimum value of given property.
*
* @param callback - The function that picks and possibly converts the property.
*
* @returns The item with the minimum if found otherwise null.
*/
min(callback) {
const iter = this._pairs[Symbol.iterator]();
let curr = iter.next();
if (curr.done) {
return null;
}
let minItem = curr.value[1];
let minValue = callback(curr.value[1], curr.value[0]);
while (!(curr = iter.next()).done) {
const [id, item] = curr.value;
const value = callback(item, id);
if (value < minValue) {
minValue = value;
minItem = item;
}
}
return minItem;
}
/**
* Reduce the items into a single value.
*
* @param callback - The function that does the reduction.
* @param accumulator - The initial value of the accumulator.
*
* @typeparam T - The type of the accumulated value.
*
* @returns The reduced value.
*/
reduce(callback, accumulator) {
for (const [id, item] of this._pairs) {
accumulator = callback(accumulator, item, id);
}
return accumulator;
}
/**
* Sort the items.
*
* @param callback - Item comparator.
*
* @returns A new stream with sorted items.
*/
sort(callback) {
return new DataStream({
[Symbol.iterator]: () => [...this._pairs]
.sort(([idA, itemA], [idB, itemB]) => callback(itemA, itemB, idA, idB))[Symbol.iterator](),
});
}
}
/* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */
/**
* Add an id to given item if it doesn't have one already.
*
* @remarks
* The item will be modified.
*
* @param item - The item that will have an id after a call to this function.
* @param idProp - The key of the id property.
*
* @typeParam Item - Item type that may or may not have an id.
* @typeParam IdProp - Name of the property that contains the id.
*
* @returns true
*/
function ensureFullItem(item, idProp) {
if (item[idProp] == null) {
// generate an id
item[idProp] = uuid.v4();
}
return item;
}
/**
* # DataSet
*
* Vis.js comes with a flexible DataSet, which can be used to hold and
* manipulate unstructured data and listen for changes in the data. The DataSet
* is key/value based. Data items can be added, updated and removed from the
* DataSet, and one can subscribe to changes in the DataSet. The data in the
* DataSet can be filtered and ordered. Data can be normalized when appending it
* to the DataSet as well.
*
* ## Example
*
* The following example shows how to use a DataSet.
*
* ```javascript
* // create a DataSet
* var options = {};
* var data = new vis.DataSet(options);
*
* // add items
* // note that the data items can contain different properties and data formats
* data.add([
* {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
* {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
* {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
* {id: 4, text: 'item 4'}
* ]);
*
* // subscribe to any change in the DataSet
* data.on('*', function (event, properties, senderId) {
* console.log('event', event, properties);
* });
*
* // update an existing item
* data.update({id: 2, group: 1});
*
* // remove an item
* data.remove(4);
*
* // get all ids
* var ids = data.getIds();
* console.log('ids', ids);
*
* // get a specific item
* var item1 = data.get(1);
* console.log('item1', item1);
*
* // retrieve a filtered subset of the data
* var items = data.get({
* filter: function (item) {
* return item.group == 1;
* }
* });
* console.log('filtered items', items);
* ```
*
* @typeParam Item - Item type that may or may not have an id.
* @typeParam IdProp - Name of the property that contains the id.
*/
class DataSet extends DataSetPart {
/**
* Construct a new DataSet.
*
* @param data - Initial data or options.
* @param options - Options (type error if data is also options).
*/
constructor(data, options) {
super();
// correctly read optional arguments
if (data && !Array.isArray(data)) {
options = data;
data = [];
}
this._options = options || {};
this._data = new Map(); // map with data indexed by id
this.length = 0; // number of items in the DataSet
this._idProp = this._options.fieldId || "id"; // name of the field containing id
// add initial data when provided
if (data && data.length) {
this.add(data);
}
this.setOptions(options);
}
/**
* Set new options.
*
* @param options - The new options.
*/
setOptions(options) {
if (options && options.queue !== undefined) {
if (options.queue === false) {
// delete queue if loaded
if (this._queue) {
this._queue.destroy();
delete this._queue;
}
}
else {
// create queue and update its options
if (!this._queue) {
this._queue = Queue.extend(this, {
replace: ["add", "update", "remove"],
});
}
if (options.queue && typeof options.queue === "object") {
this._queue.setOptions(options.queue);
}
}
}
}
/**
* Add a data item or an array with items.
*
* After the items are added to the DataSet, the DataSet will trigger an event `add`. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
*
* ## Example
*
* ```javascript
* // create a DataSet
* const data = new vis.DataSet()
*
* // add items
* const ids = data.add([
* { id: 1, text: 'item 1' },
* { id: 2, text: 'item 2' },
* { text: 'item without an id' }
* ])
*
* console.log(ids) // [1, 2, '<UUIDv4>']
* ```
*
* @param data - Items to be added (ids will be generated if missing).
* @param senderId - Sender id.
*
* @returns addedIds - Array with the ids (generated if not present) of the added items.
*
* @throws When an item with the same id as any of the added items already exists.
*/
add(data, senderId) {
const addedIds = [];
let id;
if (Array.isArray(data)) {
// Array
const idsToAdd = data.map((d) => d[this._idProp]);
if (idsToAdd.some((id) => this._data.has(id))) {
throw new Error("A duplicate id was found in the parameter array.");
}
for (let i = 0, len = data.length; i < len; i++) {
id = this._addItem(data[i]);
addedIds.push(id);
}
}
else if (data && typeof data === "object") {
// Single item
id = this._addItem(data);
addedIds.push(id);
}
else {
throw new Error("Unknown dataType");
}
if (addedIds.length) {
this._trigger("add", { items: addedIds }, senderId);
}
return addedIds;
}
/**
* Update existing items. When an item does not exist, it will be created.
*
* @remarks
* The provided properties will be merged in the existing item. When an item does not exist, it will be created.
*
* After the items are updated, the DataSet will trigger an event `add` for the added items, and an event `update`. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
*
* ## Example
*
* ```javascript
* // create a DataSet
* const data = new vis.DataSet([
* { id: 1, text: 'item 1' },
* { id: 2, text: 'item 2' },
* { id: 3, text: 'item 3' }
* ])
*
* // update items
* const ids = data.update([
* { id: 2, text: 'item 2 (updated)' },
* { id: 4, text: 'item 4 (new)' }
* ])
*
* console.log(ids) // [2, 4]
* ```
*
* ## Warning for TypeScript users
* This method may introduce partial items into the data set. Use add or updateOnly instead for better type safety.
*
* @param data - Items to be updated (if the id is already present) or added (if the id is missing).
* @param senderId - Sender id.
*
* @returns updatedIds - The ids of the added (these may be newly generated if there was no id in the item from the data) or updated items.
*
* @throws When the supplied data is neither an item nor an array of items.
*/
update(data, senderId) {
const addedIds = [];
const updatedIds = [];
const oldData = [];
const updatedData = [];
const idProp = this._idProp;
const addOrUpdate = (item) => {
const origId = item[idProp];
if (origId != null && this._data.has(origId)) {
const fullItem = item; // it has an id, therefore it is a fullitem
const oldItem = Object.assign({}, this._data.get(origId));
// update item
const id = this._updateItem(fullItem);
updatedIds.push(id);
updatedData.push(fullItem);
oldData.push(oldItem);
}
else {
// add new item
const id = this._addItem(item);
addedIds.push(id);
}
};
if (Array.isArray(data)) {
// Array
for (let i = 0, len = data.length; i < len; i++) {
if (data[i] && typeof data[i] === "object") {
addOrUpdate(data[i]);
}
else {
console.warn("Ignoring input item, which is not an object at index " + i);
}
}
}
else if (data && typeof data === "object") {
// Single item
addOrUpdate(data);
}
else {
throw new Error("Unknown dataType");
}
if (addedIds.length) {
this._trigger("add", { items: addedIds }, senderId);
}
if (updatedIds.length) {
const props = { items: updatedIds, oldData: oldData, data: updatedData };
// TODO: remove deprecated property 'data' some day
//Object.defineProperty(props, 'data', {
// 'get': (function() {
// console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data');
// return updatedData;
// }).bind(this)
//});
this._trigger("update", props, senderId);
}
return addedIds.concat(updatedIds);
}
/**
* Update existing items. When an item does not exist, an error will be thrown.
*
* @remarks
* The provided properties will be deeply merged into the existing item.
* When an item does not exist (id not present in the data set or absent), an error will be thrown and nothing will be changed.
*
* After the items are updated, the DataSet will trigger an event `update`.
* When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
*
* ## Example
*
* ```javascript
* // create a DataSet
* const data = new vis.DataSet([
* { id: 1, text: 'item 1' },
* { id: 2, text: 'item 2' },
* { id: 3, text: 'item 3' },
* ])
*
* // update items
* const ids = data.update([
* { id: 2, text: 'item 2 (updated)' }, // works
* // { id: 4, text: 'item 4 (new)' }, // would throw
* // { text: 'item 4 (new)' }, // would also throw
* ])
*
* console.log(ids) // [2]
* ```
*
* @param data - Updates (the id and optionally other props) to the items in this data set.
* @param senderId - Sender id.
*
* @returns updatedIds - The ids of the updated items.
*
* @throws When the supplied data is neither an item nor an array of items, when the ids are missing.
*/
updateOnly(data, senderId) {
if (!Array.isArray(data)) {
data = [data];
}
const updateEventData = data
.map((update) => {
const oldData = this._data.get(update[this._idProp]);
if (oldData == null) {
throw new Error("Updating non-existent items is not allowed.");
}
return { oldData, update };
})
.map(({ oldData, update }) => {
const id = oldData[this._idProp];
const updatedData = esnext.pureDeepObjectAssign(oldData, update);
this._data.set(id, updatedData);
return {
id,
oldData: oldData,
updatedData,
};
});
if (updateEventData.length) {
const props = {
items: updateEventData.map((value) => value.id),
oldData: updateEventData.map((value) => value.oldData),
data: updateEventData.map((value) => value.updatedData),
};
// TODO: remove deprecated property 'data' some day
//Object.defineProperty(props, 'data', {
// 'get': (function() {
// console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data');
// return updatedData;
// }).bind(this)
//});
this._trigger("update", props, senderId);
return props.items;
}
else {
return [];
}
}
/** @inheritdoc */
get(first, second) {
// @TODO: Woudn't it be better to split this into multiple methods?
// parse the arguments
let id = undefined;
let ids = undefined;
let options = undefined;
if (isId(first)) {
// get(id [, options])
id = first;
options = second;
}
else if (Array.isArray(first)) {
// get(ids [, options])
ids = first;
options = second;
}
else {
// get([, options])
options = first;
}
// determine the return type
const returnType = options && options.returnType === "Object" ? "Object" : "Array";
// @TODO: WTF is this? Or am I missing something?
// var returnType
// if (options && options.returnType) {
// var allowedValues = ['Array', 'Object']
// returnType =
// allowedValues.indexOf(options.returnType) == -1
// ? 'Array'
// : options.returnType
// } else {
// returnType = 'Array'
// }
// build options
const filter = options && options.filter;
const items = [];
let item = undefined;
let itemIds = undefined;
let itemId = undefined;
// convert items
if (id != null) {
// return a single item
item = this._data.get(id);
if (item && filter && !filter(item)) {
item = undefined;
}
}
else if (ids != null) {
// return a subset of items
for (let i = 0, len = ids.length; i < len; i++) {
item = this._data.get(ids[i]);
if (item != null && (!filter || filter(item))) {
items.push(item);
}
}
}
else {
// return all items
itemIds = [...this._data.keys()];
for (let i = 0, len = itemIds.length; i < len; i++) {
itemId = itemIds[i];
item = this._data.get(itemId);
if (item != null && (!filter || filter(item))) {
items.push(item);
}
}
}
// order the results
if (options && options.order && id == undefined) {
this._sort(items, options.order);
}
// filter fields of the items
if (options && options.fields) {
const fields = options.fields;
if (id != undefined && item != null) {
item = this._filterFields(item, fields);
}
else {
for (let i = 0, len = items.length; i < len; i++) {
items[i] = this._filterFields(items[i], fields);
}
}
}
// return the results
if (returnType == "Object") {
const result = {};
for (let i = 0, len = items.length; i < len; i++) {
const resultant = items[i];
// @TODO: Shoudn't this be this._fieldId?
// result[resultant.id] = resultant
const id = resultant[this._idProp];
result[id] = resultant;
}
return result;
}
else {
if (id != null) {
// a single item
return item !== null && item !== void 0 ? item : null;
}
else {
// just return our array
return items;
}
}
}
/** @inheritdoc */
getIds(options) {
const data = this._data;
const filter = options && options.filter;
const order = options && options.order;
const itemIds = [...data.keys()];
const ids = [];
if (filter) {
// get filtered items
if (order) {
// create ordered list
const items = [];
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
const item = this._data.get(id);
if (item != null && filter(item)) {
items.push(item);
}
}
this._sort(items, order);
for (let i = 0, len = items.length; i < len; i++) {
ids.push(items[i][this._idProp]);
}
}
else {
// create unordered list
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
const item = this._data.get(id);
if (item != null && filter(item)) {
ids.push(item[this._idProp]);
}
}
}
}
else {
// get all items
if (order) {
// create an ordered list
const items = [];
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
items.push(data.get(id));
}
this._sort(items, order);
for (let i = 0, len = items.length; i < len; i++) {
ids.push(items[i][this._idProp]);
}
}
else {
// create unordered list
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
const item = data.get(id);
if (item != null) {
ids.push(item[this._idProp]);
}
}
}
}
return ids;
}
/** @inheritdoc */
getDataSet() {
return this;
}
/** @inheritdoc */
forEach(callback, options) {
const filter = options && options.filter;
const data = this._data;
const itemIds = [...data.keys()];
if (options && options.order) {
// execute forEach on ordered list
const items = this.get(options);
for (let i = 0, len = items.length; i < len; i++) {
const item = items[i];
const id = item[this._idProp];
callback(item, id);
}
}
else {
// unordered
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
const item = this._data.get(id);
if (item != null && (!filter || filter(item))) {
callback(item, id);
}
}
}
}
/** @inheritdoc */
map(callback, options) {
const filter = options && options.filter;
const mappedItems = [];
const data = this._data;
const itemIds = [...data.keys()];
// convert and filter items
for (let i = 0, len = itemIds.length; i < len; i++) {
const id = itemIds[i];
const item = this._data.get(id);
if (item != null && (!filter || filter(item))) {
mappedItems.push(callback(item, id));
}
}
// order items
if (options && options.order) {
this._sort(mappedItems, options.order);
}
return mappedItems;
}
/**
* Filter the fields of an item.
*
* @param item - The item whose fields should be filtered.
* @param fields - The names of the fields that will be kept.
*
* @typeParam K - Field name type.
*
* @returns The item without any additional fields.
*/
_filterFields(item, fields) {
if (!item) {
// item is null
return item;
}
return (Array.isArray(fields)
? // Use the supplied array
fields
: // Use the keys of the supplied object
Object.keys(fields)).reduce((filteredItem, field) => {
filteredItem[field] = item[field];
return filteredItem;
}, {});
}
/**
* Sort the provided array with items.
*
* @param items - Items to be sorted in place.
* @param order - A field name or custom sort function.
*
* @typeParam T - The type of the items in the items array.
*/
_sort(items, order) {
if (typeof order === "string") {
// order by provided field name
const name = order; // field name
items.sort((a, b) => {
// @TODO: How to treat missing properties?
const av = a[name];
const bv = b[name];
return av > bv ? 1 : av < bv ? -1 : 0;
});
}
else if (typeof order === "function") {
// order by sort function
items.sort(order);
}
else {
// TODO: extend order by an Object {field:string, direction:string}
// where direction can be 'asc' or 'desc'
throw new TypeError("Order must be a function or a string");
}
}
/**
* Remove an item or multiple items by “reference” (only the id is used) or by id.
*
* The method ignores removal of non-existing items, and returns an array containing the ids of the items which are actually removed from the DataSet.
*
* After the items are removed, the DataSet will trigger an event `remove` for the removed items. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
*
* ## Example
* ```javascript
* // create a DataSet
* const data = new vis.DataSet([
* { id: 1, text: 'item 1' },
* { id: 2, text: 'item 2' },
* { id: 3, text: 'item 3' }
* ])
*
* // remove items
* const ids =