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.

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