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.

552 lines
16 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. var util = require('./util');
  2. /**
  3. * DataSet
  4. *
  5. * Usage:
  6. * var dataSet = new DataSet({
  7. * fieldId: '_id',
  8. * fieldTypes: {
  9. * // ...
  10. * }
  11. * });
  12. *
  13. * dataSet.add(item);
  14. * dataSet.add(data);
  15. * dataSet.update(item);
  16. * dataSet.update(data);
  17. * dataSet.remove(id);
  18. * dataSet.remove(ids);
  19. * var data = dataSet.get();
  20. * var data = dataSet.get(id);
  21. * var data = dataSet.get(ids);
  22. * var data = dataSet.get(ids, options, data);
  23. * dataSet.clear();
  24. *
  25. * A data set can:
  26. * - add/remove/update data
  27. * - gives triggers upon changes in the data
  28. * - can import/export data in various data formats
  29. * @param {Object} [options] Available options:
  30. * {String} fieldId Field name of the id in the
  31. * items, 'id' by default.
  32. * {Object.<String, String} fieldTypes
  33. * A map with field names as key,
  34. * and the field type as value.
  35. */
  36. function DataSet (options) {
  37. var me = this;
  38. this.options = options || {};
  39. this.data = {}; // map with data indexed by id
  40. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  41. this.fieldTypes = {}; // field types by field name
  42. if (this.options.fieldTypes) {
  43. util.forEach(this.options.fieldTypes, function (value, field) {
  44. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  45. me.fieldTypes[field] = 'Date';
  46. }
  47. else {
  48. me.fieldTypes[field] = value;
  49. }
  50. });
  51. }
  52. // event subscribers
  53. this.subscribers = {};
  54. this.internalIds = {}; // internally generated id's
  55. }
  56. /**
  57. * Subscribe to an event, add an event listener
  58. * @param {String} event Event name. Available events: 'put', 'update',
  59. * 'remove'
  60. * @param {function} callback Callback method. Called with three parameters:
  61. * {String} event
  62. * {Object | null} params
  63. * {String} senderId
  64. * @param {String} [id] Optional id for the sender, used to filter
  65. * events triggered by the sender itself.
  66. */
  67. DataSet.prototype.subscribe = function (event, callback, id) {
  68. var subscribers = this.subscribers[event];
  69. if (!subscribers) {
  70. subscribers = [];
  71. this.subscribers[event] = subscribers;
  72. }
  73. subscribers.push({
  74. id: id ? String(id) : null,
  75. callback: callback
  76. });
  77. };
  78. /**
  79. * Unsubscribe from an event, remove an event listener
  80. * @param {String} event
  81. * @param {function} callback
  82. */
  83. DataSet.prototype.unsubscribe = function (event, callback) {
  84. var subscribers = this.subscribers[event];
  85. if (subscribers) {
  86. this.subscribers[event] = subscribers.filter(function (listener) {
  87. return (listener.callback != callback);
  88. });
  89. }
  90. };
  91. /**
  92. * Trigger an event
  93. * @param {String} event
  94. * @param {Object | null} params
  95. * @param {String} [senderId] Optional id of the sender. The event will
  96. * be triggered for all subscribers except the
  97. * sender itself.
  98. * @private
  99. */
  100. DataSet.prototype._trigger = function (event, params, senderId) {
  101. if (event == '*') {
  102. throw new Error('Cannot trigger event *');
  103. }
  104. var subscribers = [];
  105. if (event in this.subscribers) {
  106. subscribers = subscribers.concat(this.subscribers[event]);
  107. }
  108. if ('*' in this.subscribers) {
  109. subscribers = subscribers.concat(this.subscribers['*']);
  110. }
  111. subscribers.forEach(function (listener) {
  112. if (listener.id != senderId && listener.callback) {
  113. listener.callback(event, params, senderId || null);
  114. }
  115. });
  116. };
  117. /**
  118. * Add data. Existing items with the same id will be overwritten.
  119. * @param {Object | Array | DataTable} data
  120. * @param {String} [senderId] Optional sender id, used to trigger events for
  121. * all but this sender's event subscribers.
  122. */
  123. DataSet.prototype.add = function (data, senderId) {
  124. var items = [],
  125. id,
  126. me = this;
  127. if (data instanceof Array) {
  128. // Array
  129. data.forEach(function (item) {
  130. var id = me._addItem(item);
  131. items.push(id);
  132. });
  133. }
  134. else if (util.isDataTable(data)) {
  135. // Google DataTable
  136. var columns = this._getColumnNames(data);
  137. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  138. var item = {};
  139. columns.forEach(function (field, col) {
  140. item[field] = data.getValue(row, col);
  141. });
  142. id = me._addItem(item);
  143. items.push(id);
  144. }
  145. }
  146. else if (data instanceof Object) {
  147. // Single item
  148. id = me._addItem(data);
  149. items.push(id);
  150. }
  151. else {
  152. throw new Error('Unknown dataType');
  153. }
  154. this._trigger('add', {items: items}, senderId);
  155. };
  156. /**
  157. * Update existing items. Items with the same id will be merged
  158. * @param {Object | Array | DataTable} data
  159. * @param {String} [senderId] Optional sender id, used to trigger events for
  160. * all but this sender's event subscribers.
  161. */
  162. DataSet.prototype.update = function (data, senderId) {
  163. var items = [],
  164. id,
  165. me = this;
  166. if (data instanceof Array) {
  167. // Array
  168. data.forEach(function (item) {
  169. var id = me._updateItem(item);
  170. items.push(id);
  171. });
  172. }
  173. else if (util.isDataTable(data)) {
  174. // Google DataTable
  175. var columns = this._getColumnNames(data);
  176. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  177. var item = {};
  178. columns.forEach(function (field, col) {
  179. item[field] = data.getValue(row, col);
  180. });
  181. id = me._updateItem(item);
  182. items.push(id);
  183. }
  184. }
  185. else if (data instanceof Object) {
  186. // Single item
  187. id = me._updateItem(data);
  188. items.push(id);
  189. }
  190. else {
  191. throw new Error('Unknown dataType');
  192. }
  193. this._trigger('update', {items: items}, senderId);
  194. };
  195. /**
  196. * Get a data item or multiple items
  197. * @param {String | Number | Array | Object} [ids] Id of a single item, or an
  198. * array with multiple id's, or
  199. * undefined or an Object with options
  200. * to retrieve all data.
  201. * @param {Object} [options] Available options:
  202. * {String} [type]
  203. * 'DataTable' or 'Array' (default)
  204. * {Object.<String, String>} [fieldTypes]
  205. * {String[]} [fields] filter fields
  206. * @param {Array | DataTable} [data] If provided, items will be appended
  207. * to this array or table. Required
  208. * in case of Google DataTable
  209. * @return {Array | Object | DataTable | null} data
  210. * @throws Error
  211. */
  212. DataSet.prototype.get = function (ids, options, data) {
  213. var me = this;
  214. // shift arguments when first argument contains the options
  215. if (util.getType(ids) == 'Object') {
  216. data = options;
  217. options = ids;
  218. ids = undefined;
  219. }
  220. // merge field types
  221. var fieldTypes = {};
  222. if (this.options && this.options.fieldTypes) {
  223. util.forEach(this.options.fieldTypes, function (value, field) {
  224. fieldTypes[field] = value;
  225. });
  226. }
  227. if (options && options.fieldTypes) {
  228. util.forEach(options.fieldTypes, function (value, field) {
  229. fieldTypes[field] = value;
  230. });
  231. }
  232. var fields = options ? options.fields : undefined;
  233. // determine the return type
  234. var type;
  235. if (options && options.type) {
  236. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  237. if (data && (type != util.getType(data))) {
  238. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  239. 'does not correspond with specified options.type (' + options.type + ')');
  240. }
  241. if (type == 'DataTable' && !util.isDataTable(data)) {
  242. throw new Error('Parameter "data" must be a DataTable ' +
  243. 'when options.type is "DataTable"');
  244. }
  245. }
  246. else if (data) {
  247. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  248. }
  249. else {
  250. type = 'Array';
  251. }
  252. if (type == 'DataTable') {
  253. // return a Google DataTable
  254. var columns = this._getColumnNames(data);
  255. if (ids == undefined) {
  256. // return all data
  257. util.forEach(this.data, function (item) {
  258. me._appendRow(data, columns, me._castItem(item));
  259. });
  260. }
  261. else if (util.isNumber(ids) || util.isString(ids)) {
  262. var item = me._castItem(me.data[ids], fieldTypes, fields);
  263. this._appendRow(data, columns, item);
  264. }
  265. else if (ids instanceof Array) {
  266. ids.forEach(function (id) {
  267. var item = me._castItem(me.data[id], fieldTypes, fields);
  268. me._appendRow(data, columns, item);
  269. });
  270. }
  271. else {
  272. throw new TypeError('Parameter "ids" must be ' +
  273. 'undefined, a String, Number, or Array');
  274. }
  275. }
  276. else {
  277. // return an array
  278. data = data || [];
  279. if (ids == undefined) {
  280. // return all data
  281. util.forEach(this.data, function (item) {
  282. data.push(me._castItem(item, fieldTypes, fields));
  283. });
  284. }
  285. else if (util.isNumber(ids) || util.isString(ids)) {
  286. // return a single item
  287. return this._castItem(me.data[ids], fieldTypes, fields);
  288. }
  289. else if (ids instanceof Array) {
  290. ids.forEach(function (id) {
  291. data.push(me._castItem(me.data[id], fieldTypes, fields));
  292. });
  293. }
  294. else {
  295. throw new TypeError('Parameter "ids" must be ' +
  296. 'undefined, a String, Number, or Array');
  297. }
  298. }
  299. return data;
  300. };
  301. /**
  302. * Remove an object by pointer or by id
  303. * @param {String | Number | Object | Array} id Object or id, or an array with
  304. * objects or ids to be removed
  305. * @param {String} [senderId] Optional sender id, used to trigger events for
  306. * all but this sender's event subscribers.
  307. */
  308. DataSet.prototype.remove = function (id, senderId) {
  309. var items = [],
  310. me = this;
  311. if (util.isNumber(id) || util.isString(id)) {
  312. delete this.data[id];
  313. delete this.internalIds[id];
  314. items.push(id);
  315. }
  316. else if (id instanceof Array) {
  317. id.forEach(function (id) {
  318. me.remove(id);
  319. });
  320. items = items.concat(id);
  321. }
  322. else if (id instanceof Object) {
  323. // search for the object
  324. for (var i in this.data) {
  325. if (this.data.hasOwnProperty(i)) {
  326. if (this.data[i] == id) {
  327. delete this.data[i];
  328. delete this.internalIds[i];
  329. items.push(i);
  330. }
  331. }
  332. }
  333. }
  334. this._trigger('remove', {items: items}, senderId);
  335. };
  336. /**
  337. * Clear the data
  338. * @param {String} [senderId] Optional sender id, used to trigger events for
  339. * all but this sender's event subscribers.
  340. */
  341. DataSet.prototype.clear = function (senderId) {
  342. var ids = Object.keys(this.data);
  343. this.data = {};
  344. this.internalIds = {};
  345. this._trigger('remove', {items: ids}, senderId);
  346. };
  347. /**
  348. * Find the item with maximum value of a specified field
  349. * @param {String} field
  350. * @return {Object} item Item containing max value, or null if no items
  351. */
  352. DataSet.prototype.max = function (field) {
  353. var data = this.data,
  354. ids = Object.keys(data);
  355. var max = null;
  356. var maxField = null;
  357. ids.forEach(function (id) {
  358. var item = data[id];
  359. var itemField = item[field];
  360. if (itemField != null && (!max || itemField > maxField)) {
  361. max = item;
  362. maxField = itemField;
  363. }
  364. });
  365. return max;
  366. };
  367. /**
  368. * Find the item with minimum value of a specified field
  369. * @param {String} field
  370. */
  371. DataSet.prototype.min = function (field) {
  372. var data = this.data,
  373. ids = Object.keys(data);
  374. var min = null;
  375. var minField = null;
  376. ids.forEach(function (id) {
  377. var item = data[id];
  378. var itemField = item[field];
  379. if (itemField != null && (!min || itemField < minField)) {
  380. min = item;
  381. minField = itemField;
  382. }
  383. });
  384. return min;
  385. };
  386. /**
  387. * Add a single item
  388. * @param {Object} item
  389. * @return {String} id
  390. * @private
  391. */
  392. DataSet.prototype._addItem = function (item) {
  393. var id = item[this.fieldId];
  394. if (id == undefined) {
  395. // generate an id
  396. id = util.randomUUID();
  397. item[this.fieldId] = id;
  398. this.internalIds[id] = item;
  399. }
  400. var d = {};
  401. for (var field in item) {
  402. if (item.hasOwnProperty(field)) {
  403. var type = this.fieldTypes[field]; // type may be undefined
  404. d[field] = util.cast(item[field], type);
  405. }
  406. }
  407. this.data[id] = d;
  408. //TODO: fail when an item with this id already exists?
  409. return id;
  410. };
  411. /**
  412. * Cast and filter the fields of an item
  413. * @param {Object | undefined} item
  414. * @param {Object.<String, String>} [fieldTypes]
  415. * @param {String[]} [fields]
  416. * @return {Object | null} castedItem
  417. * @private
  418. */
  419. DataSet.prototype._castItem = function (item, fieldTypes, fields) {
  420. var clone,
  421. fieldId = this.fieldId,
  422. internalIds = this.internalIds;
  423. if (item) {
  424. clone = {};
  425. fieldTypes = fieldTypes || {};
  426. if (fields) {
  427. // output filtered fields
  428. util.forEach(item, function (value, field) {
  429. if (fields.indexOf(field) != -1) {
  430. clone[field] = util.cast(value, fieldTypes[field]);
  431. }
  432. });
  433. }
  434. else {
  435. // output all fields, except internal ids
  436. util.forEach(item, function (value, field) {
  437. if (field != fieldId || !(value in internalIds)) {
  438. clone[field] = util.cast(value, fieldTypes[field]);
  439. }
  440. });
  441. }
  442. }
  443. else {
  444. clone = null;
  445. }
  446. return clone;
  447. };
  448. /**
  449. * Update a single item: merge with existing item
  450. * @param {Object} item
  451. * @return {String} id
  452. * @private
  453. */
  454. DataSet.prototype._updateItem = function (item) {
  455. var id = item[this.fieldId];
  456. if (id == undefined) {
  457. throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
  458. }
  459. var d = this.data[id];
  460. if (d) {
  461. // merge with current item
  462. for (var field in item) {
  463. if (item.hasOwnProperty(field)) {
  464. var type = this.fieldTypes[field]; // type may be undefined
  465. d[field] = util.cast(item[field], type);
  466. }
  467. }
  468. }
  469. else {
  470. // create new item
  471. this._addItem(item);
  472. }
  473. return id;
  474. };
  475. /**
  476. * Get an array with the column names of a Google DataTable
  477. * @param {DataTable} dataTable
  478. * @return {Array} columnNames
  479. * @private
  480. */
  481. DataSet.prototype._getColumnNames = function (dataTable) {
  482. var columns = [];
  483. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  484. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  485. }
  486. return columns;
  487. };
  488. /**
  489. * Append an item as a row to the dataTable
  490. * @param dataTable
  491. * @param columns
  492. * @param item
  493. * @private
  494. */
  495. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  496. var row = dataTable.addRow();
  497. columns.forEach(function (field, col) {
  498. dataTable.setValue(row, col, item[field]);
  499. });
  500. };
  501. // exports
  502. module.exports = exports = DataSet;