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.

504 lines
14 KiB

  1. var DataSet = require('../DataSet');
  2. var DataView = require('../DataView');
  3. var Range = require('./Range');
  4. var Filter = require('./Filter');
  5. var Settings = require('./Settings');
  6. var Point3d = require('./Point3d');
  7. /**
  8. * Creates a container for all data of one specific 3D-graph.
  9. *
  10. * On construction, the container is totally empty; the data
  11. * needs to be initialized with method initializeData().
  12. * Failure to do so will result in the following exception begin thrown
  13. * on instantiation of Graph3D:
  14. *
  15. * Error: Array, DataSet, or DataView expected
  16. *
  17. * @constructor
  18. */
  19. function DataGroup() {
  20. this.dataTable = null; // The original data table
  21. }
  22. /**
  23. * Initializes the instance from the passed data.
  24. *
  25. * Calculates minimum and maximum values and column index values.
  26. *
  27. * The graph3d instance is used internally to access the settings for
  28. * the given instance.
  29. * TODO: Pass settings only instead.
  30. *
  31. * @param {vis.Graph3D} graph3d Reference to the calling Graph3D instance.
  32. * @param {Array | DataSet | DataView} rawData The data containing the items for
  33. * the Graph.
  34. * @param {Number} style Style Number
  35. * @returns {Array<Object>}
  36. */
  37. DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
  38. if (rawData === undefined) return;
  39. if (Array.isArray(rawData)) {
  40. rawData = new DataSet(rawData);
  41. }
  42. var data;
  43. if (rawData instanceof DataSet || rawData instanceof DataView) {
  44. data = rawData.get();
  45. }
  46. else {
  47. throw new Error('Array, DataSet, or DataView expected');
  48. }
  49. if (data.length == 0) return;
  50. this.style = style;
  51. // unsubscribe from the dataTable
  52. if (this.dataSet) {
  53. this.dataSet.off('*', this._onChange);
  54. }
  55. this.dataSet = rawData;
  56. this.dataTable = data;
  57. // subscribe to changes in the dataset
  58. var me = this;
  59. this._onChange = function () {
  60. graph3d.setData(me.dataSet);
  61. };
  62. this.dataSet.on('*', this._onChange);
  63. // determine the location of x,y,z,value,filter columns
  64. this.colX = 'x';
  65. this.colY = 'y';
  66. this.colZ = 'z';
  67. var withBars = graph3d.hasBars(style);
  68. // determine barWidth from data
  69. if (withBars) {
  70. if (graph3d.defaultXBarWidth !== undefined) {
  71. this.xBarWidth = graph3d.defaultXBarWidth;
  72. }
  73. else {
  74. this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
  75. }
  76. if (graph3d.defaultYBarWidth !== undefined) {
  77. this.yBarWidth = graph3d.defaultYBarWidth;
  78. }
  79. else {
  80. this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
  81. }
  82. }
  83. // calculate minima and maxima
  84. this._initializeRange(data, this.colX, graph3d, withBars);
  85. this._initializeRange(data, this.colY, graph3d, withBars);
  86. this._initializeRange(data, this.colZ, graph3d, false);
  87. if (data[0].hasOwnProperty('style')) {
  88. this.colValue = 'style';
  89. var valueRange = this.getColumnRange(data, this.colValue);
  90. this._setRangeDefaults(valueRange, graph3d.defaultValueMin, graph3d.defaultValueMax);
  91. this.valueRange = valueRange;
  92. }
  93. // Initialize data filter if a filter column is provided
  94. var table = this.getDataTable();
  95. if (table[0].hasOwnProperty('filter')) {
  96. if (this.dataFilter === undefined) {
  97. this.dataFilter = new Filter(this, 'filter', graph3d);
  98. this.dataFilter.setOnLoadCallback(function() { graph3d.redraw(); });
  99. }
  100. }
  101. var dataPoints;
  102. if (this.dataFilter) {
  103. // apply filtering
  104. dataPoints = this.dataFilter._getDataPoints();
  105. }
  106. else {
  107. // no filtering. load all data
  108. dataPoints = this._getDataPoints(this.getDataTable());
  109. }
  110. return dataPoints;
  111. };
  112. /**
  113. * Collect the range settings for the given data column.
  114. *
  115. * This internal method is intended to make the range
  116. * initalization more generic.
  117. *
  118. * TODO: if/when combined settings per axis defined, get rid of this.
  119. *
  120. * @private
  121. *
  122. * @param {'x'|'y'|'z'} column The data column to process
  123. * @param {vis.Graph3D} graph3d Reference to the calling Graph3D instance;
  124. * required for access to settings
  125. * @returns {Object}
  126. */
  127. DataGroup.prototype._collectRangeSettings = function(column, graph3d) {
  128. var index = ['x', 'y', 'z'].indexOf(column);
  129. if (index == -1) {
  130. throw new Error('Column \'' + column + '\' invalid');
  131. }
  132. var upper = column.toUpperCase();
  133. return {
  134. barWidth : this[column + 'BarWidth'],
  135. min : graph3d['default' + upper + 'Min'],
  136. max : graph3d['default' + upper + 'Max'],
  137. step : graph3d['default' + upper + 'Step'],
  138. range_label: column + 'Range', // Name of instance field to write to
  139. step_label : column + 'Step' // Name of instance field to write to
  140. };
  141. };
  142. /**
  143. * Initializes the settings per given column.
  144. *
  145. * TODO: if/when combined settings per axis defined, rewrite this.
  146. *
  147. * @private
  148. *
  149. * @param {DataSet | DataView} data The data containing the items for the Graph
  150. * @param {'x'|'y'|'z'} column The data column to process
  151. * @param {Graph3D} graph3d Reference to the calling Graph3D instance;
  152. * required for access to settings
  153. * @param {Boolean} withBars True if initializing for bar graph
  154. */
  155. DataGroup.prototype._initializeRange = function(data, column, graph3d, withBars) {
  156. var NUMSTEPS = 5;
  157. var settings = this._collectRangeSettings(column, graph3d);
  158. var range = this.getColumnRange(data, column);
  159. if (withBars && column != 'z') { // Safeguard for 'z'; it doesn't have a bar width
  160. range.expand(settings.barWidth / 2);
  161. }
  162. this._setRangeDefaults(range, settings.min, settings.max);
  163. this[settings.range_label] = range;
  164. this[settings.step_label ] = (settings.step !== undefined) ? settings.step : range.range()/NUMSTEPS;
  165. }
  166. /**
  167. * Creates a list with all the different values in the data for the given column.
  168. *
  169. * If no data passed, use the internal data of this instance.
  170. *
  171. * @param {'x'|'y'|'z'} column The data column to process
  172. * @param {DataSet|DataView|undefined} data The data containing the items for the Graph
  173. *
  174. * @returns {Array} All distinct values in the given column data, sorted ascending.
  175. */
  176. DataGroup.prototype.getDistinctValues = function(column, data) {
  177. if (data === undefined) {
  178. data = this.dataTable;
  179. }
  180. var values = [];
  181. for (var i = 0; i < data.length; i++) {
  182. var value = data[i][column] || 0;
  183. if (values.indexOf(value) === -1) {
  184. values.push(value);
  185. }
  186. }
  187. return values.sort(function(a,b) { return a - b; });
  188. };
  189. /**
  190. * Determine the smallest difference between the values for given
  191. * column in the passed data set.
  192. *
  193. * @param {DataSet|DataView|undefined} data The data containing the items for the Graph
  194. * @param {'x'|'y'|'z'} column The data column to process
  195. *
  196. * @returns {Number|null} Smallest difference value or
  197. * null, if it can't be determined.
  198. */
  199. DataGroup.prototype.getSmallestDifference = function(data, column) {
  200. var values = this.getDistinctValues(data, column);
  201. // Get all the distinct diffs
  202. // Array values is assumed to be sorted here
  203. var smallest_diff = null;
  204. for (var i = 1; i < values.length; i++) {
  205. var diff = values[i] - values[i - 1];
  206. if (smallest_diff == null || smallest_diff > diff ) {
  207. smallest_diff = diff;
  208. }
  209. }
  210. return smallest_diff;
  211. }
  212. /**
  213. * Get the absolute min/max values for the passed data column.
  214. *
  215. * @param {DataSet|DataView|undefined} data The data containing the items for the Graph
  216. * @param {'x'|'y'|'z'} column The data column to process
  217. *
  218. * @returns {Range} A Range instance with min/max members properly set.
  219. */
  220. DataGroup.prototype.getColumnRange = function(data, column) {
  221. var range = new Range();
  222. // Adjust the range so that it covers all values in the passed data elements.
  223. for (var i = 0; i < data.length; i++) {
  224. var item = data[i][column];
  225. range.adjust(item);
  226. }
  227. return range;
  228. };
  229. /**
  230. * Determines the number of rows in the current data.
  231. *
  232. * @returns {Number}
  233. */
  234. DataGroup.prototype.getNumberOfRows = function() {
  235. return this.dataTable.length;
  236. };
  237. /**
  238. * Set default values for range
  239. *
  240. * The default values override the range values, if defined.
  241. *
  242. * Because it's possible that only defaultMin or defaultMax is set, it's better
  243. * to pass in a range already set with the min/max set from the data. Otherwise,
  244. * it's quite hard to process the min/max properly.
  245. *
  246. * @param {vis.Range} range
  247. * @param {number} [defaultMin=range.min]
  248. * @param {number} [defaultMax=range.max]
  249. * @private
  250. */
  251. DataGroup.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
  252. if (defaultMin !== undefined) {
  253. range.min = defaultMin;
  254. }
  255. if (defaultMax !== undefined) {
  256. range.max = defaultMax;
  257. }
  258. // This is the original way that the default min/max values were adjusted.
  259. // TODO: Perhaps it's better if an error is thrown if the values do not agree.
  260. // But this will change the behaviour.
  261. if (range.max <= range.min) range.max = range.min + 1;
  262. };
  263. DataGroup.prototype.getDataTable = function() {
  264. return this.dataTable;
  265. };
  266. DataGroup.prototype.getDataSet = function() {
  267. return this.dataSet;
  268. };
  269. /**
  270. * Return all data values as a list of Point3d objects
  271. * @param {Array<Object>} data
  272. * @returns {Array<Object>}
  273. */
  274. DataGroup.prototype.getDataPoints = function(data) {
  275. var dataPoints = [];
  276. for (var i = 0; i < data.length; i++) {
  277. var point = new Point3d();
  278. point.x = data[i][this.colX] || 0;
  279. point.y = data[i][this.colY] || 0;
  280. point.z = data[i][this.colZ] || 0;
  281. point.data = data[i];
  282. if (this.colValue !== undefined) {
  283. point.value = data[i][this.colValue] || 0;
  284. }
  285. var obj = {};
  286. obj.point = point;
  287. obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
  288. obj.trans = undefined;
  289. obj.screen = undefined;
  290. dataPoints.push(obj);
  291. }
  292. return dataPoints;
  293. };
  294. /**
  295. * Copy all values from the data table to a matrix.
  296. *
  297. * The provided values are supposed to form a grid of (x,y) positions.
  298. * @param {Array<Object>} data
  299. * @returns {Array<Object>}
  300. * @private
  301. */
  302. DataGroup.prototype.initDataAsMatrix = function(data) {
  303. // TODO: store the created matrix dataPoints in the filters instead of
  304. // reloading each time.
  305. var x, y, i, obj;
  306. // create two lists with all present x and y values
  307. var dataX = this.getDistinctValues(this.colX, data);
  308. var dataY = this.getDistinctValues(this.colY, data);
  309. var dataPoints = this.getDataPoints(data);
  310. // create a grid, a 2d matrix, with all values.
  311. var dataMatrix = []; // temporary data matrix
  312. for (i = 0; i < dataPoints.length; i++) {
  313. obj = dataPoints[i];
  314. // TODO: implement Array().indexOf() for Internet Explorer
  315. var xIndex = dataX.indexOf(obj.point.x);
  316. var yIndex = dataY.indexOf(obj.point.y);
  317. if (dataMatrix[xIndex] === undefined) {
  318. dataMatrix[xIndex] = [];
  319. }
  320. dataMatrix[xIndex][yIndex] = obj;
  321. }
  322. // fill in the pointers to the neighbors.
  323. for (x = 0; x < dataMatrix.length; x++) {
  324. for (y = 0; y < dataMatrix[x].length; y++) {
  325. if (dataMatrix[x][y]) {
  326. dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
  327. dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
  328. dataMatrix[x][y].pointCross =
  329. (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
  330. dataMatrix[x+1][y+1] :
  331. undefined;
  332. }
  333. }
  334. }
  335. return dataPoints;
  336. };
  337. /**
  338. * Return common information, if present
  339. *
  340. * @returns {string}
  341. */
  342. DataGroup.prototype.getInfo = function() {
  343. var dataFilter = this.dataFilter;
  344. if (!dataFilter) return undefined;
  345. return dataFilter.getLabel() + ': ' + dataFilter.getSelectedValue();
  346. };
  347. /**
  348. * Reload the data
  349. */
  350. DataGroup.prototype.reload = function() {
  351. if (this.dataTable) {
  352. this.setData(this.dataTable);
  353. }
  354. };
  355. /**
  356. * Filter the data based on the current filter
  357. *
  358. * @param {Array} data
  359. * @returns {Array} dataPoints Array with point objects which can be drawn on
  360. * screen
  361. */
  362. DataGroup.prototype._getDataPoints = function (data) {
  363. var dataPoints = [];
  364. if (this.style === Settings.STYLE.GRID || this.style === Settings.STYLE.SURFACE) {
  365. dataPoints = this.initDataAsMatrix(data);
  366. }
  367. else { // 'dot', 'dot-line', etc.
  368. this._checkValueField(data);
  369. dataPoints = this.getDataPoints(data);
  370. if (this.style === Settings.STYLE.LINE) {
  371. // Add next member points for line drawing
  372. for (var i = 0; i < dataPoints.length; i++) {
  373. if (i > 0) {
  374. dataPoints[i - 1].pointNext = dataPoints[i];
  375. }
  376. }
  377. }
  378. }
  379. return dataPoints;
  380. };
  381. /**
  382. * Check if the state is consistent for the use of the value field.
  383. *
  384. * Throws if a problem is detected.
  385. *
  386. * @param {Array<Object>} data
  387. * @private
  388. */
  389. DataGroup.prototype._checkValueField = function (data) {
  390. var hasValueField = this.style === Settings.STYLE.BARCOLOR
  391. || this.style === Settings.STYLE.BARSIZE
  392. || this.style === Settings.STYLE.DOTCOLOR
  393. || this.style === Settings.STYLE.DOTSIZE;
  394. if (!hasValueField) {
  395. return; // No need to check further
  396. }
  397. // Following field must be present for the current graph style
  398. if (this.colValue === undefined) {
  399. throw new Error('Expected data to have '
  400. + ' field \'style\' '
  401. + ' for graph style \'' + this.style + '\''
  402. );
  403. }
  404. // The data must also contain this field.
  405. // Note that only first data element is checked.
  406. if (data[0][this.colValue] === undefined) {
  407. throw new Error('Expected data to have '
  408. + ' field \'' + this.colValue + '\' '
  409. + ' for graph style \'' + this.style + '\''
  410. );
  411. }
  412. };
  413. module.exports = DataGroup;