UNPKG

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