vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
11 KiB

  1. var util = require('./util');
  2. var DataSet = require('./DataSet');
  3. /**
  4. * DataView
  5. *
  6. * a dataview offers a filtered view on a dataset or an other dataview.
  7. *
  8. * @param {DataSet | DataView} data
  9. * @param {Object} [options] Available options: see method get
  10. *
  11. * @constructor DataView
  12. */
  13. function DataView (data, options) {
  14. this._data = null;
  15. this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
  16. this.length = 0; // number of items in the DataView
  17. this._options = options || {};
  18. this._fieldId = 'id'; // name of the field containing id
  19. this._subscribers = {}; // event subscribers
  20. var me = this;
  21. this.listener = function () {
  22. me._onEvent.apply(me, arguments);
  23. };
  24. this.setData(data);
  25. }
  26. // TODO: implement a function .config() to dynamically update things like configured filter
  27. // and trigger changes accordingly
  28. /**
  29. * Set a data source for the view
  30. * @param {DataSet | DataView} data
  31. */
  32. DataView.prototype.setData = function (data) {
  33. var ids, id, i, len, items;
  34. if (this._data) {
  35. // unsubscribe from current dataset
  36. if (this._data.off) {
  37. this._data.off('*', this.listener);
  38. }
  39. // trigger a remove of all items in memory
  40. ids = this._data.getIds({filter: this._options && this._options.filter});
  41. items = [];
  42. for (i = 0, len = ids.length; i < len; i++) {
  43. items.push(this._data._data[ids[i]]);
  44. }
  45. this._ids = {};
  46. this.length = 0;
  47. this._trigger('remove', {items: ids, oldData: items});
  48. }
  49. this._data = data;
  50. if (this._data) {
  51. // update fieldId
  52. this._fieldId = this._options.fieldId ||
  53. (this._data && this._data.options && this._data.options.fieldId) ||
  54. 'id';
  55. // trigger an add of all added items
  56. ids = this._data.getIds({filter: this._options && this._options.filter});
  57. for (i = 0, len = ids.length; i < len; i++) {
  58. id = ids[i];
  59. this._ids[id] = true;
  60. }
  61. this.length = ids.length;
  62. this._trigger('add', {items: ids});
  63. // subscribe to new dataset
  64. if (this._data.on) {
  65. this._data.on('*', this.listener);
  66. }
  67. }
  68. };
  69. /**
  70. * Refresh the DataView. Useful when the DataView has a filter function
  71. * containing a variable parameter.
  72. */
  73. DataView.prototype.refresh = function () {
  74. var id, i, len;
  75. var ids = this._data.getIds({filter: this._options && this._options.filter}),
  76. oldIds = Object.keys(this._ids),
  77. newIds = {},
  78. addedIds = [],
  79. removedIds = [],
  80. removedItems = [];
  81. // check for additions
  82. for (i = 0, len = ids.length; i < len; i++) {
  83. id = ids[i];
  84. newIds[id] = true;
  85. if (!this._ids[id]) {
  86. addedIds.push(id);
  87. this._ids[id] = true;
  88. }
  89. }
  90. // check for removals
  91. for (i = 0, len = oldIds.length; i < len; i++) {
  92. id = oldIds[i];
  93. if (!newIds[id]) {
  94. removedIds.push(id);
  95. removedItems.push(this._data._data[id]);
  96. delete this._ids[id];
  97. }
  98. }
  99. this.length += addedIds.length - removedIds.length;
  100. // trigger events
  101. if (addedIds.length) {
  102. this._trigger('add', {items: addedIds});
  103. }
  104. if (removedIds.length) {
  105. this._trigger('remove', {items: removedIds, oldData: removedItems});
  106. }
  107. };
  108. /**
  109. * Get data from the data view
  110. *
  111. * Usage:
  112. *
  113. * get()
  114. * get(options: Object)
  115. * get(options: Object, data: Array | DataTable)
  116. *
  117. * get(id: Number)
  118. * get(id: Number, options: Object)
  119. * get(id: Number, options: Object, data: Array | DataTable)
  120. *
  121. * get(ids: Number[])
  122. * get(ids: Number[], options: Object)
  123. * get(ids: Number[], options: Object, data: Array | DataTable)
  124. *
  125. * Where:
  126. *
  127. * {Number | String} id The id of an item
  128. * {Number[] | String{}} ids An array with ids of items
  129. * {Object} options An Object with options. Available options:
  130. * {String} [type] Type of data to be returned. Can
  131. * be 'DataTable' or 'Array' (default)
  132. * {Object.<String, String>} [convert]
  133. * {String[]} [fields] field names to be returned
  134. * {function} [filter] filter items
  135. * {String | function} [order] Order the items by
  136. * a field name or custom sort function.
  137. * {Array | DataTable} [data] If provided, items will be appended to this
  138. * array or table. Required in case of Google
  139. * DataTable.
  140. * @param {Array} args
  141. * @return {DataSet|DataView}
  142. */
  143. DataView.prototype.get = function (args) { // eslint-disable-line no-unused-vars
  144. var me = this;
  145. // parse the arguments
  146. var ids, options, data;
  147. var firstType = util.getType(arguments[0]);
  148. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  149. // get(id(s) [, options] [, data])
  150. ids = arguments[0]; // can be a single id or an array with ids
  151. options = arguments[1];
  152. data = arguments[2];
  153. }
  154. else {
  155. // get([, options] [, data])
  156. options = arguments[0];
  157. data = arguments[1];
  158. }
  159. // extend the options with the default options and provided options
  160. var viewOptions = util.extend({}, this._options, options);
  161. // create a combined filter method when needed
  162. if (this._options.filter && options && options.filter) {
  163. viewOptions.filter = function (item) {
  164. return me._options.filter(item) && options.filter(item);
  165. }
  166. }
  167. // build up the call to the linked data set
  168. var getArguments = [];
  169. if (ids != undefined) {
  170. getArguments.push(ids);
  171. }
  172. getArguments.push(viewOptions);
  173. getArguments.push(data);
  174. return this._data && this._data.get.apply(this._data, getArguments);
  175. };
  176. /**
  177. * Get ids of all items or from a filtered set of items.
  178. * @param {Object} [options] An Object with options. Available options:
  179. * {function} [filter] filter items
  180. * {String | function} [order] Order the items by
  181. * a field name or custom sort function.
  182. * @return {Array} ids
  183. */
  184. DataView.prototype.getIds = function (options) {
  185. var ids;
  186. if (this._data) {
  187. var defaultFilter = this._options.filter;
  188. var filter;
  189. if (options && options.filter) {
  190. if (defaultFilter) {
  191. filter = function (item) {
  192. return defaultFilter(item) && options.filter(item);
  193. }
  194. }
  195. else {
  196. filter = options.filter;
  197. }
  198. }
  199. else {
  200. filter = defaultFilter;
  201. }
  202. ids = this._data.getIds({
  203. filter: filter,
  204. order: options && options.order
  205. });
  206. }
  207. else {
  208. ids = [];
  209. }
  210. return ids;
  211. };
  212. /**
  213. * Map every item in the dataset.
  214. * @param {function} callback
  215. * @param {Object} [options] Available options:
  216. * {Object.<String, String>} [type]
  217. * {String[]} [fields] filter fields
  218. * {function} [filter] filter items
  219. * {String | function} [order] Order the items by
  220. * a field name or custom sort function.
  221. * @return {Object[]} mappedItems
  222. */
  223. DataView.prototype.map = function (callback,options) {
  224. var mappedItems = [];
  225. if (this._data) {
  226. var defaultFilter = this._options.filter;
  227. var filter;
  228. if (options && options.filter) {
  229. if (defaultFilter) {
  230. filter = function (item) {
  231. return defaultFilter(item) && options.filter(item);
  232. }
  233. }
  234. else {
  235. filter = options.filter;
  236. }
  237. }
  238. else {
  239. filter = defaultFilter;
  240. }
  241. mappedItems = this._data.map(callback,{
  242. filter: filter,
  243. order: options && options.order
  244. });
  245. }
  246. else {
  247. mappedItems = [];
  248. }
  249. return mappedItems;
  250. };
  251. /**
  252. * Get the DataSet to which this DataView is connected. In case there is a chain
  253. * of multiple DataViews, the root DataSet of this chain is returned.
  254. * @return {DataSet} dataSet
  255. */
  256. DataView.prototype.getDataSet = function () {
  257. var dataSet = this;
  258. while (dataSet instanceof DataView) {
  259. dataSet = dataSet._data;
  260. }
  261. return dataSet || null;
  262. };
  263. /**
  264. * Event listener. Will propagate all events from the connected data set to
  265. * the subscribers of the DataView, but will filter the items and only trigger
  266. * when there are changes in the filtered data set.
  267. * @param {String} event
  268. * @param {Object | null} params
  269. * @param {String} senderId
  270. * @private
  271. */
  272. DataView.prototype._onEvent = function (event, params, senderId) {
  273. var i, len, id, item;
  274. var ids = params && params.items;
  275. var addedIds = [],
  276. updatedIds = [],
  277. removedIds = [],
  278. oldItems = [],
  279. updatedItems = [],
  280. removedItems = [];
  281. if (ids && this._data) {
  282. switch (event) {
  283. case 'add':
  284. // filter the ids of the added items
  285. for (i = 0, len = ids.length; i < len; i++) {
  286. id = ids[i];
  287. item = this.get(id);
  288. if (item) {
  289. this._ids[id] = true;
  290. addedIds.push(id);
  291. }
  292. }
  293. break;
  294. case 'update':
  295. // determine the event from the views viewpoint: an updated
  296. // item can be added, updated, or removed from this view.
  297. for (i = 0, len = ids.length; i < len; i++) {
  298. id = ids[i];
  299. item = this.get(id);
  300. if (item) {
  301. if (this._ids[id]) {
  302. updatedIds.push(id);
  303. updatedItems.push(params.data[i]);
  304. oldItems.push(params.oldData[i]);
  305. }
  306. else {
  307. this._ids[id] = true;
  308. addedIds.push(id);
  309. }
  310. }
  311. else {
  312. if (this._ids[id]) {
  313. delete this._ids[id];
  314. removedIds.push(id);
  315. removedItems.push(params.oldData[i]);
  316. }
  317. else {
  318. // nothing interesting for me :-(
  319. }
  320. }
  321. }
  322. break;
  323. case 'remove':
  324. // filter the ids of the removed items
  325. for (i = 0, len = ids.length; i < len; i++) {
  326. id = ids[i];
  327. if (this._ids[id]) {
  328. delete this._ids[id];
  329. removedIds.push(id);
  330. removedItems.push(params.oldData[i]);
  331. }
  332. }
  333. break;
  334. }
  335. this.length += addedIds.length - removedIds.length;
  336. if (addedIds.length) {
  337. this._trigger('add', {items: addedIds}, senderId);
  338. }
  339. if (updatedIds.length) {
  340. this._trigger('update', {items: updatedIds, oldData: oldItems, data: updatedItems}, senderId);
  341. }
  342. if (removedIds.length) {
  343. this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId);
  344. }
  345. }
  346. };
  347. // copy subscription functionality from DataSet
  348. DataView.prototype.on = DataSet.prototype.on;
  349. DataView.prototype.off = DataSet.prototype.off;
  350. DataView.prototype._trigger = DataSet.prototype._trigger;
  351. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  352. DataView.prototype.subscribe = DataView.prototype.on;
  353. DataView.prototype.unsubscribe = DataView.prototype.off;
  354. module.exports = DataView;