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.

400 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 args
  141. */
  142. DataView.prototype.get = function (args) {
  143. var me = this;
  144. // parse the arguments
  145. var ids, options, data;
  146. var firstType = util.getType(arguments[0]);
  147. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  148. // get(id(s) [, options] [, data])
  149. ids = arguments[0]; // can be a single id or an array with ids
  150. options = arguments[1];
  151. data = arguments[2];
  152. }
  153. else {
  154. // get([, options] [, data])
  155. options = arguments[0];
  156. data = arguments[1];
  157. }
  158. // extend the options with the default options and provided options
  159. var viewOptions = util.extend({}, this._options, options);
  160. // create a combined filter method when needed
  161. if (this._options.filter && options && options.filter) {
  162. viewOptions.filter = function (item) {
  163. return me._options.filter(item) && options.filter(item);
  164. }
  165. }
  166. // build up the call to the linked data set
  167. var getArguments = [];
  168. if (ids != undefined) {
  169. getArguments.push(ids);
  170. }
  171. getArguments.push(viewOptions);
  172. getArguments.push(data);
  173. return this._data && this._data.get.apply(this._data, getArguments);
  174. };
  175. /**
  176. * Get ids of all items or from a filtered set of items.
  177. * @param {Object} [options] An Object with options. Available options:
  178. * {function} [filter] filter items
  179. * {String | function} [order] Order the items by
  180. * a field name or custom sort function.
  181. * @return {Array} ids
  182. */
  183. DataView.prototype.getIds = function (options) {
  184. var ids;
  185. if (this._data) {
  186. var defaultFilter = this._options.filter;
  187. var filter;
  188. if (options && options.filter) {
  189. if (defaultFilter) {
  190. filter = function (item) {
  191. return defaultFilter(item) && options.filter(item);
  192. }
  193. }
  194. else {
  195. filter = options.filter;
  196. }
  197. }
  198. else {
  199. filter = defaultFilter;
  200. }
  201. ids = this._data.getIds({
  202. filter: filter,
  203. order: options && options.order
  204. });
  205. }
  206. else {
  207. ids = [];
  208. }
  209. return ids;
  210. };
  211. /**
  212. * Map every item in the dataset.
  213. * @param {function} callback
  214. * @param {Object} [options] Available options:
  215. * {Object.<String, String>} [type]
  216. * {String[]} [fields] filter fields
  217. * {function} [filter] filter items
  218. * {String | function} [order] Order the items by
  219. * a field name or custom sort function.
  220. * @return {Object[]} mappedItems
  221. */
  222. DataView.prototype.map = function (callback,options) {
  223. var mappedItems = [];
  224. if (this._data) {
  225. var defaultFilter = this._options.filter;
  226. var filter;
  227. if (options && options.filter) {
  228. if (defaultFilter) {
  229. filter = function (item) {
  230. return defaultFilter(item) && options.filter(item);
  231. }
  232. }
  233. else {
  234. filter = options.filter;
  235. }
  236. }
  237. else {
  238. filter = defaultFilter;
  239. }
  240. mappedItems = this._data.map(callback,{
  241. filter: filter,
  242. order: options && options.order
  243. });
  244. }
  245. else {
  246. mappedItems = [];
  247. }
  248. return mappedItems;
  249. };
  250. /**
  251. * Get the DataSet to which this DataView is connected. In case there is a chain
  252. * of multiple DataViews, the root DataSet of this chain is returned.
  253. * @return {DataSet} dataSet
  254. */
  255. DataView.prototype.getDataSet = function () {
  256. var dataSet = this;
  257. while (dataSet instanceof DataView) {
  258. dataSet = dataSet._data;
  259. }
  260. return dataSet || null;
  261. };
  262. /**
  263. * Event listener. Will propagate all events from the connected data set to
  264. * the subscribers of the DataView, but will filter the items and only trigger
  265. * when there are changes in the filtered data set.
  266. * @param {String} event
  267. * @param {Object | null} params
  268. * @param {String} senderId
  269. * @private
  270. */
  271. DataView.prototype._onEvent = function (event, params, senderId) {
  272. var i, len, id, item;
  273. var ids = params && params.items;
  274. var addedIds = [],
  275. updatedIds = [],
  276. removedIds = [],
  277. oldItems = [],
  278. updatedItems = [],
  279. removedItems = [];
  280. if (ids && this._data) {
  281. switch (event) {
  282. case 'add':
  283. // filter the ids of the added items
  284. for (i = 0, len = ids.length; i < len; i++) {
  285. id = ids[i];
  286. item = this.get(id);
  287. if (item) {
  288. this._ids[id] = true;
  289. addedIds.push(id);
  290. }
  291. }
  292. break;
  293. case 'update':
  294. // determine the event from the views viewpoint: an updated
  295. // item can be added, updated, or removed from this view.
  296. for (i = 0, len = ids.length; i < len; i++) {
  297. id = ids[i];
  298. item = this.get(id);
  299. if (item) {
  300. if (this._ids[id]) {
  301. updatedIds.push(id);
  302. updatedItems.push(params.data[i]);
  303. oldItems.push(params.oldData[i]);
  304. }
  305. else {
  306. this._ids[id] = true;
  307. addedIds.push(id);
  308. }
  309. }
  310. else {
  311. if (this._ids[id]) {
  312. delete this._ids[id];
  313. removedIds.push(id);
  314. removedItems.push(params.oldData[i]);
  315. }
  316. else {
  317. // nothing interesting for me :-(
  318. }
  319. }
  320. }
  321. break;
  322. case 'remove':
  323. // filter the ids of the removed items
  324. for (i = 0, len = ids.length; i < len; i++) {
  325. id = ids[i];
  326. if (this._ids[id]) {
  327. delete this._ids[id];
  328. removedIds.push(id);
  329. removedItems.push(params.oldData[i]);
  330. }
  331. }
  332. break;
  333. }
  334. this.length += addedIds.length - removedIds.length;
  335. if (addedIds.length) {
  336. this._trigger('add', {items: addedIds}, senderId);
  337. }
  338. if (updatedIds.length) {
  339. this._trigger('update', {items: updatedIds, oldData: oldItems, data: updatedItems}, senderId);
  340. }
  341. if (removedIds.length) {
  342. this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId);
  343. }
  344. }
  345. };
  346. // copy subscription functionality from DataSet
  347. DataView.prototype.on = DataSet.prototype.on;
  348. DataView.prototype.off = DataSet.prototype.off;
  349. DataView.prototype._trigger = DataSet.prototype._trigger;
  350. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  351. DataView.prototype.subscribe = DataView.prototype.on;
  352. DataView.prototype.unsubscribe = DataView.prototype.off;
  353. module.exports = DataView;