UNPKG

74.4 kBJavaScriptView Raw
1/**
2 * vis-data
3 * http://visjs.org/
4 *
5 * Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data.
6 *
7 * @version 7.1.2
8 * @date 2021-01-08T20:37:11.601Z
9 *
10 * @copyright (c) 2011-2017 Almende B.V, http://almende.com
11 * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
12 *
13 * @license
14 * vis.js is dual licensed under both
15 *
16 * 1. The Apache 2.0 License
17 * http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * and
20 *
21 * 2. The MIT License
22 * http://opensource.org/licenses/MIT
23 *
24 * vis.js may be distributed under either license.
25 */
26
27(function (global, factory) {
28 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vis-util/esnext/umd/vis-util.js'), require('uuid')) :
29 typeof define === 'function' && define.amd ? define(['exports', 'vis-util/esnext/umd/vis-util.js', 'uuid'], factory) :
30 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vis = global.vis || {}, global.vis, global.uuidv4));
31}(this, (function (exports, esnext, uuid) {
32 /**
33 * Create new data pipe.
34 *
35 * @param from - The source data set or data view.
36 *
37 * @remarks
38 * Example usage:
39 * ```typescript
40 * interface AppItem {
41 * whoami: string;
42 * appData: unknown;
43 * visData: VisItem;
44 * }
45 * interface VisItem {
46 * id: number;
47 * label: string;
48 * color: string;
49 * x: number;
50 * y: number;
51 * }
52 *
53 * const ds1 = new DataSet<AppItem, "whoami">([], { fieldId: "whoami" });
54 * const ds2 = new DataSet<VisItem, "id">();
55 *
56 * const pipe = createNewDataPipeFrom(ds1)
57 * .filter((item): boolean => item.enabled === true)
58 * .map<VisItem, "id">((item): VisItem => item.visData)
59 * .to(ds2);
60 *
61 * pipe.start();
62 * ```
63 *
64 * @returns A factory whose methods can be used to configure the pipe.
65 */
66 function createNewDataPipeFrom(from) {
67 return new DataPipeUnderConstruction(from);
68 }
69 /**
70 * Internal implementation of the pipe. This should be accessible only through
71 * `createNewDataPipeFrom` from the outside.
72 *
73 * @typeParam SI - Source item type.
74 * @typeParam SP - Source item type's id property name.
75 * @typeParam TI - Target item type.
76 * @typeParam TP - Target item type's id property name.
77 */
78 class SimpleDataPipe {
79 /**
80 * Create a new data pipe.
81 *
82 * @param _source - The data set or data view that will be observed.
83 * @param _transformers - An array of transforming functions to be used to
84 * filter or transform the items in the pipe.
85 * @param _target - The data set or data view that will receive the items.
86 */
87 constructor(_source, _transformers, _target) {
88 this._source = _source;
89 this._transformers = _transformers;
90 this._target = _target;
91 /**
92 * Bound listeners for use with `DataInterface['on' | 'off']`.
93 */
94 this._listeners = {
95 add: this._add.bind(this),
96 remove: this._remove.bind(this),
97 update: this._update.bind(this),
98 };
99 }
100 /** @inheritDoc */
101 all() {
102 this._target.update(this._transformItems(this._source.get()));
103 return this;
104 }
105 /** @inheritDoc */
106 start() {
107 this._source.on("add", this._listeners.add);
108 this._source.on("remove", this._listeners.remove);
109 this._source.on("update", this._listeners.update);
110 return this;
111 }
112 /** @inheritDoc */
113 stop() {
114 this._source.off("add", this._listeners.add);
115 this._source.off("remove", this._listeners.remove);
116 this._source.off("update", this._listeners.update);
117 return this;
118 }
119 /**
120 * Apply the transformers to the items.
121 *
122 * @param items - The items to be transformed.
123 *
124 * @returns The transformed items.
125 */
126 _transformItems(items) {
127 return this._transformers.reduce((items, transform) => {
128 return transform(items);
129 }, items);
130 }
131 /**
132 * Handle an add event.
133 *
134 * @param _name - Ignored.
135 * @param payload - The payload containing the ids of the added items.
136 */
137 _add(_name, payload) {
138 if (payload == null) {
139 return;
140 }
141 this._target.add(this._transformItems(this._source.get(payload.items)));
142 }
143 /**
144 * Handle an update event.
145 *
146 * @param _name - Ignored.
147 * @param payload - The payload containing the ids of the updated items.
148 */
149 _update(_name, payload) {
150 if (payload == null) {
151 return;
152 }
153 this._target.update(this._transformItems(this._source.get(payload.items)));
154 }
155 /**
156 * Handle a remove event.
157 *
158 * @param _name - Ignored.
159 * @param payload - The payload containing the data of the removed items.
160 */
161 _remove(_name, payload) {
162 if (payload == null) {
163 return;
164 }
165 this._target.remove(this._transformItems(payload.oldData));
166 }
167 }
168 /**
169 * Internal implementation of the pipe factory. This should be accessible
170 * only through `createNewDataPipeFrom` from the outside.
171 *
172 * @typeParam TI - Target item type.
173 * @typeParam TP - Target item type's id property name.
174 */
175 class DataPipeUnderConstruction {
176 /**
177 * Create a new data pipe factory. This is an internal constructor that
178 * should never be called from outside of this file.
179 *
180 * @param _source - The source data set or data view for this pipe.
181 */
182 constructor(_source) {
183 this._source = _source;
184 /**
185 * Array transformers used to transform items within the pipe. This is typed
186 * as any for the sake of simplicity.
187 */
188 this._transformers = [];
189 }
190 /**
191 * Filter the items.
192 *
193 * @param callback - A filtering function that returns true if given item
194 * should be piped and false if not.
195 *
196 * @returns This factory for further configuration.
197 */
198 filter(callback) {
199 this._transformers.push((input) => input.filter(callback));
200 return this;
201 }
202 /**
203 * Map each source item to a new type.
204 *
205 * @param callback - A mapping function that takes a source item and returns
206 * corresponding mapped item.
207 *
208 * @typeParam TI - Target item type.
209 * @typeParam TP - Target item type's id property name.
210 *
211 * @returns This factory for further configuration.
212 */
213 map(callback) {
214 this._transformers.push((input) => input.map(callback));
215 return this;
216 }
217 /**
218 * Map each source item to zero or more items of a new type.
219 *
220 * @param callback - A mapping function that takes a source item and returns
221 * an array of corresponding mapped items.
222 *
223 * @typeParam TI - Target item type.
224 * @typeParam TP - Target item type's id property name.
225 *
226 * @returns This factory for further configuration.
227 */
228 flatMap(callback) {
229 this._transformers.push((input) => input.flatMap(callback));
230 return this;
231 }
232 /**
233 * Connect this pipe to given data set.
234 *
235 * @param target - The data set that will receive the items from this pipe.
236 *
237 * @returns The pipe connected between given data sets and performing
238 * configured transformation on the processed items.
239 */
240 to(target) {
241 return new SimpleDataPipe(this._source, this._transformers, target);
242 }
243 }
244
245 /**
246 * Determine whether a value can be used as an id.
247 *
248 * @param value - Input value of unknown type.
249 *
250 * @returns True if the value is valid id, false otherwise.
251 */
252 function isId(value) {
253 return typeof value === "string" || typeof value === "number";
254 }
255
256 /**
257 * A queue.
258 *
259 * @typeParam T - The type of method names to be replaced by queued versions.
260 */
261 class Queue {
262 /**
263 * Construct a new Queue.
264 *
265 * @param options - Queue configuration.
266 */
267 constructor(options) {
268 this._queue = [];
269 this._timeout = null;
270 this._extended = null;
271 // options
272 this.delay = null;
273 this.max = Infinity;
274 this.setOptions(options);
275 }
276 /**
277 * Update the configuration of the queue.
278 *
279 * @param options - Queue configuration.
280 */
281 setOptions(options) {
282 if (options && typeof options.delay !== "undefined") {
283 this.delay = options.delay;
284 }
285 if (options && typeof options.max !== "undefined") {
286 this.max = options.max;
287 }
288 this._flushIfNeeded();
289 }
290 /**
291 * Extend an object with queuing functionality.
292 * The object will be extended with a function flush, and the methods provided in options.replace will be replaced with queued ones.
293 *
294 * @param object - The object to be extended.
295 * @param options - Additional options.
296 *
297 * @returns The created queue.
298 */
299 static extend(object, options) {
300 const queue = new Queue(options);
301 if (object.flush !== undefined) {
302 throw new Error("Target object already has a property flush");
303 }
304 object.flush = () => {
305 queue.flush();
306 };
307 const methods = [
308 {
309 name: "flush",
310 original: undefined,
311 },
312 ];
313 if (options && options.replace) {
314 for (let i = 0; i < options.replace.length; i++) {
315 const name = options.replace[i];
316 methods.push({
317 name: name,
318 // @TODO: better solution?
319 original: object[name],
320 });
321 // @TODO: better solution?
322 queue.replace(object, name);
323 }
324 }
325 queue._extended = {
326 object: object,
327 methods: methods,
328 };
329 return queue;
330 }
331 /**
332 * Destroy the queue. The queue will first flush all queued actions, and in case it has extended an object, will restore the original object.
333 */
334 destroy() {
335 this.flush();
336 if (this._extended) {
337 const object = this._extended.object;
338 const methods = this._extended.methods;
339 for (let i = 0; i < methods.length; i++) {
340 const method = methods[i];
341 if (method.original) {
342 // @TODO: better solution?
343 object[method.name] = method.original;
344 }
345 else {
346 // @TODO: better solution?
347 delete object[method.name];
348 }
349 }
350 this._extended = null;
351 }
352 }
353 /**
354 * Replace a method on an object with a queued version.
355 *
356 * @param object - Object having the method.
357 * @param method - The method name.
358 */
359 replace(object, method) {
360 /* eslint-disable-next-line @typescript-eslint/no-this-alias -- Function this is necessary in the function bellow, so class this has to be saved into a variable here. */
361 const me = this;
362 const original = object[method];
363 if (!original) {
364 throw new Error("Method " + method + " undefined");
365 }
366 object[method] = function (...args) {
367 // add this call to the queue
368 me.queue({
369 args: args,
370 fn: original,
371 context: this,
372 });
373 };
374 }
375 /**
376 * Queue a call.
377 *
378 * @param entry - The function or entry to be queued.
379 */
380 queue(entry) {
381 if (typeof entry === "function") {
382 this._queue.push({ fn: entry });
383 }
384 else {
385 this._queue.push(entry);
386 }
387 this._flushIfNeeded();
388 }
389 /**
390 * Check whether the queue needs to be flushed.
391 */
392 _flushIfNeeded() {
393 // flush when the maximum is exceeded.
394 if (this._queue.length > this.max) {
395 this.flush();
396 }
397 // flush after a period of inactivity when a delay is configured
398 if (this._timeout != null) {
399 clearTimeout(this._timeout);
400 this._timeout = null;
401 }
402 if (this.queue.length > 0 && typeof this.delay === "number") {
403 this._timeout = setTimeout(() => {
404 this.flush();
405 }, this.delay);
406 }
407 }
408 /**
409 * Flush all queued calls
410 */
411 flush() {
412 this._queue.splice(0).forEach((entry) => {
413 entry.fn.apply(entry.context || entry.fn, entry.args || []);
414 });
415 }
416 }
417
418 /**
419 * [[DataSet]] code that can be reused in [[DataView]] or other similar implementations of [[DataInterface]].
420 *
421 * @typeParam Item - Item type that may or may not have an id.
422 * @typeParam IdProp - Name of the property that contains the id.
423 */
424 class DataSetPart {
425 constructor() {
426 this._subscribers = {
427 "*": [],
428 add: [],
429 remove: [],
430 update: [],
431 };
432 /**
433 * @deprecated Use on instead (PS: DataView.subscribe === DataView.on).
434 */
435 this.subscribe = DataSetPart.prototype.on;
436 /**
437 * @deprecated Use off instead (PS: DataView.unsubscribe === DataView.off).
438 */
439 this.unsubscribe = DataSetPart.prototype.off;
440 }
441 /**
442 * Trigger an event
443 *
444 * @param event - Event name.
445 * @param payload - Event payload.
446 * @param senderId - Id of the sender.
447 */
448 _trigger(event, payload, senderId) {
449 if (event === "*") {
450 throw new Error("Cannot trigger event *");
451 }
452 [...this._subscribers[event], ...this._subscribers["*"]].forEach((subscriber) => {
453 subscriber(event, payload, senderId != null ? senderId : null);
454 });
455 }
456 /**
457 * Subscribe to an event, add an event listener.
458 *
459 * @remarks Non-function callbacks are ignored.
460 *
461 * @param event - Event name.
462 * @param callback - Callback method.
463 */
464 on(event, callback) {
465 if (typeof callback === "function") {
466 this._subscribers[event].push(callback);
467 }
468 // @TODO: Maybe throw for invalid callbacks?
469 }
470 /**
471 * Unsubscribe from an event, remove an event listener.
472 *
473 * @remarks If the same callback was subscribed more than once **all** occurences will be removed.
474 *
475 * @param event - Event name.
476 * @param callback - Callback method.
477 */
478 off(event, callback) {
479 this._subscribers[event] = this._subscribers[event].filter((subscriber) => subscriber !== callback);
480 }
481 }
482
483 /**
484 * Data stream
485 *
486 * @remarks
487 * [[DataStream]] offers an always up to date stream of items from a [[DataSet]] or [[DataView]].
488 * 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.
489 * Multiple invocations of for example [[toItemArray]] may yield different results (if the data source like for example [[DataSet]] gets modified).
490 *
491 * @typeParam Item - The item type this stream is going to work with.
492 */
493 class DataStream {
494 /**
495 * Create a new data stream.
496 *
497 * @param pairs - The id, item pairs.
498 */
499 constructor(pairs) {
500 this._pairs = pairs;
501 }
502 /**
503 * Return an iterable of key, value pairs for every entry in the stream.
504 */
505 *[Symbol.iterator]() {
506 for (const [id, item] of this._pairs) {
507 yield [id, item];
508 }
509 }
510 /**
511 * Return an iterable of key, value pairs for every entry in the stream.
512 */
513 *entries() {
514 for (const [id, item] of this._pairs) {
515 yield [id, item];
516 }
517 }
518 /**
519 * Return an iterable of keys in the stream.
520 */
521 *keys() {
522 for (const [id] of this._pairs) {
523 yield id;
524 }
525 }
526 /**
527 * Return an iterable of values in the stream.
528 */
529 *values() {
530 for (const [, item] of this._pairs) {
531 yield item;
532 }
533 }
534 /**
535 * Return an array containing all the ids in this stream.
536 *
537 * @remarks
538 * The array may contain duplicities.
539 *
540 * @returns The array with all ids from this stream.
541 */
542 toIdArray() {
543 return [...this._pairs].map((pair) => pair[0]);
544 }
545 /**
546 * Return an array containing all the items in this stream.
547 *
548 * @remarks
549 * The array may contain duplicities.
550 *
551 * @returns The array with all items from this stream.
552 */
553 toItemArray() {
554 return [...this._pairs].map((pair) => pair[1]);
555 }
556 /**
557 * Return an array containing all the entries in this stream.
558 *
559 * @remarks
560 * The array may contain duplicities.
561 *
562 * @returns The array with all entries from this stream.
563 */
564 toEntryArray() {
565 return [...this._pairs];
566 }
567 /**
568 * Return an object map containing all the items in this stream accessible by ids.
569 *
570 * @remarks
571 * In case of duplicate ids (coerced to string so `7 == '7'`) the last encoutered appears in the returned object.
572 *
573 * @returns The object map of all id → item pairs from this stream.
574 */
575 toObjectMap() {
576 const map = Object.create(null);
577 for (const [id, item] of this._pairs) {
578 map[id] = item;
579 }
580 return map;
581 }
582 /**
583 * Return a map containing all the items in this stream accessible by ids.
584 *
585 * @returns The map of all id → item pairs from this stream.
586 */
587 toMap() {
588 return new Map(this._pairs);
589 }
590 /**
591 * Return a set containing all the (unique) ids in this stream.
592 *
593 * @returns The set of all ids from this stream.
594 */
595 toIdSet() {
596 return new Set(this.toIdArray());
597 }
598 /**
599 * Return a set containing all the (unique) items in this stream.
600 *
601 * @returns The set of all items from this stream.
602 */
603 toItemSet() {
604 return new Set(this.toItemArray());
605 }
606 /**
607 * Cache the items from this stream.
608 *
609 * @remarks
610 * This method allows for items to be fetched immediatelly and used (possibly multiple times) later.
611 * It can also be used to optimize performance as [[DataStream]] would otherwise reevaluate everything upon each iteration.
612 *
613 * ## Example
614 * ```javascript
615 * const ds = new DataSet([…])
616 *
617 * const cachedStream = ds.stream()
618 * .filter(…)
619 * .sort(…)
620 * .map(…)
621 * .cached(…) // Data are fetched, processed and cached here.
622 *
623 * ds.clear()
624 * chachedStream // Still has all the items.
625 * ```
626 *
627 * @returns A new [[DataStream]] with cached items (detached from the original [[DataSet]]).
628 */
629 cache() {
630 return new DataStream([...this._pairs]);
631 }
632 /**
633 * Get the distinct values of given property.
634 *
635 * @param callback - The function that picks and possibly converts the property.
636 *
637 * @typeParam T - The type of the distinct value.
638 *
639 * @returns A set of all distinct properties.
640 */
641 distinct(callback) {
642 const set = new Set();
643 for (const [id, item] of this._pairs) {
644 set.add(callback(item, id));
645 }
646 return set;
647 }
648 /**
649 * Filter the items of the stream.
650 *
651 * @param callback - The function that decides whether an item will be included.
652 *
653 * @returns A new data stream with the filtered items.
654 */
655 filter(callback) {
656 const pairs = this._pairs;
657 return new DataStream({
658 *[Symbol.iterator]() {
659 for (const [id, item] of pairs) {
660 if (callback(item, id)) {
661 yield [id, item];
662 }
663 }
664 },
665 });
666 }
667 /**
668 * Execute a callback for each item of the stream.
669 *
670 * @param callback - The function that will be invoked for each item.
671 */
672 forEach(callback) {
673 for (const [id, item] of this._pairs) {
674 callback(item, id);
675 }
676 }
677 /**
678 * Map the items into a different type.
679 *
680 * @param callback - The function that does the conversion.
681 *
682 * @typeParam Mapped - The type of the item after mapping.
683 *
684 * @returns A new data stream with the mapped items.
685 */
686 map(callback) {
687 const pairs = this._pairs;
688 return new DataStream({
689 *[Symbol.iterator]() {
690 for (const [id, item] of pairs) {
691 yield [id, callback(item, id)];
692 }
693 },
694 });
695 }
696 /**
697 * Get the item with the maximum value of given property.
698 *
699 * @param callback - The function that picks and possibly converts the property.
700 *
701 * @returns The item with the maximum if found otherwise null.
702 */
703 max(callback) {
704 const iter = this._pairs[Symbol.iterator]();
705 let curr = iter.next();
706 if (curr.done) {
707 return null;
708 }
709 let maxItem = curr.value[1];
710 let maxValue = callback(curr.value[1], curr.value[0]);
711 while (!(curr = iter.next()).done) {
712 const [id, item] = curr.value;
713 const value = callback(item, id);
714 if (value > maxValue) {
715 maxValue = value;
716 maxItem = item;
717 }
718 }
719 return maxItem;
720 }
721 /**
722 * Get the item with the minimum value of given property.
723 *
724 * @param callback - The function that picks and possibly converts the property.
725 *
726 * @returns The item with the minimum if found otherwise null.
727 */
728 min(callback) {
729 const iter = this._pairs[Symbol.iterator]();
730 let curr = iter.next();
731 if (curr.done) {
732 return null;
733 }
734 let minItem = curr.value[1];
735 let minValue = callback(curr.value[1], curr.value[0]);
736 while (!(curr = iter.next()).done) {
737 const [id, item] = curr.value;
738 const value = callback(item, id);
739 if (value < minValue) {
740 minValue = value;
741 minItem = item;
742 }
743 }
744 return minItem;
745 }
746 /**
747 * Reduce the items into a single value.
748 *
749 * @param callback - The function that does the reduction.
750 * @param accumulator - The initial value of the accumulator.
751 *
752 * @typeParam T - The type of the accumulated value.
753 *
754 * @returns The reduced value.
755 */
756 reduce(callback, accumulator) {
757 for (const [id, item] of this._pairs) {
758 accumulator = callback(accumulator, item, id);
759 }
760 return accumulator;
761 }
762 /**
763 * Sort the items.
764 *
765 * @param callback - Item comparator.
766 *
767 * @returns A new stream with sorted items.
768 */
769 sort(callback) {
770 return new DataStream({
771 [Symbol.iterator]: () => [...this._pairs]
772 .sort(([idA, itemA], [idB, itemB]) => callback(itemA, itemB, idA, idB))[Symbol.iterator](),
773 });
774 }
775 }
776
777 /**
778 * Add an id to given item if it doesn't have one already.
779 *
780 * @remarks
781 * The item will be modified.
782 *
783 * @param item - The item that will have an id after a call to this function.
784 * @param idProp - The key of the id property.
785 *
786 * @typeParam Item - Item type that may or may not have an id.
787 * @typeParam IdProp - Name of the property that contains the id.
788 *
789 * @returns true
790 */
791 function ensureFullItem(item, idProp) {
792 if (item[idProp] == null) {
793 // generate an id
794 item[idProp] = uuid.v4();
795 }
796 return item;
797 }
798 /**
799 * # DataSet
800 *
801 * Vis.js comes with a flexible DataSet, which can be used to hold and
802 * manipulate unstructured data and listen for changes in the data. The DataSet
803 * is key/value based. Data items can be added, updated and removed from the
804 * DataSet, and one can subscribe to changes in the DataSet. The data in the
805 * DataSet can be filtered and ordered. Data can be normalized when appending it
806 * to the DataSet as well.
807 *
808 * ## Example
809 *
810 * The following example shows how to use a DataSet.
811 *
812 * ```javascript
813 * // create a DataSet
814 * var options = {};
815 * var data = new vis.DataSet(options);
816 *
817 * // add items
818 * // note that the data items can contain different properties and data formats
819 * data.add([
820 * {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
821 * {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
822 * {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
823 * {id: 4, text: 'item 4'}
824 * ]);
825 *
826 * // subscribe to any change in the DataSet
827 * data.on('*', function (event, properties, senderId) {
828 * console.log('event', event, properties);
829 * });
830 *
831 * // update an existing item
832 * data.update({id: 2, group: 1});
833 *
834 * // remove an item
835 * data.remove(4);
836 *
837 * // get all ids
838 * var ids = data.getIds();
839 * console.log('ids', ids);
840 *
841 * // get a specific item
842 * var item1 = data.get(1);
843 * console.log('item1', item1);
844 *
845 * // retrieve a filtered subset of the data
846 * var items = data.get({
847 * filter: function (item) {
848 * return item.group == 1;
849 * }
850 * });
851 * console.log('filtered items', items);
852 * ```
853 *
854 * @typeParam Item - Item type that may or may not have an id.
855 * @typeParam IdProp - Name of the property that contains the id.
856 */
857 class DataSet extends DataSetPart {
858 /**
859 * Construct a new DataSet.
860 *
861 * @param data - Initial data or options.
862 * @param options - Options (type error if data is also options).
863 */
864 constructor(data, options) {
865 super();
866 this._queue = null;
867 // correctly read optional arguments
868 if (data && !Array.isArray(data)) {
869 options = data;
870 data = [];
871 }
872 this._options = options || {};
873 this._data = new Map(); // map with data indexed by id
874 this.length = 0; // number of items in the DataSet
875 this._idProp = this._options.fieldId || "id"; // name of the field containing id
876 // add initial data when provided
877 if (data && data.length) {
878 this.add(data);
879 }
880 this.setOptions(options);
881 }
882 /** @inheritDoc */
883 get idProp() {
884 return this._idProp;
885 }
886 /**
887 * Set new options.
888 *
889 * @param options - The new options.
890 */
891 setOptions(options) {
892 if (options && options.queue !== undefined) {
893 if (options.queue === false) {
894 // delete queue if loaded
895 if (this._queue) {
896 this._queue.destroy();
897 this._queue = null;
898 }
899 }
900 else {
901 // create queue and update its options
902 if (!this._queue) {
903 this._queue = Queue.extend(this, {
904 replace: ["add", "update", "remove"],
905 });
906 }
907 if (options.queue && typeof options.queue === "object") {
908 this._queue.setOptions(options.queue);
909 }
910 }
911 }
912 }
913 /**
914 * Add a data item or an array with items.
915 *
916 * 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.
917 *
918 * ## Example
919 *
920 * ```javascript
921 * // create a DataSet
922 * const data = new vis.DataSet()
923 *
924 * // add items
925 * const ids = data.add([
926 * { id: 1, text: 'item 1' },
927 * { id: 2, text: 'item 2' },
928 * { text: 'item without an id' }
929 * ])
930 *
931 * console.log(ids) // [1, 2, '<UUIDv4>']
932 * ```
933 *
934 * @param data - Items to be added (ids will be generated if missing).
935 * @param senderId - Sender id.
936 *
937 * @returns addedIds - Array with the ids (generated if not present) of the added items.
938 *
939 * @throws When an item with the same id as any of the added items already exists.
940 */
941 add(data, senderId) {
942 const addedIds = [];
943 let id;
944 if (Array.isArray(data)) {
945 // Array
946 const idsToAdd = data.map((d) => d[this._idProp]);
947 if (idsToAdd.some((id) => this._data.has(id))) {
948 throw new Error("A duplicate id was found in the parameter array.");
949 }
950 for (let i = 0, len = data.length; i < len; i++) {
951 id = this._addItem(data[i]);
952 addedIds.push(id);
953 }
954 }
955 else if (data && typeof data === "object") {
956 // Single item
957 id = this._addItem(data);
958 addedIds.push(id);
959 }
960 else {
961 throw new Error("Unknown dataType");
962 }
963 if (addedIds.length) {
964 this._trigger("add", { items: addedIds }, senderId);
965 }
966 return addedIds;
967 }
968 /**
969 * Update existing items. When an item does not exist, it will be created.
970 *
971 * @remarks
972 * The provided properties will be merged in the existing item. When an item does not exist, it will be created.
973 *
974 * 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.
975 *
976 * ## Example
977 *
978 * ```javascript
979 * // create a DataSet
980 * const data = new vis.DataSet([
981 * { id: 1, text: 'item 1' },
982 * { id: 2, text: 'item 2' },
983 * { id: 3, text: 'item 3' }
984 * ])
985 *
986 * // update items
987 * const ids = data.update([
988 * { id: 2, text: 'item 2 (updated)' },
989 * { id: 4, text: 'item 4 (new)' }
990 * ])
991 *
992 * console.log(ids) // [2, 4]
993 * ```
994 *
995 * ## Warning for TypeScript users
996 * This method may introduce partial items into the data set. Use add or updateOnly instead for better type safety.
997 *
998 * @param data - Items to be updated (if the id is already present) or added (if the id is missing).
999 * @param senderId - Sender id.
1000 *
1001 * @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.
1002 *
1003 * @throws When the supplied data is neither an item nor an array of items.
1004 */
1005 update(data, senderId) {
1006 const addedIds = [];
1007 const updatedIds = [];
1008 const oldData = [];
1009 const updatedData = [];
1010 const idProp = this._idProp;
1011 const addOrUpdate = (item) => {
1012 const origId = item[idProp];
1013 if (origId != null && this._data.has(origId)) {
1014 const fullItem = item; // it has an id, therefore it is a fullitem
1015 const oldItem = Object.assign({}, this._data.get(origId));
1016 // update item
1017 const id = this._updateItem(fullItem);
1018 updatedIds.push(id);
1019 updatedData.push(fullItem);
1020 oldData.push(oldItem);
1021 }
1022 else {
1023 // add new item
1024 const id = this._addItem(item);
1025 addedIds.push(id);
1026 }
1027 };
1028 if (Array.isArray(data)) {
1029 // Array
1030 for (let i = 0, len = data.length; i < len; i++) {
1031 if (data[i] && typeof data[i] === "object") {
1032 addOrUpdate(data[i]);
1033 }
1034 else {
1035 console.warn("Ignoring input item, which is not an object at index " + i);
1036 }
1037 }
1038 }
1039 else if (data && typeof data === "object") {
1040 // Single item
1041 addOrUpdate(data);
1042 }
1043 else {
1044 throw new Error("Unknown dataType");
1045 }
1046 if (addedIds.length) {
1047 this._trigger("add", { items: addedIds }, senderId);
1048 }
1049 if (updatedIds.length) {
1050 const props = { items: updatedIds, oldData: oldData, data: updatedData };
1051 // TODO: remove deprecated property 'data' some day
1052 //Object.defineProperty(props, 'data', {
1053 // 'get': (function() {
1054 // 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');
1055 // return updatedData;
1056 // }).bind(this)
1057 //});
1058 this._trigger("update", props, senderId);
1059 }
1060 return addedIds.concat(updatedIds);
1061 }
1062 /**
1063 * Update existing items. When an item does not exist, an error will be thrown.
1064 *
1065 * @remarks
1066 * The provided properties will be deeply merged into the existing item.
1067 * 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.
1068 *
1069 * After the items are updated, the DataSet will trigger an event `update`.
1070 * When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
1071 *
1072 * ## Example
1073 *
1074 * ```javascript
1075 * // create a DataSet
1076 * const data = new vis.DataSet([
1077 * { id: 1, text: 'item 1' },
1078 * { id: 2, text: 'item 2' },
1079 * { id: 3, text: 'item 3' },
1080 * ])
1081 *
1082 * // update items
1083 * const ids = data.update([
1084 * { id: 2, text: 'item 2 (updated)' }, // works
1085 * // { id: 4, text: 'item 4 (new)' }, // would throw
1086 * // { text: 'item 4 (new)' }, // would also throw
1087 * ])
1088 *
1089 * console.log(ids) // [2]
1090 * ```
1091 *
1092 * @param data - Updates (the id and optionally other props) to the items in this data set.
1093 * @param senderId - Sender id.
1094 *
1095 * @returns updatedIds - The ids of the updated items.
1096 *
1097 * @throws When the supplied data is neither an item nor an array of items, when the ids are missing.
1098 */
1099 updateOnly(data, senderId) {
1100 if (!Array.isArray(data)) {
1101 data = [data];
1102 }
1103 const updateEventData = data
1104 .map((update) => {
1105 const oldData = this._data.get(update[this._idProp]);
1106 if (oldData == null) {
1107 throw new Error("Updating non-existent items is not allowed.");
1108 }
1109 return { oldData, update };
1110 })
1111 .map(({ oldData, update }) => {
1112 const id = oldData[this._idProp];
1113 const updatedData = esnext.pureDeepObjectAssign(oldData, update);
1114 this._data.set(id, updatedData);
1115 return {
1116 id,
1117 oldData: oldData,
1118 updatedData,
1119 };
1120 });
1121 if (updateEventData.length) {
1122 const props = {
1123 items: updateEventData.map((value) => value.id),
1124 oldData: updateEventData.map((value) => value.oldData),
1125 data: updateEventData.map((value) => value.updatedData),
1126 };
1127 // TODO: remove deprecated property 'data' some day
1128 //Object.defineProperty(props, 'data', {
1129 // 'get': (function() {
1130 // 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');
1131 // return updatedData;
1132 // }).bind(this)
1133 //});
1134 this._trigger("update", props, senderId);
1135 return props.items;
1136 }
1137 else {
1138 return [];
1139 }
1140 }
1141 /** @inheritDoc */
1142 get(first, second) {
1143 // @TODO: Woudn't it be better to split this into multiple methods?
1144 // parse the arguments
1145 let id = undefined;
1146 let ids = undefined;
1147 let options = undefined;
1148 if (isId(first)) {
1149 // get(id [, options])
1150 id = first;
1151 options = second;
1152 }
1153 else if (Array.isArray(first)) {
1154 // get(ids [, options])
1155 ids = first;
1156 options = second;
1157 }
1158 else {
1159 // get([, options])
1160 options = first;
1161 }
1162 // determine the return type
1163 const returnType = options && options.returnType === "Object" ? "Object" : "Array";
1164 // @TODO: WTF is this? Or am I missing something?
1165 // var returnType
1166 // if (options && options.returnType) {
1167 // var allowedValues = ['Array', 'Object']
1168 // returnType =
1169 // allowedValues.indexOf(options.returnType) == -1
1170 // ? 'Array'
1171 // : options.returnType
1172 // } else {
1173 // returnType = 'Array'
1174 // }
1175 // build options
1176 const filter = options && options.filter;
1177 const items = [];
1178 let item = undefined;
1179 let itemIds = undefined;
1180 let itemId = undefined;
1181 // convert items
1182 if (id != null) {
1183 // return a single item
1184 item = this._data.get(id);
1185 if (item && filter && !filter(item)) {
1186 item = undefined;
1187 }
1188 }
1189 else if (ids != null) {
1190 // return a subset of items
1191 for (let i = 0, len = ids.length; i < len; i++) {
1192 item = this._data.get(ids[i]);
1193 if (item != null && (!filter || filter(item))) {
1194 items.push(item);
1195 }
1196 }
1197 }
1198 else {
1199 // return all items
1200 itemIds = [...this._data.keys()];
1201 for (let i = 0, len = itemIds.length; i < len; i++) {
1202 itemId = itemIds[i];
1203 item = this._data.get(itemId);
1204 if (item != null && (!filter || filter(item))) {
1205 items.push(item);
1206 }
1207 }
1208 }
1209 // order the results
1210 if (options && options.order && id == undefined) {
1211 this._sort(items, options.order);
1212 }
1213 // filter fields of the items
1214 if (options && options.fields) {
1215 const fields = options.fields;
1216 if (id != undefined && item != null) {
1217 item = this._filterFields(item, fields);
1218 }
1219 else {
1220 for (let i = 0, len = items.length; i < len; i++) {
1221 items[i] = this._filterFields(items[i], fields);
1222 }
1223 }
1224 }
1225 // return the results
1226 if (returnType == "Object") {
1227 const result = {};
1228 for (let i = 0, len = items.length; i < len; i++) {
1229 const resultant = items[i];
1230 // @TODO: Shoudn't this be this._fieldId?
1231 // result[resultant.id] = resultant
1232 const id = resultant[this._idProp];
1233 result[id] = resultant;
1234 }
1235 return result;
1236 }
1237 else {
1238 if (id != null) {
1239 // a single item
1240 return item ?? null;
1241 }
1242 else {
1243 // just return our array
1244 return items;
1245 }
1246 }
1247 }
1248 /** @inheritDoc */
1249 getIds(options) {
1250 const data = this._data;
1251 const filter = options && options.filter;
1252 const order = options && options.order;
1253 const itemIds = [...data.keys()];
1254 const ids = [];
1255 if (filter) {
1256 // get filtered items
1257 if (order) {
1258 // create ordered list
1259 const items = [];
1260 for (let i = 0, len = itemIds.length; i < len; i++) {
1261 const id = itemIds[i];
1262 const item = this._data.get(id);
1263 if (item != null && filter(item)) {
1264 items.push(item);
1265 }
1266 }
1267 this._sort(items, order);
1268 for (let i = 0, len = items.length; i < len; i++) {
1269 ids.push(items[i][this._idProp]);
1270 }
1271 }
1272 else {
1273 // create unordered list
1274 for (let i = 0, len = itemIds.length; i < len; i++) {
1275 const id = itemIds[i];
1276 const item = this._data.get(id);
1277 if (item != null && filter(item)) {
1278 ids.push(item[this._idProp]);
1279 }
1280 }
1281 }
1282 }
1283 else {
1284 // get all items
1285 if (order) {
1286 // create an ordered list
1287 const items = [];
1288 for (let i = 0, len = itemIds.length; i < len; i++) {
1289 const id = itemIds[i];
1290 items.push(data.get(id));
1291 }
1292 this._sort(items, order);
1293 for (let i = 0, len = items.length; i < len; i++) {
1294 ids.push(items[i][this._idProp]);
1295 }
1296 }
1297 else {
1298 // create unordered list
1299 for (let i = 0, len = itemIds.length; i < len; i++) {
1300 const id = itemIds[i];
1301 const item = data.get(id);
1302 if (item != null) {
1303 ids.push(item[this._idProp]);
1304 }
1305 }
1306 }
1307 }
1308 return ids;
1309 }
1310 /** @inheritDoc */
1311 getDataSet() {
1312 return this;
1313 }
1314 /** @inheritDoc */
1315 forEach(callback, options) {
1316 const filter = options && options.filter;
1317 const data = this._data;
1318 const itemIds = [...data.keys()];
1319 if (options && options.order) {
1320 // execute forEach on ordered list
1321 const items = this.get(options);
1322 for (let i = 0, len = items.length; i < len; i++) {
1323 const item = items[i];
1324 const id = item[this._idProp];
1325 callback(item, id);
1326 }
1327 }
1328 else {
1329 // unordered
1330 for (let i = 0, len = itemIds.length; i < len; i++) {
1331 const id = itemIds[i];
1332 const item = this._data.get(id);
1333 if (item != null && (!filter || filter(item))) {
1334 callback(item, id);
1335 }
1336 }
1337 }
1338 }
1339 /** @inheritDoc */
1340 map(callback, options) {
1341 const filter = options && options.filter;
1342 const mappedItems = [];
1343 const data = this._data;
1344 const itemIds = [...data.keys()];
1345 // convert and filter items
1346 for (let i = 0, len = itemIds.length; i < len; i++) {
1347 const id = itemIds[i];
1348 const item = this._data.get(id);
1349 if (item != null && (!filter || filter(item))) {
1350 mappedItems.push(callback(item, id));
1351 }
1352 }
1353 // order items
1354 if (options && options.order) {
1355 this._sort(mappedItems, options.order);
1356 }
1357 return mappedItems;
1358 }
1359 /**
1360 * Filter the fields of an item.
1361 *
1362 * @param item - The item whose fields should be filtered.
1363 * @param fields - The names of the fields that will be kept.
1364 *
1365 * @typeParam K - Field name type.
1366 *
1367 * @returns The item without any additional fields.
1368 */
1369 _filterFields(item, fields) {
1370 if (!item) {
1371 // item is null
1372 return item;
1373 }
1374 return (Array.isArray(fields)
1375 ? // Use the supplied array
1376 fields
1377 : // Use the keys of the supplied object
1378 Object.keys(fields)).reduce((filteredItem, field) => {
1379 filteredItem[field] = item[field];
1380 return filteredItem;
1381 }, {});
1382 }
1383 /**
1384 * Sort the provided array with items.
1385 *
1386 * @param items - Items to be sorted in place.
1387 * @param order - A field name or custom sort function.
1388 *
1389 * @typeParam T - The type of the items in the items array.
1390 */
1391 _sort(items, order) {
1392 if (typeof order === "string") {
1393 // order by provided field name
1394 const name = order; // field name
1395 items.sort((a, b) => {
1396 // @TODO: How to treat missing properties?
1397 const av = a[name];
1398 const bv = b[name];
1399 return av > bv ? 1 : av < bv ? -1 : 0;
1400 });
1401 }
1402 else if (typeof order === "function") {
1403 // order by sort function
1404 items.sort(order);
1405 }
1406 else {
1407 // TODO: extend order by an Object {field:string, direction:string}
1408 // where direction can be 'asc' or 'desc'
1409 throw new TypeError("Order must be a function or a string");
1410 }
1411 }
1412 /**
1413 * Remove an item or multiple items by “reference” (only the id is used) or by id.
1414 *
1415 * 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.
1416 *
1417 * 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.
1418 *
1419 * ## Example
1420 * ```javascript
1421 * // create a DataSet
1422 * const data = new vis.DataSet([
1423 * { id: 1, text: 'item 1' },
1424 * { id: 2, text: 'item 2' },
1425 * { id: 3, text: 'item 3' }
1426 * ])
1427 *
1428 * // remove items
1429 * const ids = data.remove([2, { id: 3 }, 4])
1430 *
1431 * console.log(ids) // [2, 3]
1432 * ```
1433 *
1434 * @param id - One or more items or ids of items to be removed.
1435 * @param senderId - Sender id.
1436 *
1437 * @returns The ids of the removed items.
1438 */
1439 remove(id, senderId) {
1440 const removedIds = [];
1441 const removedItems = [];
1442 // force everything to be an array for simplicity
1443 const ids = Array.isArray(id) ? id : [id];
1444 for (let i = 0, len = ids.length; i < len; i++) {
1445 const item = this._remove(ids[i]);
1446 if (item) {
1447 const itemId = item[this._idProp];
1448 if (itemId != null) {
1449 removedIds.push(itemId);
1450 removedItems.push(item);
1451 }
1452 }
1453 }
1454 if (removedIds.length) {
1455 this._trigger("remove", { items: removedIds, oldData: removedItems }, senderId);
1456 }
1457 return removedIds;
1458 }
1459 /**
1460 * Remove an item by its id or reference.
1461 *
1462 * @param id - Id of an item or the item itself.
1463 *
1464 * @returns The removed item if removed, null otherwise.
1465 */
1466 _remove(id) {
1467 // @TODO: It origianlly returned the item although the docs say id.
1468 // The code expects the item, so probably an error in the docs.
1469 let ident;
1470 // confirm the id to use based on the args type
1471 if (isId(id)) {
1472 ident = id;
1473 }
1474 else if (id && typeof id === "object") {
1475 ident = id[this._idProp]; // look for the identifier field using ._idProp
1476 }
1477 // do the removing if the item is found
1478 if (ident != null && this._data.has(ident)) {
1479 const item = this._data.get(ident) || null;
1480 this._data.delete(ident);
1481 --this.length;
1482 return item;
1483 }
1484 return null;
1485 }
1486 /**
1487 * Clear the entire data set.
1488 *
1489 * After the items are removed, the [[DataSet]] will trigger an event `remove` for all removed items. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers.
1490 *
1491 * @param senderId - Sender id.
1492 *
1493 * @returns removedIds - The ids of all removed items.
1494 */
1495 clear(senderId) {
1496 const ids = [...this._data.keys()];
1497 const items = [];
1498 for (let i = 0, len = ids.length; i < len; i++) {
1499 items.push(this._data.get(ids[i]));
1500 }
1501 this._data.clear();
1502 this.length = 0;
1503 this._trigger("remove", { items: ids, oldData: items }, senderId);
1504 return ids;
1505 }
1506 /**
1507 * Find the item with maximum value of a specified field.
1508 *
1509 * @param field - Name of the property that should be searched for max value.
1510 *
1511 * @returns Item containing max value, or null if no items.
1512 */
1513 max(field) {
1514 let max = null;
1515 let maxField = null;
1516 for (const item of this._data.values()) {
1517 const itemField = item[field];
1518 if (typeof itemField === "number" &&
1519 (maxField == null || itemField > maxField)) {
1520 max = item;
1521 maxField = itemField;
1522 }
1523 }
1524 return max || null;
1525 }
1526 /**
1527 * Find the item with minimum value of a specified field.
1528 *
1529 * @param field - Name of the property that should be searched for min value.
1530 *
1531 * @returns Item containing min value, or null if no items.
1532 */
1533 min(field) {
1534 let min = null;
1535 let minField = null;
1536 for (const item of this._data.values()) {
1537 const itemField = item[field];
1538 if (typeof itemField === "number" &&
1539 (minField == null || itemField < minField)) {
1540 min = item;
1541 minField = itemField;
1542 }
1543 }
1544 return min || null;
1545 }
1546 /**
1547 * Find all distinct values of a specified field
1548 *
1549 * @param prop - The property name whose distinct values should be returned.
1550 *
1551 * @returns Unordered array containing all distinct values. Items without specified property are ignored.
1552 */
1553 distinct(prop) {
1554 const data = this._data;
1555 const itemIds = [...data.keys()];
1556 const values = [];
1557 let count = 0;
1558 for (let i = 0, len = itemIds.length; i < len; i++) {
1559 const id = itemIds[i];
1560 const item = data.get(id);
1561 const value = item[prop];
1562 let exists = false;
1563 for (let j = 0; j < count; j++) {
1564 if (values[j] == value) {
1565 exists = true;
1566 break;
1567 }
1568 }
1569 if (!exists && value !== undefined) {
1570 values[count] = value;
1571 count++;
1572 }
1573 }
1574 return values;
1575 }
1576 /**
1577 * Add a single item. Will fail when an item with the same id already exists.
1578 *
1579 * @param item - A new item to be added.
1580 *
1581 * @returns Added item's id. An id is generated when it is not present in the item.
1582 */
1583 _addItem(item) {
1584 const fullItem = ensureFullItem(item, this._idProp);
1585 const id = fullItem[this._idProp];
1586 // check whether this id is already taken
1587 if (this._data.has(id)) {
1588 // item already exists
1589 throw new Error("Cannot add item: item with id " + id + " already exists");
1590 }
1591 this._data.set(id, fullItem);
1592 ++this.length;
1593 return id;
1594 }
1595 /**
1596 * Update a single item: merge with existing item.
1597 * Will fail when the item has no id, or when there does not exist an item with the same id.
1598 *
1599 * @param update - The new item
1600 *
1601 * @returns The id of the updated item.
1602 */
1603 _updateItem(update) {
1604 const id = update[this._idProp];
1605 if (id == null) {
1606 throw new Error("Cannot update item: item has no id (item: " +
1607 JSON.stringify(update) +
1608 ")");
1609 }
1610 const item = this._data.get(id);
1611 if (!item) {
1612 // item doesn't exist
1613 throw new Error("Cannot update item: no item with id " + id + " found");
1614 }
1615 this._data.set(id, { ...item, ...update });
1616 return id;
1617 }
1618 /** @inheritDoc */
1619 stream(ids) {
1620 if (ids) {
1621 const data = this._data;
1622 return new DataStream({
1623 *[Symbol.iterator]() {
1624 for (const id of ids) {
1625 const item = data.get(id);
1626 if (item != null) {
1627 yield [id, item];
1628 }
1629 }
1630 },
1631 });
1632 }
1633 else {
1634 return new DataStream({
1635 [Symbol.iterator]: this._data.entries.bind(this._data),
1636 });
1637 }
1638 }
1639 }
1640
1641 /**
1642 * DataView
1643 *
1644 * A DataView offers a filtered and/or formatted view on a DataSet. One can subscribe to changes in a DataView, and easily get filtered or formatted data without having to specify filters and field types all the time.
1645 *
1646 * ## Example
1647 * ```javascript
1648 * // create a DataSet
1649 * var data = new vis.DataSet();
1650 * data.add([
1651 * {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
1652 * {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
1653 * {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
1654 * {id: 4, text: 'item 4'}
1655 * ]);
1656 *
1657 * // create a DataView
1658 * // the view will only contain items having a property group with value 1,
1659 * // and will only output fields id, text, and date.
1660 * var view = new vis.DataView(data, {
1661 * filter: function (item) {
1662 * return (item.group == 1);
1663 * },
1664 * fields: ['id', 'text', 'date']
1665 * });
1666 *
1667 * // subscribe to any change in the DataView
1668 * view.on('*', function (event, properties, senderId) {
1669 * console.log('event', event, properties);
1670 * });
1671 *
1672 * // update an item in the data set
1673 * data.update({id: 2, group: 1});
1674 *
1675 * // get all ids in the view
1676 * var ids = view.getIds();
1677 * console.log('ids', ids); // will output [1, 2]
1678 *
1679 * // get all items in the view
1680 * var items = view.get();
1681 * ```
1682 *
1683 * @typeParam Item - Item type that may or may not have an id.
1684 * @typeParam IdProp - Name of the property that contains the id.
1685 */
1686 class DataView extends DataSetPart {
1687 /**
1688 * Create a DataView.
1689 *
1690 * @param data - The instance containing data (directly or indirectly).
1691 * @param options - Options to configure this data view.
1692 */
1693 constructor(data, options) {
1694 super();
1695 /** @inheritDoc */
1696 this.length = 0;
1697 this._ids = new Set(); // ids of the items currently in memory (just contains a boolean true)
1698 this._options = options || {};
1699 this._listener = this._onEvent.bind(this);
1700 this.setData(data);
1701 }
1702 /** @inheritDoc */
1703 get idProp() {
1704 return this.getDataSet().idProp;
1705 }
1706 // TODO: implement a function .config() to dynamically update things like configured filter
1707 // and trigger changes accordingly
1708 /**
1709 * Set a data source for the view.
1710 *
1711 * @param data - The instance containing data (directly or indirectly).
1712 *
1713 * @remarks
1714 * Note that when the data view is bound to a data set it won't be garbage
1715 * collected unless the data set is too. Use `dataView.setData(null)` or
1716 * `dataView.dispose()` to enable garbage collection before you lose the last
1717 * reference.
1718 */
1719 setData(data) {
1720 if (this._data) {
1721 // unsubscribe from current dataset
1722 if (this._data.off) {
1723 this._data.off("*", this._listener);
1724 }
1725 // trigger a remove of all items in memory
1726 const ids = this._data.getIds({ filter: this._options.filter });
1727 const items = this._data.get(ids);
1728 this._ids.clear();
1729 this.length = 0;
1730 this._trigger("remove", { items: ids, oldData: items });
1731 }
1732 if (data != null) {
1733 this._data = data;
1734 // trigger an add of all added items
1735 const ids = this._data.getIds({ filter: this._options.filter });
1736 for (let i = 0, len = ids.length; i < len; i++) {
1737 const id = ids[i];
1738 this._ids.add(id);
1739 }
1740 this.length = ids.length;
1741 this._trigger("add", { items: ids });
1742 }
1743 else {
1744 this._data = new DataSet();
1745 }
1746 // subscribe to new dataset
1747 if (this._data.on) {
1748 this._data.on("*", this._listener);
1749 }
1750 }
1751 /**
1752 * Refresh the DataView.
1753 * Useful when the DataView has a filter function containing a variable parameter.
1754 */
1755 refresh() {
1756 const ids = this._data.getIds({
1757 filter: this._options.filter,
1758 });
1759 const oldIds = [...this._ids];
1760 const newIds = {};
1761 const addedIds = [];
1762 const removedIds = [];
1763 const removedItems = [];
1764 // check for additions
1765 for (let i = 0, len = ids.length; i < len; i++) {
1766 const id = ids[i];
1767 newIds[id] = true;
1768 if (!this._ids.has(id)) {
1769 addedIds.push(id);
1770 this._ids.add(id);
1771 }
1772 }
1773 // check for removals
1774 for (let i = 0, len = oldIds.length; i < len; i++) {
1775 const id = oldIds[i];
1776 const item = this._data.get(id);
1777 if (item == null) {
1778 // @TODO: Investigate.
1779 // Doesn't happen during tests or examples.
1780 // Is it really impossible or could it eventually happen?
1781 // How to handle it if it does? The types guarantee non-nullable items.
1782 console.error("If you see this, report it please.");
1783 }
1784 else if (!newIds[id]) {
1785 removedIds.push(id);
1786 removedItems.push(item);
1787 this._ids.delete(id);
1788 }
1789 }
1790 this.length += addedIds.length - removedIds.length;
1791 // trigger events
1792 if (addedIds.length) {
1793 this._trigger("add", { items: addedIds });
1794 }
1795 if (removedIds.length) {
1796 this._trigger("remove", { items: removedIds, oldData: removedItems });
1797 }
1798 }
1799 /** @inheritDoc */
1800 get(first, second) {
1801 if (this._data == null) {
1802 return null;
1803 }
1804 // parse the arguments
1805 let ids = null;
1806 let options;
1807 if (isId(first) || Array.isArray(first)) {
1808 ids = first;
1809 options = second;
1810 }
1811 else {
1812 options = first;
1813 }
1814 // extend the options with the default options and provided options
1815 const viewOptions = Object.assign({}, this._options, options);
1816 // create a combined filter method when needed
1817 const thisFilter = this._options.filter;
1818 const optionsFilter = options && options.filter;
1819 if (thisFilter && optionsFilter) {
1820 viewOptions.filter = (item) => {
1821 return thisFilter(item) && optionsFilter(item);
1822 };
1823 }
1824 if (ids == null) {
1825 return this._data.get(viewOptions);
1826 }
1827 else {
1828 return this._data.get(ids, viewOptions);
1829 }
1830 }
1831 /** @inheritDoc */
1832 getIds(options) {
1833 if (this._data.length) {
1834 const defaultFilter = this._options.filter;
1835 const optionsFilter = options != null ? options.filter : null;
1836 let filter;
1837 if (optionsFilter) {
1838 if (defaultFilter) {
1839 filter = (item) => {
1840 return defaultFilter(item) && optionsFilter(item);
1841 };
1842 }
1843 else {
1844 filter = optionsFilter;
1845 }
1846 }
1847 else {
1848 filter = defaultFilter;
1849 }
1850 return this._data.getIds({
1851 filter: filter,
1852 order: options && options.order,
1853 });
1854 }
1855 else {
1856 return [];
1857 }
1858 }
1859 /** @inheritDoc */
1860 forEach(callback, options) {
1861 if (this._data) {
1862 const defaultFilter = this._options.filter;
1863 const optionsFilter = options && options.filter;
1864 let filter;
1865 if (optionsFilter) {
1866 if (defaultFilter) {
1867 filter = function (item) {
1868 return defaultFilter(item) && optionsFilter(item);
1869 };
1870 }
1871 else {
1872 filter = optionsFilter;
1873 }
1874 }
1875 else {
1876 filter = defaultFilter;
1877 }
1878 this._data.forEach(callback, {
1879 filter: filter,
1880 order: options && options.order,
1881 });
1882 }
1883 }
1884 /** @inheritDoc */
1885 map(callback, options) {
1886 if (this._data) {
1887 const defaultFilter = this._options.filter;
1888 const optionsFilter = options && options.filter;
1889 let filter;
1890 if (optionsFilter) {
1891 if (defaultFilter) {
1892 filter = (item) => {
1893 return defaultFilter(item) && optionsFilter(item);
1894 };
1895 }
1896 else {
1897 filter = optionsFilter;
1898 }
1899 }
1900 else {
1901 filter = defaultFilter;
1902 }
1903 return this._data.map(callback, {
1904 filter: filter,
1905 order: options && options.order,
1906 });
1907 }
1908 else {
1909 return [];
1910 }
1911 }
1912 /** @inheritDoc */
1913 getDataSet() {
1914 return this._data.getDataSet();
1915 }
1916 /** @inheritDoc */
1917 stream(ids) {
1918 return this._data.stream(ids || {
1919 [Symbol.iterator]: this._ids.keys.bind(this._ids),
1920 });
1921 }
1922 /**
1923 * Render the instance unusable prior to garbage collection.
1924 *
1925 * @remarks
1926 * The intention of this method is to help discover scenarios where the data
1927 * view is being used when the programmer thinks it has been garbage collected
1928 * already. It's stricter version of `dataView.setData(null)`.
1929 */
1930 dispose() {
1931 if (this._data?.off) {
1932 this._data.off("*", this._listener);
1933 }
1934 const message = "This data view has already been disposed of.";
1935 const replacement = {
1936 get: () => {
1937 throw new Error(message);
1938 },
1939 set: () => {
1940 throw new Error(message);
1941 },
1942 configurable: false,
1943 };
1944 for (const key of Reflect.ownKeys(DataView.prototype)) {
1945 Object.defineProperty(this, key, replacement);
1946 }
1947 }
1948 /**
1949 * 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.
1950 *
1951 * @param event - The name of the event.
1952 * @param params - Parameters of the event.
1953 * @param senderId - Id supplied by the sender.
1954 */
1955 _onEvent(event, params, senderId) {
1956 if (!params || !params.items || !this._data) {
1957 return;
1958 }
1959 const ids = params.items;
1960 const addedIds = [];
1961 const updatedIds = [];
1962 const removedIds = [];
1963 const oldItems = [];
1964 const updatedItems = [];
1965 const removedItems = [];
1966 switch (event) {
1967 case "add":
1968 // filter the ids of the added items
1969 for (let i = 0, len = ids.length; i < len; i++) {
1970 const id = ids[i];
1971 const item = this.get(id);
1972 if (item) {
1973 this._ids.add(id);
1974 addedIds.push(id);
1975 }
1976 }
1977 break;
1978 case "update":
1979 // determine the event from the views viewpoint: an updated
1980 // item can be added, updated, or removed from this view.
1981 for (let i = 0, len = ids.length; i < len; i++) {
1982 const id = ids[i];
1983 const item = this.get(id);
1984 if (item) {
1985 if (this._ids.has(id)) {
1986 updatedIds.push(id);
1987 updatedItems.push(params.data[i]);
1988 oldItems.push(params.oldData[i]);
1989 }
1990 else {
1991 this._ids.add(id);
1992 addedIds.push(id);
1993 }
1994 }
1995 else {
1996 if (this._ids.has(id)) {
1997 this._ids.delete(id);
1998 removedIds.push(id);
1999 removedItems.push(params.oldData[i]);
2000 }
2001 }
2002 }
2003 break;
2004 case "remove":
2005 // filter the ids of the removed items
2006 for (let i = 0, len = ids.length; i < len; i++) {
2007 const id = ids[i];
2008 if (this._ids.has(id)) {
2009 this._ids.delete(id);
2010 removedIds.push(id);
2011 removedItems.push(params.oldData[i]);
2012 }
2013 }
2014 break;
2015 }
2016 this.length += addedIds.length - removedIds.length;
2017 if (addedIds.length) {
2018 this._trigger("add", { items: addedIds }, senderId);
2019 }
2020 if (updatedIds.length) {
2021 this._trigger("update", { items: updatedIds, oldData: oldItems, data: updatedItems }, senderId);
2022 }
2023 if (removedIds.length) {
2024 this._trigger("remove", { items: removedIds, oldData: removedItems }, senderId);
2025 }
2026 }
2027 }
2028
2029 /**
2030 * Check that given value is compatible with Vis Data Set interface.
2031 *
2032 * @param idProp - The expected property to contain item id.
2033 * @param v - The value to be tested.
2034 *
2035 * @returns True if all expected values and methods match, false otherwise.
2036 */
2037 function isDataSetLike(idProp, v) {
2038 return (typeof v === "object" &&
2039 v !== null &&
2040 idProp === v.idProp &&
2041 typeof v.add === "function" &&
2042 typeof v.clear === "function" &&
2043 typeof v.distinct === "function" &&
2044 typeof v.forEach === "function" &&
2045 typeof v.get === "function" &&
2046 typeof v.getDataSet === "function" &&
2047 typeof v.getIds === "function" &&
2048 typeof v.length === "number" &&
2049 typeof v.map === "function" &&
2050 typeof v.max === "function" &&
2051 typeof v.min === "function" &&
2052 typeof v.off === "function" &&
2053 typeof v.on === "function" &&
2054 typeof v.remove === "function" &&
2055 typeof v.setOptions === "function" &&
2056 typeof v.stream === "function" &&
2057 typeof v.update === "function" &&
2058 typeof v.updateOnly === "function");
2059 }
2060
2061 /**
2062 * Check that given value is compatible with Vis Data View interface.
2063 *
2064 * @param idProp - The expected property to contain item id.
2065 * @param v - The value to be tested.
2066 *
2067 * @returns True if all expected values and methods match, false otherwise.
2068 */
2069 function isDataViewLike(idProp, v) {
2070 return (typeof v === "object" &&
2071 v !== null &&
2072 idProp === v.idProp &&
2073 typeof v.forEach === "function" &&
2074 typeof v.get === "function" &&
2075 typeof v.getDataSet === "function" &&
2076 typeof v.getIds === "function" &&
2077 typeof v.length === "number" &&
2078 typeof v.map === "function" &&
2079 typeof v.off === "function" &&
2080 typeof v.on === "function" &&
2081 typeof v.stream === "function" &&
2082 isDataSetLike(idProp, v.getDataSet()));
2083 }
2084
2085 Object.defineProperty(exports, 'DELETE', {
2086 enumerable: true,
2087 get: function () {
2088 return esnext.DELETE;
2089 }
2090 });
2091 exports.DataSet = DataSet;
2092 exports.DataStream = DataStream;
2093 exports.DataView = DataView;
2094 exports.Queue = Queue;
2095 exports.createNewDataPipeFrom = createNewDataPipeFrom;
2096 exports.isDataSetLike = isDataSetLike;
2097 exports.isDataViewLike = isDataViewLike;
2098
2099 Object.defineProperty(exports, '__esModule', { value: true });
2100
2101})));
2102//# sourceMappingURL=vis-data.js.map