vis-rails 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +2 -0
  4. data/lib/vis/rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
  6. data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
  7. data/vendor/assets/javascripts/vis.js +3 -3
  8. data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
  9. data/vendor/assets/vis/DataSet.js +106 -130
  10. data/vendor/assets/vis/DataView.js +35 -37
  11. data/vendor/assets/vis/graph/Edge.js +225 -45
  12. data/vendor/assets/vis/graph/Graph.js +120 -24
  13. data/vendor/assets/vis/graph/Node.js +16 -16
  14. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
  15. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
  16. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
  17. data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
  18. data/vendor/assets/vis/module/exports.js +2 -3
  19. data/vendor/assets/vis/timeline/Range.js +93 -80
  20. data/vendor/assets/vis/timeline/Timeline.js +525 -428
  21. data/vendor/assets/vis/timeline/component/Component.js +19 -53
  22. data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
  23. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
  24. data/vendor/assets/vis/timeline/component/Group.js +47 -50
  25. data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
  26. data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
  27. data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
  28. data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
  29. data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
  30. data/vendor/assets/vis/timeline/component/css/item.css +1 -11
  31. data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
  32. data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
  33. data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
  34. data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
  35. data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
  36. data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
  37. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
  38. data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
  39. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
  40. data/vendor/assets/vis/timeline/stack.js +5 -5
  41. data/vendor/assets/vis/util.js +81 -35
  42. metadata +7 -4
  43. data/vendor/assets/vis/timeline/component/Panel.js +0 -170
  44. data/vendor/assets/vis/timeline/component/RootPanel.js +0 -176
@@ -137,7 +137,21 @@ var SelectionMixin = {
137
137
  else {
138
138
  this.selectionObj.edges[obj.id] = obj;
139
139
  }
140
+ },
140
141
 
142
+ /**
143
+ * Add object to the selection array.
144
+ *
145
+ * @param obj
146
+ * @private
147
+ */
148
+ _addToHover : function(obj) {
149
+ if (obj instanceof Node) {
150
+ this.hoverObj.nodes[obj.id] = obj;
151
+ }
152
+ else {
153
+ this.hoverObj.edges[obj.id] = obj;
154
+ }
141
155
  },
142
156
 
143
157
 
@@ -156,7 +170,6 @@ var SelectionMixin = {
156
170
  }
157
171
  },
158
172
 
159
-
160
173
  /**
161
174
  * Unselect all. The selectionObj is useful for this.
162
175
  *
@@ -228,7 +241,7 @@ var SelectionMixin = {
228
241
  },
229
242
 
230
243
  /**
231
- * return the number of selected nodes
244
+ * return the selected node
232
245
  *
233
246
  * @returns {number}
234
247
  * @private
@@ -242,6 +255,21 @@ var SelectionMixin = {
242
255
  return null;
243
256
  },
244
257
 
258
+ /**
259
+ * return the selected edge
260
+ *
261
+ * @returns {number}
262
+ * @private
263
+ */
264
+ _getSelectedEdge : function() {
265
+ for (var edgeId in this.selectionObj.edges) {
266
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
267
+ return this.selectionObj.edges[edgeId];
268
+ }
269
+ }
270
+ return null;
271
+ },
272
+
245
273
 
246
274
  /**
247
275
  * return the number of selected edges
@@ -333,6 +361,20 @@ var SelectionMixin = {
333
361
  }
334
362
  },
335
363
 
364
+ /**
365
+ * select the edges connected to the node that is being selected
366
+ *
367
+ * @param {Node} node
368
+ * @private
369
+ */
370
+ _hoverConnectedEdges : function(node) {
371
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
372
+ var edge = node.dynamicEdges[i];
373
+ edge.hover = true;
374
+ this._addToHover(edge);
375
+ }
376
+ },
377
+
336
378
 
337
379
  /**
338
380
  * unselect the edges connected to the node that is being selected
@@ -350,6 +392,7 @@ var SelectionMixin = {
350
392
 
351
393
 
352
394
 
395
+
353
396
  /**
354
397
  * This is called when someone clicks on a node. either select or deselect it.
355
398
  * If there is an existing selection and we don't want to append to it, clear the existing selection
@@ -379,12 +422,48 @@ var SelectionMixin = {
379
422
  object.unselect();
380
423
  this._removeFromSelection(object);
381
424
  }
425
+
382
426
  if (doNotTrigger == false) {
383
427
  this.emit('select', this.getSelection());
384
428
  }
385
429
  },
386
430
 
387
431
 
432
+ /**
433
+ * This is called when someone clicks on a node. either select or deselect it.
434
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
435
+ *
436
+ * @param {Node || Edge} object
437
+ * @private
438
+ */
439
+ _blurObject : function(object) {
440
+ if (object.hover == true) {
441
+ object.hover = false;
442
+ this.emit("blurNode",{node:object.id});
443
+ }
444
+ },
445
+
446
+ /**
447
+ * This is called when someone clicks on a node. either select or deselect it.
448
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
449
+ *
450
+ * @param {Node || Edge} object
451
+ * @private
452
+ */
453
+ _hoverObject : function(object) {
454
+ if (object.hover == false) {
455
+ object.hover = true;
456
+ this._addToHover(object);
457
+ if (object instanceof Node) {
458
+ this.emit("hoverNode",{node:object.id});
459
+ }
460
+ }
461
+ if (object instanceof Node) {
462
+ this._hoverConnectedEdges(object);
463
+ }
464
+ },
465
+
466
+
388
467
  /**
389
468
  * handles the selection part of the touch, only for navigation controls elements;
390
469
  * Touch is triggered before tap, also before hold. Hold triggers after a while.
@@ -394,7 +473,6 @@ var SelectionMixin = {
394
473
  * @private
395
474
  */
396
475
  _handleTouch : function(pointer) {
397
-
398
476
  },
399
477
 
400
478
 
@@ -0,0 +1,3306 @@
1
+ /**
2
+ * @constructor Graph3d
3
+ * The Graph is a visualization Graphs on a time line
4
+ *
5
+ * Graph is developed in javascript as a Google Visualization Chart.
6
+ *
7
+ * @param {Element} container The DOM element in which the Graph will
8
+ * be created. Normally a div element.
9
+ * @param {DataSet | DataView | Array} [data]
10
+ * @param {Object} [options]
11
+ */
12
+ function Graph3d(container, data, options) {
13
+ // create variables and set default values
14
+ this.containerElement = container;
15
+ this.width = '400px';
16
+ this.height = '400px';
17
+ this.margin = 10; // px
18
+ this.defaultXCenter = '55%';
19
+ this.defaultYCenter = '50%';
20
+
21
+ this.xLabel = 'x';
22
+ this.yLabel = 'y';
23
+ this.zLabel = 'z';
24
+ this.filterLabel = 'time';
25
+ this.legendLabel = 'value';
26
+
27
+ this.style = Graph3d.STYLE.DOT;
28
+ this.showPerspective = true;
29
+ this.showGrid = true;
30
+ this.keepAspectRatio = true;
31
+ this.showShadow = false;
32
+ this.showGrayBottom = false; // TODO: this does not work correctly
33
+ this.showTooltip = false;
34
+ this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
35
+
36
+ this.animationInterval = 1000; // milliseconds
37
+ this.animationPreload = false;
38
+
39
+ this.camera = new Graph3d.Camera();
40
+ this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
41
+
42
+ this.dataTable = null; // The original data table
43
+ this.dataPoints = null; // The table with point objects
44
+
45
+ // the column indexes
46
+ this.colX = undefined;
47
+ this.colY = undefined;
48
+ this.colZ = undefined;
49
+ this.colValue = undefined;
50
+ this.colFilter = undefined;
51
+
52
+ this.xMin = 0;
53
+ this.xStep = undefined; // auto by default
54
+ this.xMax = 1;
55
+ this.yMin = 0;
56
+ this.yStep = undefined; // auto by default
57
+ this.yMax = 1;
58
+ this.zMin = 0;
59
+ this.zStep = undefined; // auto by default
60
+ this.zMax = 1;
61
+ this.valueMin = 0;
62
+ this.valueMax = 1;
63
+ this.xBarWidth = 1;
64
+ this.yBarWidth = 1;
65
+ // TODO: customize axis range
66
+
67
+ // constants
68
+ this.colorAxis = '#4D4D4D';
69
+ this.colorGrid = '#D3D3D3';
70
+ this.colorDot = '#7DC1FF';
71
+ this.colorDotBorder = '#3267D2';
72
+
73
+ // create a frame and canvas
74
+ this.create();
75
+
76
+ // apply options (also when undefined)
77
+ this.setOptions(options);
78
+
79
+ // apply data
80
+ if (data) {
81
+ this.setData(data);
82
+ }
83
+ }
84
+
85
+ // Extend Graph with an Emitter mixin
86
+ Emitter(Graph3d.prototype);
87
+
88
+ /**
89
+ * @class Camera
90
+ * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
91
+ * The camera is always looking in the direction of the origin of the arm.
92
+ * This way, the camera always rotates around one fixed point, the location
93
+ * of the camera arm.
94
+ *
95
+ * Documentation:
96
+ * http://en.wikipedia.org/wiki/3D_projection
97
+ */
98
+ Graph3d.Camera = function () {
99
+ this.armLocation = new Point3d();
100
+ this.armRotation = {};
101
+ this.armRotation.horizontal = 0;
102
+ this.armRotation.vertical = 0;
103
+ this.armLength = 1.7;
104
+
105
+ this.cameraLocation = new Point3d();
106
+ this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
107
+
108
+ this.calculateCameraOrientation();
109
+ };
110
+
111
+ /**
112
+ * Set the location (origin) of the arm
113
+ * @param {Number} x Normalized value of x
114
+ * @param {Number} y Normalized value of y
115
+ * @param {Number} z Normalized value of z
116
+ */
117
+ Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
118
+ this.armLocation.x = x;
119
+ this.armLocation.y = y;
120
+ this.armLocation.z = z;
121
+
122
+ this.calculateCameraOrientation();
123
+ };
124
+
125
+ /**
126
+ * Set the rotation of the camera arm
127
+ * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
128
+ * Optional, can be left undefined.
129
+ * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
130
+ * if vertical=0.5*PI, the graph is shown from the
131
+ * top. Optional, can be left undefined.
132
+ */
133
+ Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
134
+ if (horizontal !== undefined) {
135
+ this.armRotation.horizontal = horizontal;
136
+ }
137
+
138
+ if (vertical !== undefined) {
139
+ this.armRotation.vertical = vertical;
140
+ if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
141
+ if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
142
+ }
143
+
144
+ if (horizontal !== undefined || vertical !== undefined) {
145
+ this.calculateCameraOrientation();
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Retrieve the current arm rotation
151
+ * @return {object} An object with parameters horizontal and vertical
152
+ */
153
+ Graph3d.Camera.prototype.getArmRotation = function() {
154
+ var rot = {};
155
+ rot.horizontal = this.armRotation.horizontal;
156
+ rot.vertical = this.armRotation.vertical;
157
+
158
+ return rot;
159
+ };
160
+
161
+ /**
162
+ * Set the (normalized) length of the camera arm.
163
+ * @param {Number} length A length between 0.71 and 5.0
164
+ */
165
+ Graph3d.Camera.prototype.setArmLength = function(length) {
166
+ if (length === undefined)
167
+ return;
168
+
169
+ this.armLength = length;
170
+
171
+ // Radius must be larger than the corner of the graph,
172
+ // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
173
+ // graph
174
+ if (this.armLength < 0.71) this.armLength = 0.71;
175
+ if (this.armLength > 5.0) this.armLength = 5.0;
176
+
177
+ this.calculateCameraOrientation();
178
+ };
179
+
180
+ /**
181
+ * Retrieve the arm length
182
+ * @return {Number} length
183
+ */
184
+ Graph3d.Camera.prototype.getArmLength = function() {
185
+ return this.armLength;
186
+ };
187
+
188
+ /**
189
+ * Retrieve the camera location
190
+ * @return {Point3d} cameraLocation
191
+ */
192
+ Graph3d.Camera.prototype.getCameraLocation = function() {
193
+ return this.cameraLocation;
194
+ };
195
+
196
+ /**
197
+ * Retrieve the camera rotation
198
+ * @return {Point3d} cameraRotation
199
+ */
200
+ Graph3d.Camera.prototype.getCameraRotation = function() {
201
+ return this.cameraRotation;
202
+ };
203
+
204
+ /**
205
+ * Calculate the location and rotation of the camera based on the
206
+ * position and orientation of the camera arm
207
+ */
208
+ Graph3d.Camera.prototype.calculateCameraOrientation = function() {
209
+ // calculate location of the camera
210
+ this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
211
+ this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
212
+ this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
213
+
214
+ // calculate rotation of the camera
215
+ this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
216
+ this.cameraRotation.y = 0;
217
+ this.cameraRotation.z = -this.armRotation.horizontal;
218
+ };
219
+
220
+ /**
221
+ * Calculate the scaling values, dependent on the range in x, y, and z direction
222
+ */
223
+ Graph3d.prototype._setScale = function() {
224
+ this.scale = new Point3d(1 / (this.xMax - this.xMin),
225
+ 1 / (this.yMax - this.yMin),
226
+ 1 / (this.zMax - this.zMin));
227
+
228
+ // keep aspect ration between x and y scale if desired
229
+ if (this.keepAspectRatio) {
230
+ if (this.scale.x < this.scale.y) {
231
+ //noinspection JSSuspiciousNameCombination
232
+ this.scale.y = this.scale.x;
233
+ }
234
+ else {
235
+ //noinspection JSSuspiciousNameCombination
236
+ this.scale.x = this.scale.y;
237
+ }
238
+ }
239
+
240
+ // scale the vertical axis
241
+ this.scale.z *= this.verticalRatio;
242
+ // TODO: can this be automated? verticalRatio?
243
+
244
+ // determine scale for (optional) value
245
+ this.scale.value = 1 / (this.valueMax - this.valueMin);
246
+
247
+ // position the camera arm
248
+ var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
249
+ var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
250
+ var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
251
+ this.camera.setArmLocation(xCenter, yCenter, zCenter);
252
+ };
253
+
254
+
255
+ /**
256
+ * Convert a 3D location to a 2D location on screen
257
+ * http://en.wikipedia.org/wiki/3D_projection
258
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
259
+ * @return {Point2d} point2d A 2D point with parameters x, y
260
+ */
261
+ Graph3d.prototype._convert3Dto2D = function(point3d) {
262
+ var translation = this._convertPointToTranslation(point3d);
263
+ return this._convertTranslationToScreen(translation);
264
+ };
265
+
266
+ /**
267
+ * Convert a 3D location its translation seen from the camera
268
+ * http://en.wikipedia.org/wiki/3D_projection
269
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
270
+ * @return {Point3d} translation A 3D point with parameters x, y, z This is
271
+ * the translation of the point, seen from the
272
+ * camera
273
+ */
274
+ Graph3d.prototype._convertPointToTranslation = function(point3d) {
275
+ var ax = point3d.x * this.scale.x,
276
+ ay = point3d.y * this.scale.y,
277
+ az = point3d.z * this.scale.z,
278
+
279
+ cx = this.camera.getCameraLocation().x,
280
+ cy = this.camera.getCameraLocation().y,
281
+ cz = this.camera.getCameraLocation().z,
282
+
283
+ // calculate angles
284
+ sinTx = Math.sin(this.camera.getCameraRotation().x),
285
+ cosTx = Math.cos(this.camera.getCameraRotation().x),
286
+ sinTy = Math.sin(this.camera.getCameraRotation().y),
287
+ cosTy = Math.cos(this.camera.getCameraRotation().y),
288
+ sinTz = Math.sin(this.camera.getCameraRotation().z),
289
+ cosTz = Math.cos(this.camera.getCameraRotation().z),
290
+
291
+ // calculate translation
292
+ dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
293
+ dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
294
+ dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
295
+
296
+ return new Point3d(dx, dy, dz);
297
+ };
298
+
299
+ /**
300
+ * Convert a translation point to a point on the screen
301
+ * @param {Point3d} translation A 3D point with parameters x, y, z This is
302
+ * the translation of the point, seen from the
303
+ * camera
304
+ * @return {Point2d} point2d A 2D point with parameters x, y
305
+ */
306
+ Graph3d.prototype._convertTranslationToScreen = function(translation) {
307
+ var ex = this.eye.x,
308
+ ey = this.eye.y,
309
+ ez = this.eye.z,
310
+ dx = translation.x,
311
+ dy = translation.y,
312
+ dz = translation.z;
313
+
314
+ // calculate position on screen from translation
315
+ var bx;
316
+ var by;
317
+ if (this.showPerspective) {
318
+ bx = (dx - ex) * (ez / dz);
319
+ by = (dy - ey) * (ez / dz);
320
+ }
321
+ else {
322
+ bx = dx * -(ez / this.camera.getArmLength());
323
+ by = dy * -(ez / this.camera.getArmLength());
324
+ }
325
+
326
+ // shift and scale the point to the center of the screen
327
+ // use the width of the graph to scale both horizontally and vertically.
328
+ return new Point2d(
329
+ this.xcenter + bx * this.frame.canvas.clientWidth,
330
+ this.ycenter - by * this.frame.canvas.clientWidth);
331
+ };
332
+
333
+ /**
334
+ * Set the background styling for the graph
335
+ * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
336
+ */
337
+ Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
338
+ var fill = 'white';
339
+ var stroke = 'gray';
340
+ var strokeWidth = 1;
341
+
342
+ if (typeof(backgroundColor) === 'string') {
343
+ fill = backgroundColor;
344
+ stroke = 'none';
345
+ strokeWidth = 0;
346
+ }
347
+ else if (typeof(backgroundColor) === 'object') {
348
+ if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
349
+ if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
350
+ if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
351
+ }
352
+ else if (backgroundColor === undefined) {
353
+ // use use defaults
354
+ }
355
+ else {
356
+ throw 'Unsupported type of backgroundColor';
357
+ }
358
+
359
+ this.frame.style.backgroundColor = fill;
360
+ this.frame.style.borderColor = stroke;
361
+ this.frame.style.borderWidth = strokeWidth + 'px';
362
+ this.frame.style.borderStyle = 'solid';
363
+ };
364
+
365
+
366
+ /// enumerate the available styles
367
+ Graph3d.STYLE = {
368
+ BAR: 0,
369
+ BARCOLOR: 1,
370
+ BARSIZE: 2,
371
+ DOT : 3,
372
+ DOTLINE : 4,
373
+ DOTCOLOR: 5,
374
+ DOTSIZE: 6,
375
+ GRID : 7,
376
+ LINE: 8,
377
+ SURFACE : 9
378
+ };
379
+
380
+ /**
381
+ * Retrieve the style index from given styleName
382
+ * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
383
+ * @return {Number} styleNumber Enumeration value representing the style, or -1
384
+ * when not found
385
+ */
386
+ Graph3d.prototype._getStyleNumber = function(styleName) {
387
+ switch (styleName) {
388
+ case 'dot': return Graph3d.STYLE.DOT;
389
+ case 'dot-line': return Graph3d.STYLE.DOTLINE;
390
+ case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
391
+ case 'dot-size': return Graph3d.STYLE.DOTSIZE;
392
+ case 'line': return Graph3d.STYLE.LINE;
393
+ case 'grid': return Graph3d.STYLE.GRID;
394
+ case 'surface': return Graph3d.STYLE.SURFACE;
395
+ case 'bar': return Graph3d.STYLE.BAR;
396
+ case 'bar-color': return Graph3d.STYLE.BARCOLOR;
397
+ case 'bar-size': return Graph3d.STYLE.BARSIZE;
398
+ }
399
+
400
+ return -1;
401
+ };
402
+
403
+ /**
404
+ * Determine the indexes of the data columns, based on the given style and data
405
+ * @param {DataSet} data
406
+ * @param {Number} style
407
+ */
408
+ Graph3d.prototype._determineColumnIndexes = function(data, style) {
409
+ if (this.style === Graph3d.STYLE.DOT ||
410
+ this.style === Graph3d.STYLE.DOTLINE ||
411
+ this.style === Graph3d.STYLE.LINE ||
412
+ this.style === Graph3d.STYLE.GRID ||
413
+ this.style === Graph3d.STYLE.SURFACE ||
414
+ this.style === Graph3d.STYLE.BAR) {
415
+ // 3 columns expected, and optionally a 4th with filter values
416
+ this.colX = 0;
417
+ this.colY = 1;
418
+ this.colZ = 2;
419
+ this.colValue = undefined;
420
+
421
+ if (data.getNumberOfColumns() > 3) {
422
+ this.colFilter = 3;
423
+ }
424
+ }
425
+ else if (this.style === Graph3d.STYLE.DOTCOLOR ||
426
+ this.style === Graph3d.STYLE.DOTSIZE ||
427
+ this.style === Graph3d.STYLE.BARCOLOR ||
428
+ this.style === Graph3d.STYLE.BARSIZE) {
429
+ // 4 columns expected, and optionally a 5th with filter values
430
+ this.colX = 0;
431
+ this.colY = 1;
432
+ this.colZ = 2;
433
+ this.colValue = 3;
434
+
435
+ if (data.getNumberOfColumns() > 4) {
436
+ this.colFilter = 4;
437
+ }
438
+ }
439
+ else {
440
+ throw 'Unknown style "' + this.style + '"';
441
+ }
442
+ };
443
+
444
+ Graph3d.prototype.getNumberOfRows = function(data) {
445
+ return data.length;
446
+ }
447
+
448
+
449
+ Graph3d.prototype.getNumberOfColumns = function(data) {
450
+ var counter = 0;
451
+ for (var column in data[0]) {
452
+ if (data[0].hasOwnProperty(column)) {
453
+ counter++;
454
+ }
455
+ }
456
+ return counter;
457
+ }
458
+
459
+
460
+ Graph3d.prototype.getDistinctValues = function(data, column) {
461
+ var distinctValues = [];
462
+ for (var i = 0; i < data.length; i++) {
463
+ if (distinctValues.indexOf(data[i][column]) == -1) {
464
+ distinctValues.push(data[i][column]);
465
+ }
466
+ }
467
+ return distinctValues;
468
+ }
469
+
470
+
471
+ Graph3d.prototype.getColumnRange = function(data,column) {
472
+ var minMax = {min:data[0][column],max:data[0][column]};
473
+ for (var i = 0; i < data.length; i++) {
474
+ if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
475
+ if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
476
+ }
477
+ return minMax;
478
+ };
479
+
480
+ /**
481
+ * Initialize the data from the data table. Calculate minimum and maximum values
482
+ * and column index values
483
+ * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
484
+ * @param {Number} style Style Number
485
+ */
486
+ Graph3d.prototype._dataInitialize = function (rawData, style) {
487
+ var me = this;
488
+
489
+ // unsubscribe from the dataTable
490
+ if (this.dataSet) {
491
+ this.dataSet.off('*', this._onChange);
492
+ }
493
+
494
+ if (rawData === undefined)
495
+ return;
496
+
497
+ if (Array.isArray(rawData)) {
498
+ rawData = new DataSet(rawData);
499
+ }
500
+
501
+ var data;
502
+ if (rawData instanceof DataSet || rawData instanceof DataView) {
503
+ data = rawData.get();
504
+ }
505
+ else {
506
+ throw new Error('Array, DataSet, or DataView expected');
507
+ }
508
+
509
+ if (data.length == 0)
510
+ return;
511
+
512
+ this.dataSet = rawData;
513
+ this.dataTable = data;
514
+
515
+ // subscribe to changes in the dataset
516
+ this._onChange = function () {
517
+ me.setData(me.dataSet);
518
+ };
519
+ this.dataSet.on('*', this._onChange);
520
+
521
+ // _determineColumnIndexes
522
+ // getNumberOfRows (points)
523
+ // getNumberOfColumns (x,y,z,v,t,t1,t2...)
524
+ // getDistinctValues (unique values?)
525
+ // getColumnRange
526
+
527
+ // determine the location of x,y,z,value,filter columns
528
+ this.colX = 'x';
529
+ this.colY = 'y';
530
+ this.colZ = 'z';
531
+ this.colValue = 'style';
532
+ this.colFilter = 'filter';
533
+
534
+
535
+
536
+ // check if a filter column is provided
537
+ if (data[0].hasOwnProperty('filter')) {
538
+ if (this.dataFilter === undefined) {
539
+ this.dataFilter = new Filter(rawData, this.colFilter, this);
540
+ this.dataFilter.setOnLoadCallback(function() {me.redraw();});
541
+ }
542
+ }
543
+
544
+
545
+ var withBars = this.style == Graph3d.STYLE.BAR ||
546
+ this.style == Graph3d.STYLE.BARCOLOR ||
547
+ this.style == Graph3d.STYLE.BARSIZE;
548
+
549
+ // determine barWidth from data
550
+ if (withBars) {
551
+ if (this.defaultXBarWidth !== undefined) {
552
+ this.xBarWidth = this.defaultXBarWidth;
553
+ }
554
+ else {
555
+ var dataX = this.getDistinctValues(data,this.colX);
556
+ this.xBarWidth = (dataX[1] - dataX[0]) || 1;
557
+ }
558
+
559
+ if (this.defaultYBarWidth !== undefined) {
560
+ this.yBarWidth = this.defaultYBarWidth;
561
+ }
562
+ else {
563
+ var dataY = this.getDistinctValues(data,this.colY);
564
+ this.yBarWidth = (dataY[1] - dataY[0]) || 1;
565
+ }
566
+ }
567
+
568
+ // calculate minimums and maximums
569
+ var xRange = this.getColumnRange(data,this.colX);
570
+ if (withBars) {
571
+ xRange.min -= this.xBarWidth / 2;
572
+ xRange.max += this.xBarWidth / 2;
573
+ }
574
+ this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
575
+ this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
576
+ if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
577
+ this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
578
+
579
+ var yRange = this.getColumnRange(data,this.colY);
580
+ if (withBars) {
581
+ yRange.min -= this.yBarWidth / 2;
582
+ yRange.max += this.yBarWidth / 2;
583
+ }
584
+ this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
585
+ this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
586
+ if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
587
+ this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
588
+
589
+ var zRange = this.getColumnRange(data,this.colZ);
590
+ this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
591
+ this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
592
+ if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
593
+ this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
594
+
595
+ if (this.colValue !== undefined) {
596
+ var valueRange = this.getColumnRange(data,this.colValue);
597
+ this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
598
+ this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
599
+ if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
600
+ }
601
+
602
+ // set the scale dependent on the ranges.
603
+ this._setScale();
604
+ };
605
+
606
+
607
+
608
+ /**
609
+ * Filter the data based on the current filter
610
+ * @param {Array} data
611
+ * @return {Array} dataPoints Array with point objects which can be drawn on screen
612
+ */
613
+ Graph3d.prototype._getDataPoints = function (data) {
614
+ // TODO: store the created matrix dataPoints in the filters instead of reloading each time
615
+ var x, y, i, z, obj, point;
616
+
617
+ var dataPoints = [];
618
+
619
+ if (this.style === Graph3d.STYLE.GRID ||
620
+ this.style === Graph3d.STYLE.SURFACE) {
621
+ // copy all values from the google data table to a matrix
622
+ // the provided values are supposed to form a grid of (x,y) positions
623
+
624
+ // create two lists with all present x and y values
625
+ var dataX = [];
626
+ var dataY = [];
627
+ for (i = 0; i < this.getNumberOfRows(data); i++) {
628
+ x = data[i][this.colX] || 0;
629
+ y = data[i][this.colY] || 0;
630
+
631
+ if (dataX.indexOf(x) === -1) {
632
+ dataX.push(x);
633
+ }
634
+ if (dataY.indexOf(y) === -1) {
635
+ dataY.push(y);
636
+ }
637
+ }
638
+
639
+ function sortNumber(a, b) {
640
+ return a - b;
641
+ }
642
+ dataX.sort(sortNumber);
643
+ dataY.sort(sortNumber);
644
+
645
+ // create a grid, a 2d matrix, with all values.
646
+ var dataMatrix = []; // temporary data matrix
647
+ for (i = 0; i < data.length; i++) {
648
+ x = data[i][this.colX] || 0;
649
+ y = data[i][this.colY] || 0;
650
+ z = data[i][this.colZ] || 0;
651
+
652
+ var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
653
+ var yIndex = dataY.indexOf(y);
654
+
655
+ if (dataMatrix[xIndex] === undefined) {
656
+ dataMatrix[xIndex] = [];
657
+ }
658
+
659
+ var point3d = new Point3d();
660
+ point3d.x = x;
661
+ point3d.y = y;
662
+ point3d.z = z;
663
+
664
+ obj = {};
665
+ obj.point = point3d;
666
+ obj.trans = undefined;
667
+ obj.screen = undefined;
668
+ obj.bottom = new Point3d(x, y, this.zMin);
669
+
670
+ dataMatrix[xIndex][yIndex] = obj;
671
+
672
+ dataPoints.push(obj);
673
+ }
674
+
675
+ // fill in the pointers to the neighbors.
676
+ for (x = 0; x < dataMatrix.length; x++) {
677
+ for (y = 0; y < dataMatrix[x].length; y++) {
678
+ if (dataMatrix[x][y]) {
679
+ dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
680
+ dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
681
+ dataMatrix[x][y].pointCross =
682
+ (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
683
+ dataMatrix[x+1][y+1] :
684
+ undefined;
685
+ }
686
+ }
687
+ }
688
+ }
689
+ else { // 'dot', 'dot-line', etc.
690
+ // copy all values from the google data table to a list with Point3d objects
691
+ for (i = 0; i < data.length; i++) {
692
+ point = new Point3d();
693
+ point.x = data[i][this.colX] || 0;
694
+ point.y = data[i][this.colY] || 0;
695
+ point.z = data[i][this.colZ] || 0;
696
+
697
+ if (this.colValue !== undefined) {
698
+ point.value = data[i][this.colValue] || 0;
699
+ }
700
+
701
+ obj = {};
702
+ obj.point = point;
703
+ obj.bottom = new Point3d(point.x, point.y, this.zMin);
704
+ obj.trans = undefined;
705
+ obj.screen = undefined;
706
+
707
+ dataPoints.push(obj);
708
+ }
709
+ }
710
+
711
+ return dataPoints;
712
+ };
713
+
714
+
715
+
716
+
717
+ /**
718
+ * Append suffix 'px' to provided value x
719
+ * @param {int} x An integer value
720
+ * @return {string} the string value of x, followed by the suffix 'px'
721
+ */
722
+ Graph3d.px = function(x) {
723
+ return x + 'px';
724
+ };
725
+
726
+
727
+ /**
728
+ * Create the main frame for the Graph3d.
729
+ * This function is executed once when a Graph3d object is created. The frame
730
+ * contains a canvas, and this canvas contains all objects like the axis and
731
+ * nodes.
732
+ */
733
+ Graph3d.prototype.create = function () {
734
+ // remove all elements from the container element.
735
+ while (this.containerElement.hasChildNodes()) {
736
+ this.containerElement.removeChild(this.containerElement.firstChild);
737
+ }
738
+
739
+ this.frame = document.createElement('div');
740
+ this.frame.style.position = 'relative';
741
+ this.frame.style.overflow = 'hidden';
742
+
743
+ // create the graph canvas (HTML canvas element)
744
+ this.frame.canvas = document.createElement( 'canvas' );
745
+ this.frame.canvas.style.position = 'relative';
746
+ this.frame.appendChild(this.frame.canvas);
747
+ //if (!this.frame.canvas.getContext) {
748
+ {
749
+ var noCanvas = document.createElement( 'DIV' );
750
+ noCanvas.style.color = 'red';
751
+ noCanvas.style.fontWeight = 'bold' ;
752
+ noCanvas.style.padding = '10px';
753
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
754
+ this.frame.canvas.appendChild(noCanvas);
755
+ }
756
+
757
+ this.frame.filter = document.createElement( 'div' );
758
+ this.frame.filter.style.position = 'absolute';
759
+ this.frame.filter.style.bottom = '0px';
760
+ this.frame.filter.style.left = '0px';
761
+ this.frame.filter.style.width = '100%';
762
+ this.frame.appendChild(this.frame.filter);
763
+
764
+ // add event listeners to handle moving and zooming the contents
765
+ var me = this;
766
+ var onmousedown = function (event) {me._onMouseDown(event);};
767
+ var ontouchstart = function (event) {me._onTouchStart(event);};
768
+ var onmousewheel = function (event) {me._onWheel(event);};
769
+ var ontooltip = function (event) {me._onTooltip(event);};
770
+ // TODO: these events are never cleaned up... can give a 'memory leakage'
771
+
772
+ G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
773
+ G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
774
+ G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
775
+ G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
776
+ G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
777
+
778
+ // add the new graph to the container element
779
+ this.containerElement.appendChild(this.frame);
780
+ };
781
+
782
+
783
+ /**
784
+ * Set a new size for the graph
785
+ * @param {string} width Width in pixels or percentage (for example '800px'
786
+ * or '50%')
787
+ * @param {string} height Height in pixels or percentage (for example '400px'
788
+ * or '30%')
789
+ */
790
+ Graph3d.prototype.setSize = function(width, height) {
791
+ this.frame.style.width = width;
792
+ this.frame.style.height = height;
793
+
794
+ this._resizeCanvas();
795
+ };
796
+
797
+ /**
798
+ * Resize the canvas to the current size of the frame
799
+ */
800
+ Graph3d.prototype._resizeCanvas = function() {
801
+ this.frame.canvas.style.width = '100%';
802
+ this.frame.canvas.style.height = '100%';
803
+
804
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
805
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
806
+
807
+ // adjust with for margin
808
+ this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
809
+ };
810
+
811
+ /**
812
+ * Start animation
813
+ */
814
+ Graph3d.prototype.animationStart = function() {
815
+ if (!this.frame.filter || !this.frame.filter.slider)
816
+ throw 'No animation available';
817
+
818
+ this.frame.filter.slider.play();
819
+ };
820
+
821
+
822
+ /**
823
+ * Stop animation
824
+ */
825
+ Graph3d.prototype.animationStop = function() {
826
+ if (!this.frame.filter || !this.frame.filter.slider) return;
827
+
828
+ this.frame.filter.slider.stop();
829
+ };
830
+
831
+
832
+ /**
833
+ * Resize the center position based on the current values in this.defaultXCenter
834
+ * and this.defaultYCenter (which are strings with a percentage or a value
835
+ * in pixels). The center positions are the variables this.xCenter
836
+ * and this.yCenter
837
+ */
838
+ Graph3d.prototype._resizeCenter = function() {
839
+ // calculate the horizontal center position
840
+ if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
841
+ this.xcenter =
842
+ parseFloat(this.defaultXCenter) / 100 *
843
+ this.frame.canvas.clientWidth;
844
+ }
845
+ else {
846
+ this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
847
+ }
848
+
849
+ // calculate the vertical center position
850
+ if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
851
+ this.ycenter =
852
+ parseFloat(this.defaultYCenter) / 100 *
853
+ (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
854
+ }
855
+ else {
856
+ this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
857
+ }
858
+ };
859
+
860
+ /**
861
+ * Set the rotation and distance of the camera
862
+ * @param {Object} pos An object with the camera position. The object
863
+ * contains three parameters:
864
+ * - horizontal {Number}
865
+ * The horizontal rotation, between 0 and 2*PI.
866
+ * Optional, can be left undefined.
867
+ * - vertical {Number}
868
+ * The vertical rotation, between 0 and 0.5*PI
869
+ * if vertical=0.5*PI, the graph is shown from the
870
+ * top. Optional, can be left undefined.
871
+ * - distance {Number}
872
+ * The (normalized) distance of the camera to the
873
+ * center of the graph, a value between 0.71 and 5.0.
874
+ * Optional, can be left undefined.
875
+ */
876
+ Graph3d.prototype.setCameraPosition = function(pos) {
877
+ if (pos === undefined) {
878
+ return;
879
+ }
880
+
881
+ if (pos.horizontal !== undefined && pos.vertical !== undefined) {
882
+ this.camera.setArmRotation(pos.horizontal, pos.vertical);
883
+ }
884
+
885
+ if (pos.distance !== undefined) {
886
+ this.camera.setArmLength(pos.distance);
887
+ }
888
+
889
+ this.redraw();
890
+ };
891
+
892
+
893
+ /**
894
+ * Retrieve the current camera rotation
895
+ * @return {object} An object with parameters horizontal, vertical, and
896
+ * distance
897
+ */
898
+ Graph3d.prototype.getCameraPosition = function() {
899
+ var pos = this.camera.getArmRotation();
900
+ pos.distance = this.camera.getArmLength();
901
+ return pos;
902
+ };
903
+
904
+ /**
905
+ * Load data into the 3D Graph
906
+ */
907
+ Graph3d.prototype._readData = function(data) {
908
+ // read the data
909
+ this._dataInitialize(data, this.style);
910
+
911
+
912
+ if (this.dataFilter) {
913
+ // apply filtering
914
+ this.dataPoints = this.dataFilter._getDataPoints();
915
+ }
916
+ else {
917
+ // no filtering. load all data
918
+ this.dataPoints = this._getDataPoints(this.dataTable);
919
+ }
920
+
921
+ // draw the filter
922
+ this._redrawFilter();
923
+ };
924
+
925
+ /**
926
+ * Replace the dataset of the Graph3d
927
+ * @param {Array | DataSet | DataView} data
928
+ */
929
+ Graph3d.prototype.setData = function (data) {
930
+ this._readData(data);
931
+ this.redraw();
932
+
933
+ // start animation when option is true
934
+ if (this.animationAutoStart && this.dataFilter) {
935
+ this.animationStart();
936
+ }
937
+ };
938
+
939
+ /**
940
+ * Update the options. Options will be merged with current options
941
+ * @param {Object} options
942
+ */
943
+ Graph3d.prototype.setOptions = function (options) {
944
+ var cameraPosition = undefined;
945
+
946
+ this.animationStop();
947
+
948
+ if (options !== undefined) {
949
+ // retrieve parameter values
950
+ if (options.width !== undefined) this.width = options.width;
951
+ if (options.height !== undefined) this.height = options.height;
952
+
953
+ if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
954
+ if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
955
+
956
+ if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
957
+ if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
958
+ if (options.xLabel !== undefined) this.xLabel = options.xLabel;
959
+ if (options.yLabel !== undefined) this.yLabel = options.yLabel;
960
+ if (options.zLabel !== undefined) this.zLabel = options.zLabel;
961
+
962
+ if (options.style !== undefined) {
963
+ var styleNumber = this._getStyleNumber(options.style);
964
+ if (styleNumber !== -1) {
965
+ this.style = styleNumber;
966
+ }
967
+ }
968
+ if (options.showGrid !== undefined) this.showGrid = options.showGrid;
969
+ if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
970
+ if (options.showShadow !== undefined) this.showShadow = options.showShadow;
971
+ if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
972
+ if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
973
+ if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
974
+ if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
975
+
976
+ if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
977
+ if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
978
+ if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
979
+
980
+ if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
981
+ if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
982
+
983
+ if (options.xMin !== undefined) this.defaultXMin = options.xMin;
984
+ if (options.xStep !== undefined) this.defaultXStep = options.xStep;
985
+ if (options.xMax !== undefined) this.defaultXMax = options.xMax;
986
+ if (options.yMin !== undefined) this.defaultYMin = options.yMin;
987
+ if (options.yStep !== undefined) this.defaultYStep = options.yStep;
988
+ if (options.yMax !== undefined) this.defaultYMax = options.yMax;
989
+ if (options.zMin !== undefined) this.defaultZMin = options.zMin;
990
+ if (options.zStep !== undefined) this.defaultZStep = options.zStep;
991
+ if (options.zMax !== undefined) this.defaultZMax = options.zMax;
992
+ if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
993
+ if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
994
+
995
+ if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
996
+
997
+ if (cameraPosition !== undefined) {
998
+ this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
999
+ this.camera.setArmLength(cameraPosition.distance);
1000
+ }
1001
+ else {
1002
+ this.camera.setArmRotation(1.0, 0.5);
1003
+ this.camera.setArmLength(1.7);
1004
+ }
1005
+ }
1006
+
1007
+ this._setBackgroundColor(options && options.backgroundColor);
1008
+
1009
+ this.setSize(this.width, this.height);
1010
+
1011
+ // re-load the data
1012
+ if (this.dataTable) {
1013
+ this.setData(this.dataTable);
1014
+ }
1015
+
1016
+ // start animation when option is true
1017
+ if (this.animationAutoStart && this.dataFilter) {
1018
+ this.animationStart();
1019
+ }
1020
+ };
1021
+
1022
+ /**
1023
+ * Redraw the Graph.
1024
+ */
1025
+ Graph3d.prototype.redraw = function() {
1026
+ if (this.dataPoints === undefined) {
1027
+ throw 'Error: graph data not initialized';
1028
+ }
1029
+
1030
+ this._resizeCanvas();
1031
+ this._resizeCenter();
1032
+ this._redrawSlider();
1033
+ this._redrawClear();
1034
+ this._redrawAxis();
1035
+
1036
+ if (this.style === Graph3d.STYLE.GRID ||
1037
+ this.style === Graph3d.STYLE.SURFACE) {
1038
+ this._redrawDataGrid();
1039
+ }
1040
+ else if (this.style === Graph3d.STYLE.LINE) {
1041
+ this._redrawDataLine();
1042
+ }
1043
+ else if (this.style === Graph3d.STYLE.BAR ||
1044
+ this.style === Graph3d.STYLE.BARCOLOR ||
1045
+ this.style === Graph3d.STYLE.BARSIZE) {
1046
+ this._redrawDataBar();
1047
+ }
1048
+ else {
1049
+ // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
1050
+ this._redrawDataDot();
1051
+ }
1052
+
1053
+ this._redrawInfo();
1054
+ this._redrawLegend();
1055
+ };
1056
+
1057
+ /**
1058
+ * Clear the canvas before redrawing
1059
+ */
1060
+ Graph3d.prototype._redrawClear = function() {
1061
+ var canvas = this.frame.canvas;
1062
+ var ctx = canvas.getContext('2d');
1063
+
1064
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1065
+ };
1066
+
1067
+
1068
+ /**
1069
+ * Redraw the legend showing the colors
1070
+ */
1071
+ Graph3d.prototype._redrawLegend = function() {
1072
+ var y;
1073
+
1074
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
1075
+ this.style === Graph3d.STYLE.DOTSIZE) {
1076
+
1077
+ var dotSize = this.frame.clientWidth * 0.02;
1078
+
1079
+ var widthMin, widthMax;
1080
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
1081
+ widthMin = dotSize / 2; // px
1082
+ widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
1083
+ }
1084
+ else {
1085
+ widthMin = 20; // px
1086
+ widthMax = 20; // px
1087
+ }
1088
+
1089
+ var height = Math.max(this.frame.clientHeight * 0.25, 100);
1090
+ var top = this.margin;
1091
+ var right = this.frame.clientWidth - this.margin;
1092
+ var left = right - widthMax;
1093
+ var bottom = top + height;
1094
+ }
1095
+
1096
+ var canvas = this.frame.canvas;
1097
+ var ctx = canvas.getContext('2d');
1098
+ ctx.lineWidth = 1;
1099
+ ctx.font = '14px arial'; // TODO: put in options
1100
+
1101
+ if (this.style === Graph3d.STYLE.DOTCOLOR) {
1102
+ // draw the color bar
1103
+ var ymin = 0;
1104
+ var ymax = height; // Todo: make height customizable
1105
+ for (y = ymin; y < ymax; y++) {
1106
+ var f = (y - ymin) / (ymax - ymin);
1107
+
1108
+ //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
1109
+ var hue = f * 240;
1110
+ var color = this._hsv2rgb(hue, 1, 1);
1111
+
1112
+ ctx.strokeStyle = color;
1113
+ ctx.beginPath();
1114
+ ctx.moveTo(left, top + y);
1115
+ ctx.lineTo(right, top + y);
1116
+ ctx.stroke();
1117
+ }
1118
+
1119
+ ctx.strokeStyle = this.colorAxis;
1120
+ ctx.strokeRect(left, top, widthMax, height);
1121
+ }
1122
+
1123
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
1124
+ // draw border around color bar
1125
+ ctx.strokeStyle = this.colorAxis;
1126
+ ctx.fillStyle = this.colorDot;
1127
+ ctx.beginPath();
1128
+ ctx.moveTo(left, top);
1129
+ ctx.lineTo(right, top);
1130
+ ctx.lineTo(right - widthMax + widthMin, bottom);
1131
+ ctx.lineTo(left, bottom);
1132
+ ctx.closePath();
1133
+ ctx.fill();
1134
+ ctx.stroke();
1135
+ }
1136
+
1137
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
1138
+ this.style === Graph3d.STYLE.DOTSIZE) {
1139
+ // print values along the color bar
1140
+ var gridLineLen = 5; // px
1141
+ var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
1142
+ step.start();
1143
+ if (step.getCurrent() < this.valueMin) {
1144
+ step.next();
1145
+ }
1146
+ while (!step.end()) {
1147
+ y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
1148
+
1149
+ ctx.beginPath();
1150
+ ctx.moveTo(left - gridLineLen, y);
1151
+ ctx.lineTo(left, y);
1152
+ ctx.stroke();
1153
+
1154
+ ctx.textAlign = 'right';
1155
+ ctx.textBaseline = 'middle';
1156
+ ctx.fillStyle = this.colorAxis;
1157
+ ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
1158
+
1159
+ step.next();
1160
+ }
1161
+
1162
+ ctx.textAlign = 'right';
1163
+ ctx.textBaseline = 'top';
1164
+ var label = this.legendLabel;
1165
+ ctx.fillText(label, right, bottom + this.margin);
1166
+ }
1167
+ };
1168
+
1169
+ /**
1170
+ * Redraw the filter
1171
+ */
1172
+ Graph3d.prototype._redrawFilter = function() {
1173
+ this.frame.filter.innerHTML = '';
1174
+
1175
+ if (this.dataFilter) {
1176
+ var options = {
1177
+ 'visible': this.showAnimationControls
1178
+ };
1179
+ var slider = new Slider(this.frame.filter, options);
1180
+ this.frame.filter.slider = slider;
1181
+
1182
+ // TODO: css here is not nice here...
1183
+ this.frame.filter.style.padding = '10px';
1184
+ //this.frame.filter.style.backgroundColor = '#EFEFEF';
1185
+
1186
+ slider.setValues(this.dataFilter.values);
1187
+ slider.setPlayInterval(this.animationInterval);
1188
+
1189
+ // create an event handler
1190
+ var me = this;
1191
+ var onchange = function () {
1192
+ var index = slider.getIndex();
1193
+
1194
+ me.dataFilter.selectValue(index);
1195
+ me.dataPoints = me.dataFilter._getDataPoints();
1196
+
1197
+ me.redraw();
1198
+ };
1199
+ slider.setOnChangeCallback(onchange);
1200
+ }
1201
+ else {
1202
+ this.frame.filter.slider = undefined;
1203
+ }
1204
+ };
1205
+
1206
+ /**
1207
+ * Redraw the slider
1208
+ */
1209
+ Graph3d.prototype._redrawSlider = function() {
1210
+ if ( this.frame.filter.slider !== undefined) {
1211
+ this.frame.filter.slider.redraw();
1212
+ }
1213
+ };
1214
+
1215
+
1216
+ /**
1217
+ * Redraw common information
1218
+ */
1219
+ Graph3d.prototype._redrawInfo = function() {
1220
+ if (this.dataFilter) {
1221
+ var canvas = this.frame.canvas;
1222
+ var ctx = canvas.getContext('2d');
1223
+
1224
+ ctx.font = '14px arial'; // TODO: put in options
1225
+ ctx.lineStyle = 'gray';
1226
+ ctx.fillStyle = 'gray';
1227
+ ctx.textAlign = 'left';
1228
+ ctx.textBaseline = 'top';
1229
+
1230
+ var x = this.margin;
1231
+ var y = this.margin;
1232
+ ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
1233
+ }
1234
+ };
1235
+
1236
+
1237
+ /**
1238
+ * Redraw the axis
1239
+ */
1240
+ Graph3d.prototype._redrawAxis = function() {
1241
+ var canvas = this.frame.canvas,
1242
+ ctx = canvas.getContext('2d'),
1243
+ from, to, step, prettyStep,
1244
+ text, xText, yText, zText,
1245
+ offset, xOffset, yOffset,
1246
+ xMin2d, xMax2d;
1247
+
1248
+ // TODO: get the actual rendered style of the containerElement
1249
+ //ctx.font = this.containerElement.style.font;
1250
+ ctx.font = 24 / this.camera.getArmLength() + 'px arial';
1251
+
1252
+ // calculate the length for the short grid lines
1253
+ var gridLenX = 0.025 / this.scale.x;
1254
+ var gridLenY = 0.025 / this.scale.y;
1255
+ var textMargin = 5 / this.camera.getArmLength(); // px
1256
+ var armAngle = this.camera.getArmRotation().horizontal;
1257
+
1258
+ // draw x-grid lines
1259
+ ctx.lineWidth = 1;
1260
+ prettyStep = (this.defaultXStep === undefined);
1261
+ step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
1262
+ step.start();
1263
+ if (step.getCurrent() < this.xMin) {
1264
+ step.next();
1265
+ }
1266
+ while (!step.end()) {
1267
+ var x = step.getCurrent();
1268
+
1269
+ if (this.showGrid) {
1270
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
1271
+ to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
1272
+ ctx.strokeStyle = this.colorGrid;
1273
+ ctx.beginPath();
1274
+ ctx.moveTo(from.x, from.y);
1275
+ ctx.lineTo(to.x, to.y);
1276
+ ctx.stroke();
1277
+ }
1278
+ else {
1279
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
1280
+ to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
1281
+ ctx.strokeStyle = this.colorAxis;
1282
+ ctx.beginPath();
1283
+ ctx.moveTo(from.x, from.y);
1284
+ ctx.lineTo(to.x, to.y);
1285
+ ctx.stroke();
1286
+
1287
+ from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
1288
+ to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
1289
+ ctx.strokeStyle = this.colorAxis;
1290
+ ctx.beginPath();
1291
+ ctx.moveTo(from.x, from.y);
1292
+ ctx.lineTo(to.x, to.y);
1293
+ ctx.stroke();
1294
+ }
1295
+
1296
+ yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
1297
+ text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
1298
+ if (Math.cos(armAngle * 2) > 0) {
1299
+ ctx.textAlign = 'center';
1300
+ ctx.textBaseline = 'top';
1301
+ text.y += textMargin;
1302
+ }
1303
+ else if (Math.sin(armAngle * 2) < 0){
1304
+ ctx.textAlign = 'right';
1305
+ ctx.textBaseline = 'middle';
1306
+ }
1307
+ else {
1308
+ ctx.textAlign = 'left';
1309
+ ctx.textBaseline = 'middle';
1310
+ }
1311
+ ctx.fillStyle = this.colorAxis;
1312
+ ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
1313
+
1314
+ step.next();
1315
+ }
1316
+
1317
+ // draw y-grid lines
1318
+ ctx.lineWidth = 1;
1319
+ prettyStep = (this.defaultYStep === undefined);
1320
+ step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
1321
+ step.start();
1322
+ if (step.getCurrent() < this.yMin) {
1323
+ step.next();
1324
+ }
1325
+ while (!step.end()) {
1326
+ if (this.showGrid) {
1327
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
1328
+ to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
1329
+ ctx.strokeStyle = this.colorGrid;
1330
+ ctx.beginPath();
1331
+ ctx.moveTo(from.x, from.y);
1332
+ ctx.lineTo(to.x, to.y);
1333
+ ctx.stroke();
1334
+ }
1335
+ else {
1336
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
1337
+ to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
1338
+ ctx.strokeStyle = this.colorAxis;
1339
+ ctx.beginPath();
1340
+ ctx.moveTo(from.x, from.y);
1341
+ ctx.lineTo(to.x, to.y);
1342
+ ctx.stroke();
1343
+
1344
+ from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
1345
+ to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
1346
+ ctx.strokeStyle = this.colorAxis;
1347
+ ctx.beginPath();
1348
+ ctx.moveTo(from.x, from.y);
1349
+ ctx.lineTo(to.x, to.y);
1350
+ ctx.stroke();
1351
+ }
1352
+
1353
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
1354
+ text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
1355
+ if (Math.cos(armAngle * 2) < 0) {
1356
+ ctx.textAlign = 'center';
1357
+ ctx.textBaseline = 'top';
1358
+ text.y += textMargin;
1359
+ }
1360
+ else if (Math.sin(armAngle * 2) > 0){
1361
+ ctx.textAlign = 'right';
1362
+ ctx.textBaseline = 'middle';
1363
+ }
1364
+ else {
1365
+ ctx.textAlign = 'left';
1366
+ ctx.textBaseline = 'middle';
1367
+ }
1368
+ ctx.fillStyle = this.colorAxis;
1369
+ ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
1370
+
1371
+ step.next();
1372
+ }
1373
+
1374
+ // draw z-grid lines and axis
1375
+ ctx.lineWidth = 1;
1376
+ prettyStep = (this.defaultZStep === undefined);
1377
+ step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
1378
+ step.start();
1379
+ if (step.getCurrent() < this.zMin) {
1380
+ step.next();
1381
+ }
1382
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
1383
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
1384
+ while (!step.end()) {
1385
+ // TODO: make z-grid lines really 3d?
1386
+ from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
1387
+ ctx.strokeStyle = this.colorAxis;
1388
+ ctx.beginPath();
1389
+ ctx.moveTo(from.x, from.y);
1390
+ ctx.lineTo(from.x - textMargin, from.y);
1391
+ ctx.stroke();
1392
+
1393
+ ctx.textAlign = 'right';
1394
+ ctx.textBaseline = 'middle';
1395
+ ctx.fillStyle = this.colorAxis;
1396
+ ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
1397
+
1398
+ step.next();
1399
+ }
1400
+ ctx.lineWidth = 1;
1401
+ from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
1402
+ to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
1403
+ ctx.strokeStyle = this.colorAxis;
1404
+ ctx.beginPath();
1405
+ ctx.moveTo(from.x, from.y);
1406
+ ctx.lineTo(to.x, to.y);
1407
+ ctx.stroke();
1408
+
1409
+ // draw x-axis
1410
+ ctx.lineWidth = 1;
1411
+ // line at yMin
1412
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
1413
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
1414
+ ctx.strokeStyle = this.colorAxis;
1415
+ ctx.beginPath();
1416
+ ctx.moveTo(xMin2d.x, xMin2d.y);
1417
+ ctx.lineTo(xMax2d.x, xMax2d.y);
1418
+ ctx.stroke();
1419
+ // line at ymax
1420
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
1421
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
1422
+ ctx.strokeStyle = this.colorAxis;
1423
+ ctx.beginPath();
1424
+ ctx.moveTo(xMin2d.x, xMin2d.y);
1425
+ ctx.lineTo(xMax2d.x, xMax2d.y);
1426
+ ctx.stroke();
1427
+
1428
+ // draw y-axis
1429
+ ctx.lineWidth = 1;
1430
+ // line at xMin
1431
+ from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
1432
+ to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
1433
+ ctx.strokeStyle = this.colorAxis;
1434
+ ctx.beginPath();
1435
+ ctx.moveTo(from.x, from.y);
1436
+ ctx.lineTo(to.x, to.y);
1437
+ ctx.stroke();
1438
+ // line at xMax
1439
+ from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
1440
+ to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
1441
+ ctx.strokeStyle = this.colorAxis;
1442
+ ctx.beginPath();
1443
+ ctx.moveTo(from.x, from.y);
1444
+ ctx.lineTo(to.x, to.y);
1445
+ ctx.stroke();
1446
+
1447
+ // draw x-label
1448
+ var xLabel = this.xLabel;
1449
+ if (xLabel.length > 0) {
1450
+ yOffset = 0.1 / this.scale.y;
1451
+ xText = (this.xMin + this.xMax) / 2;
1452
+ yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
1453
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
1454
+ if (Math.cos(armAngle * 2) > 0) {
1455
+ ctx.textAlign = 'center';
1456
+ ctx.textBaseline = 'top';
1457
+ }
1458
+ else if (Math.sin(armAngle * 2) < 0){
1459
+ ctx.textAlign = 'right';
1460
+ ctx.textBaseline = 'middle';
1461
+ }
1462
+ else {
1463
+ ctx.textAlign = 'left';
1464
+ ctx.textBaseline = 'middle';
1465
+ }
1466
+ ctx.fillStyle = this.colorAxis;
1467
+ ctx.fillText(xLabel, text.x, text.y);
1468
+ }
1469
+
1470
+ // draw y-label
1471
+ var yLabel = this.yLabel;
1472
+ if (yLabel.length > 0) {
1473
+ xOffset = 0.1 / this.scale.x;
1474
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
1475
+ yText = (this.yMin + this.yMax) / 2;
1476
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
1477
+ if (Math.cos(armAngle * 2) < 0) {
1478
+ ctx.textAlign = 'center';
1479
+ ctx.textBaseline = 'top';
1480
+ }
1481
+ else if (Math.sin(armAngle * 2) > 0){
1482
+ ctx.textAlign = 'right';
1483
+ ctx.textBaseline = 'middle';
1484
+ }
1485
+ else {
1486
+ ctx.textAlign = 'left';
1487
+ ctx.textBaseline = 'middle';
1488
+ }
1489
+ ctx.fillStyle = this.colorAxis;
1490
+ ctx.fillText(yLabel, text.x, text.y);
1491
+ }
1492
+
1493
+ // draw z-label
1494
+ var zLabel = this.zLabel;
1495
+ if (zLabel.length > 0) {
1496
+ offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
1497
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
1498
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
1499
+ zText = (this.zMin + this.zMax) / 2;
1500
+ text = this._convert3Dto2D(new Point3d(xText, yText, zText));
1501
+ ctx.textAlign = 'right';
1502
+ ctx.textBaseline = 'middle';
1503
+ ctx.fillStyle = this.colorAxis;
1504
+ ctx.fillText(zLabel, text.x - offset, text.y);
1505
+ }
1506
+ };
1507
+
1508
+ /**
1509
+ * Calculate the color based on the given value.
1510
+ * @param {Number} H Hue, a value be between 0 and 360
1511
+ * @param {Number} S Saturation, a value between 0 and 1
1512
+ * @param {Number} V Value, a value between 0 and 1
1513
+ */
1514
+ Graph3d.prototype._hsv2rgb = function(H, S, V) {
1515
+ var R, G, B, C, Hi, X;
1516
+
1517
+ C = V * S;
1518
+ Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
1519
+ X = C * (1 - Math.abs(((H/60) % 2) - 1));
1520
+
1521
+ switch (Hi) {
1522
+ case 0: R = C; G = X; B = 0; break;
1523
+ case 1: R = X; G = C; B = 0; break;
1524
+ case 2: R = 0; G = C; B = X; break;
1525
+ case 3: R = 0; G = X; B = C; break;
1526
+ case 4: R = X; G = 0; B = C; break;
1527
+ case 5: R = C; G = 0; B = X; break;
1528
+
1529
+ default: R = 0; G = 0; B = 0; break;
1530
+ }
1531
+
1532
+ return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
1533
+ };
1534
+
1535
+
1536
+ /**
1537
+ * Draw all datapoints as a grid
1538
+ * This function can be used when the style is 'grid'
1539
+ */
1540
+ Graph3d.prototype._redrawDataGrid = function() {
1541
+ var canvas = this.frame.canvas,
1542
+ ctx = canvas.getContext('2d'),
1543
+ point, right, top, cross,
1544
+ i,
1545
+ topSideVisible, fillStyle, strokeStyle, lineWidth,
1546
+ h, s, v, zAvg;
1547
+
1548
+
1549
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1550
+ return; // TODO: throw exception?
1551
+
1552
+ // calculate the translations and screen position of all points
1553
+ for (i = 0; i < this.dataPoints.length; i++) {
1554
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1555
+ var screen = this._convertTranslationToScreen(trans);
1556
+
1557
+ this.dataPoints[i].trans = trans;
1558
+ this.dataPoints[i].screen = screen;
1559
+
1560
+ // calculate the translation of the point at the bottom (needed for sorting)
1561
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
1562
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
1563
+ }
1564
+
1565
+ // sort the points on depth of their (x,y) position (not on z)
1566
+ var sortDepth = function (a, b) {
1567
+ return b.dist - a.dist;
1568
+ };
1569
+ this.dataPoints.sort(sortDepth);
1570
+
1571
+ if (this.style === Graph3d.STYLE.SURFACE) {
1572
+ for (i = 0; i < this.dataPoints.length; i++) {
1573
+ point = this.dataPoints[i];
1574
+ right = this.dataPoints[i].pointRight;
1575
+ top = this.dataPoints[i].pointTop;
1576
+ cross = this.dataPoints[i].pointCross;
1577
+
1578
+ if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
1579
+
1580
+ if (this.showGrayBottom || this.showShadow) {
1581
+ // calculate the cross product of the two vectors from center
1582
+ // to left and right, in order to know whether we are looking at the
1583
+ // bottom or at the top side. We can also use the cross product
1584
+ // for calculating light intensity
1585
+ var aDiff = Point3d.subtract(cross.trans, point.trans);
1586
+ var bDiff = Point3d.subtract(top.trans, right.trans);
1587
+ var crossproduct = Point3d.crossProduct(aDiff, bDiff);
1588
+ var len = crossproduct.length();
1589
+ // FIXME: there is a bug with determining the surface side (shadow or colored)
1590
+
1591
+ topSideVisible = (crossproduct.z > 0);
1592
+ }
1593
+ else {
1594
+ topSideVisible = true;
1595
+ }
1596
+
1597
+ if (topSideVisible) {
1598
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1599
+ zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
1600
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
1601
+ s = 1; // saturation
1602
+
1603
+ if (this.showShadow) {
1604
+ v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
1605
+ fillStyle = this._hsv2rgb(h, s, v);
1606
+ strokeStyle = fillStyle;
1607
+ }
1608
+ else {
1609
+ v = 1;
1610
+ fillStyle = this._hsv2rgb(h, s, v);
1611
+ strokeStyle = this.colorAxis;
1612
+ }
1613
+ }
1614
+ else {
1615
+ fillStyle = 'gray';
1616
+ strokeStyle = this.colorAxis;
1617
+ }
1618
+ lineWidth = 0.5;
1619
+
1620
+ ctx.lineWidth = lineWidth;
1621
+ ctx.fillStyle = fillStyle;
1622
+ ctx.strokeStyle = strokeStyle;
1623
+ ctx.beginPath();
1624
+ ctx.moveTo(point.screen.x, point.screen.y);
1625
+ ctx.lineTo(right.screen.x, right.screen.y);
1626
+ ctx.lineTo(cross.screen.x, cross.screen.y);
1627
+ ctx.lineTo(top.screen.x, top.screen.y);
1628
+ ctx.closePath();
1629
+ ctx.fill();
1630
+ ctx.stroke();
1631
+ }
1632
+ }
1633
+ }
1634
+ else { // grid style
1635
+ for (i = 0; i < this.dataPoints.length; i++) {
1636
+ point = this.dataPoints[i];
1637
+ right = this.dataPoints[i].pointRight;
1638
+ top = this.dataPoints[i].pointTop;
1639
+
1640
+ if (point !== undefined) {
1641
+ if (this.showPerspective) {
1642
+ lineWidth = 2 / -point.trans.z;
1643
+ }
1644
+ else {
1645
+ lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
1646
+ }
1647
+ }
1648
+
1649
+ if (point !== undefined && right !== undefined) {
1650
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1651
+ zAvg = (point.point.z + right.point.z) / 2;
1652
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
1653
+
1654
+ ctx.lineWidth = lineWidth;
1655
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
1656
+ ctx.beginPath();
1657
+ ctx.moveTo(point.screen.x, point.screen.y);
1658
+ ctx.lineTo(right.screen.x, right.screen.y);
1659
+ ctx.stroke();
1660
+ }
1661
+
1662
+ if (point !== undefined && top !== undefined) {
1663
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1664
+ zAvg = (point.point.z + top.point.z) / 2;
1665
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
1666
+
1667
+ ctx.lineWidth = lineWidth;
1668
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
1669
+ ctx.beginPath();
1670
+ ctx.moveTo(point.screen.x, point.screen.y);
1671
+ ctx.lineTo(top.screen.x, top.screen.y);
1672
+ ctx.stroke();
1673
+ }
1674
+ }
1675
+ }
1676
+ };
1677
+
1678
+
1679
+ /**
1680
+ * Draw all datapoints as dots.
1681
+ * This function can be used when the style is 'dot' or 'dot-line'
1682
+ */
1683
+ Graph3d.prototype._redrawDataDot = function() {
1684
+ var canvas = this.frame.canvas;
1685
+ var ctx = canvas.getContext('2d');
1686
+ var i;
1687
+
1688
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1689
+ return; // TODO: throw exception?
1690
+
1691
+ // calculate the translations of all points
1692
+ for (i = 0; i < this.dataPoints.length; i++) {
1693
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1694
+ var screen = this._convertTranslationToScreen(trans);
1695
+ this.dataPoints[i].trans = trans;
1696
+ this.dataPoints[i].screen = screen;
1697
+
1698
+ // calculate the distance from the point at the bottom to the camera
1699
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
1700
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
1701
+ }
1702
+
1703
+ // order the translated points by depth
1704
+ var sortDepth = function (a, b) {
1705
+ return b.dist - a.dist;
1706
+ };
1707
+ this.dataPoints.sort(sortDepth);
1708
+
1709
+ // draw the datapoints as colored circles
1710
+ var dotSize = this.frame.clientWidth * 0.02; // px
1711
+ for (i = 0; i < this.dataPoints.length; i++) {
1712
+ var point = this.dataPoints[i];
1713
+
1714
+ if (this.style === Graph3d.STYLE.DOTLINE) {
1715
+ // draw a vertical line from the bottom to the graph value
1716
+ //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
1717
+ var from = this._convert3Dto2D(point.bottom);
1718
+ ctx.lineWidth = 1;
1719
+ ctx.strokeStyle = this.colorGrid;
1720
+ ctx.beginPath();
1721
+ ctx.moveTo(from.x, from.y);
1722
+ ctx.lineTo(point.screen.x, point.screen.y);
1723
+ ctx.stroke();
1724
+ }
1725
+
1726
+ // calculate radius for the circle
1727
+ var size;
1728
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
1729
+ size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
1730
+ }
1731
+ else {
1732
+ size = dotSize;
1733
+ }
1734
+
1735
+ var radius;
1736
+ if (this.showPerspective) {
1737
+ radius = size / -point.trans.z;
1738
+ }
1739
+ else {
1740
+ radius = size * -(this.eye.z / this.camera.getArmLength());
1741
+ }
1742
+ if (radius < 0) {
1743
+ radius = 0;
1744
+ }
1745
+
1746
+ var hue, color, borderColor;
1747
+ if (this.style === Graph3d.STYLE.DOTCOLOR ) {
1748
+ // calculate the color based on the value
1749
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
1750
+ color = this._hsv2rgb(hue, 1, 1);
1751
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
1752
+ }
1753
+ else if (this.style === Graph3d.STYLE.DOTSIZE) {
1754
+ color = this.colorDot;
1755
+ borderColor = this.colorDotBorder;
1756
+ }
1757
+ else {
1758
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1759
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
1760
+ color = this._hsv2rgb(hue, 1, 1);
1761
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
1762
+ }
1763
+
1764
+ // draw the circle
1765
+ ctx.lineWidth = 1.0;
1766
+ ctx.strokeStyle = borderColor;
1767
+ ctx.fillStyle = color;
1768
+ ctx.beginPath();
1769
+ ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
1770
+ ctx.fill();
1771
+ ctx.stroke();
1772
+ }
1773
+ };
1774
+
1775
+ /**
1776
+ * Draw all datapoints as bars.
1777
+ * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
1778
+ */
1779
+ Graph3d.prototype._redrawDataBar = function() {
1780
+ var canvas = this.frame.canvas;
1781
+ var ctx = canvas.getContext('2d');
1782
+ var i, j, surface, corners;
1783
+
1784
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1785
+ return; // TODO: throw exception?
1786
+
1787
+ // calculate the translations of all points
1788
+ for (i = 0; i < this.dataPoints.length; i++) {
1789
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1790
+ var screen = this._convertTranslationToScreen(trans);
1791
+ this.dataPoints[i].trans = trans;
1792
+ this.dataPoints[i].screen = screen;
1793
+
1794
+ // calculate the distance from the point at the bottom to the camera
1795
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
1796
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
1797
+ }
1798
+
1799
+ // order the translated points by depth
1800
+ var sortDepth = function (a, b) {
1801
+ return b.dist - a.dist;
1802
+ };
1803
+ this.dataPoints.sort(sortDepth);
1804
+
1805
+ // draw the datapoints as bars
1806
+ var xWidth = this.xBarWidth / 2;
1807
+ var yWidth = this.yBarWidth / 2;
1808
+ for (i = 0; i < this.dataPoints.length; i++) {
1809
+ var point = this.dataPoints[i];
1810
+
1811
+ // determine color
1812
+ var hue, color, borderColor;
1813
+ if (this.style === Graph3d.STYLE.BARCOLOR ) {
1814
+ // calculate the color based on the value
1815
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
1816
+ color = this._hsv2rgb(hue, 1, 1);
1817
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
1818
+ }
1819
+ else if (this.style === Graph3d.STYLE.BARSIZE) {
1820
+ color = this.colorDot;
1821
+ borderColor = this.colorDotBorder;
1822
+ }
1823
+ else {
1824
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1825
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
1826
+ color = this._hsv2rgb(hue, 1, 1);
1827
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
1828
+ }
1829
+
1830
+ // calculate size for the bar
1831
+ if (this.style === Graph3d.STYLE.BARSIZE) {
1832
+ xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
1833
+ yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
1834
+ }
1835
+
1836
+ // calculate all corner points
1837
+ var me = this;
1838
+ var point3d = point.point;
1839
+ var top = [
1840
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
1841
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
1842
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
1843
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
1844
+ ];
1845
+ var bottom = [
1846
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
1847
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
1848
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
1849
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
1850
+ ];
1851
+
1852
+ // calculate screen location of the points
1853
+ top.forEach(function (obj) {
1854
+ obj.screen = me._convert3Dto2D(obj.point);
1855
+ });
1856
+ bottom.forEach(function (obj) {
1857
+ obj.screen = me._convert3Dto2D(obj.point);
1858
+ });
1859
+
1860
+ // create five sides, calculate both corner points and center points
1861
+ var surfaces = [
1862
+ {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
1863
+ {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
1864
+ {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
1865
+ {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
1866
+ {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
1867
+ ];
1868
+ point.surfaces = surfaces;
1869
+
1870
+ // calculate the distance of each of the surface centers to the camera
1871
+ for (j = 0; j < surfaces.length; j++) {
1872
+ surface = surfaces[j];
1873
+ var transCenter = this._convertPointToTranslation(surface.center);
1874
+ surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
1875
+ // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
1876
+ // but the current solution is fast/simple and works in 99.9% of all cases
1877
+ // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
1878
+ }
1879
+
1880
+ // order the surfaces by their (translated) depth
1881
+ surfaces.sort(function (a, b) {
1882
+ var diff = b.dist - a.dist;
1883
+ if (diff) return diff;
1884
+
1885
+ // if equal depth, sort the top surface last
1886
+ if (a.corners === top) return 1;
1887
+ if (b.corners === top) return -1;
1888
+
1889
+ // both are equal
1890
+ return 0;
1891
+ });
1892
+
1893
+ // draw the ordered surfaces
1894
+ ctx.lineWidth = 1;
1895
+ ctx.strokeStyle = borderColor;
1896
+ ctx.fillStyle = color;
1897
+ // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
1898
+ for (j = 2; j < surfaces.length; j++) {
1899
+ surface = surfaces[j];
1900
+ corners = surface.corners;
1901
+ ctx.beginPath();
1902
+ ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
1903
+ ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
1904
+ ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
1905
+ ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
1906
+ ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
1907
+ ctx.fill();
1908
+ ctx.stroke();
1909
+ }
1910
+ }
1911
+ };
1912
+
1913
+
1914
+ /**
1915
+ * Draw a line through all datapoints.
1916
+ * This function can be used when the style is 'line'
1917
+ */
1918
+ Graph3d.prototype._redrawDataLine = function() {
1919
+ var canvas = this.frame.canvas,
1920
+ ctx = canvas.getContext('2d'),
1921
+ point, i;
1922
+
1923
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1924
+ return; // TODO: throw exception?
1925
+
1926
+ // calculate the translations of all points
1927
+ for (i = 0; i < this.dataPoints.length; i++) {
1928
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1929
+ var screen = this._convertTranslationToScreen(trans);
1930
+
1931
+ this.dataPoints[i].trans = trans;
1932
+ this.dataPoints[i].screen = screen;
1933
+ }
1934
+
1935
+ // start the line
1936
+ if (this.dataPoints.length > 0) {
1937
+ point = this.dataPoints[0];
1938
+
1939
+ ctx.lineWidth = 1; // TODO: make customizable
1940
+ ctx.strokeStyle = 'blue'; // TODO: make customizable
1941
+ ctx.beginPath();
1942
+ ctx.moveTo(point.screen.x, point.screen.y);
1943
+ }
1944
+
1945
+ // draw the datapoints as colored circles
1946
+ for (i = 1; i < this.dataPoints.length; i++) {
1947
+ point = this.dataPoints[i];
1948
+ ctx.lineTo(point.screen.x, point.screen.y);
1949
+ }
1950
+
1951
+ // finish the line
1952
+ if (this.dataPoints.length > 0) {
1953
+ ctx.stroke();
1954
+ }
1955
+ };
1956
+
1957
+ /**
1958
+ * Start a moving operation inside the provided parent element
1959
+ * @param {Event} event The event that occurred (required for
1960
+ * retrieving the mouse position)
1961
+ */
1962
+ Graph3d.prototype._onMouseDown = function(event) {
1963
+ event = event || window.event;
1964
+
1965
+ // check if mouse is still down (may be up when focus is lost for example
1966
+ // in an iframe)
1967
+ if (this.leftButtonDown) {
1968
+ this._onMouseUp(event);
1969
+ }
1970
+
1971
+ // only react on left mouse button down
1972
+ this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
1973
+ if (!this.leftButtonDown && !this.touchDown) return;
1974
+
1975
+ // get mouse position (different code for IE and all other browsers)
1976
+ this.startMouseX = getMouseX(event);
1977
+ this.startMouseY = getMouseY(event);
1978
+
1979
+ this.startStart = new Date(this.start);
1980
+ this.startEnd = new Date(this.end);
1981
+ this.startArmRotation = this.camera.getArmRotation();
1982
+
1983
+ this.frame.style.cursor = 'move';
1984
+
1985
+ // add event listeners to handle moving the contents
1986
+ // we store the function onmousemove and onmouseup in the graph, so we can
1987
+ // remove the eventlisteners lateron in the function mouseUp()
1988
+ var me = this;
1989
+ this.onmousemove = function (event) {me._onMouseMove(event);};
1990
+ this.onmouseup = function (event) {me._onMouseUp(event);};
1991
+ G3DaddEventListener(document, 'mousemove', me.onmousemove);
1992
+ G3DaddEventListener(document, 'mouseup', me.onmouseup);
1993
+ G3DpreventDefault(event);
1994
+ };
1995
+
1996
+
1997
+ /**
1998
+ * Perform moving operating.
1999
+ * This function activated from within the funcion Graph.mouseDown().
2000
+ * @param {Event} event Well, eehh, the event
2001
+ */
2002
+ Graph3d.prototype._onMouseMove = function (event) {
2003
+ event = event || window.event;
2004
+
2005
+ // calculate change in mouse position
2006
+ var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
2007
+ var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
2008
+
2009
+ var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
2010
+ var verticalNew = this.startArmRotation.vertical + diffY / 200;
2011
+
2012
+ var snapAngle = 4; // degrees
2013
+ var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
2014
+
2015
+ // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
2016
+ // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
2017
+ if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
2018
+ horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
2019
+ }
2020
+ if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
2021
+ horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
2022
+ }
2023
+
2024
+ // snap vertically to nice angles
2025
+ if (Math.abs(Math.sin(verticalNew)) < snapValue) {
2026
+ verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
2027
+ }
2028
+ if (Math.abs(Math.cos(verticalNew)) < snapValue) {
2029
+ verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
2030
+ }
2031
+
2032
+ this.camera.setArmRotation(horizontalNew, verticalNew);
2033
+ this.redraw();
2034
+
2035
+ // fire a cameraPositionChange event
2036
+ var parameters = this.getCameraPosition();
2037
+ this.emit('cameraPositionChange', parameters);
2038
+
2039
+ G3DpreventDefault(event);
2040
+ };
2041
+
2042
+
2043
+ /**
2044
+ * Stop moving operating.
2045
+ * This function activated from within the funcion Graph.mouseDown().
2046
+ * @param {event} event The event
2047
+ */
2048
+ Graph3d.prototype._onMouseUp = function (event) {
2049
+ this.frame.style.cursor = 'auto';
2050
+ this.leftButtonDown = false;
2051
+
2052
+ // remove event listeners here
2053
+ G3DremoveEventListener(document, 'mousemove', this.onmousemove);
2054
+ G3DremoveEventListener(document, 'mouseup', this.onmouseup);
2055
+ G3DpreventDefault(event);
2056
+ };
2057
+
2058
+ /**
2059
+ * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
2060
+ * @param {Event} event A mouse move event
2061
+ */
2062
+ Graph3d.prototype._onTooltip = function (event) {
2063
+ var delay = 300; // ms
2064
+ var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
2065
+ var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
2066
+
2067
+ if (!this.showTooltip) {
2068
+ return;
2069
+ }
2070
+
2071
+ if (this.tooltipTimeout) {
2072
+ clearTimeout(this.tooltipTimeout);
2073
+ }
2074
+
2075
+ // (delayed) display of a tooltip only if no mouse button is down
2076
+ if (this.leftButtonDown) {
2077
+ this._hideTooltip();
2078
+ return;
2079
+ }
2080
+
2081
+ if (this.tooltip && this.tooltip.dataPoint) {
2082
+ // tooltip is currently visible
2083
+ var dataPoint = this._dataPointFromXY(mouseX, mouseY);
2084
+ if (dataPoint !== this.tooltip.dataPoint) {
2085
+ // datapoint changed
2086
+ if (dataPoint) {
2087
+ this._showTooltip(dataPoint);
2088
+ }
2089
+ else {
2090
+ this._hideTooltip();
2091
+ }
2092
+ }
2093
+ }
2094
+ else {
2095
+ // tooltip is currently not visible
2096
+ var me = this;
2097
+ this.tooltipTimeout = setTimeout(function () {
2098
+ me.tooltipTimeout = null;
2099
+
2100
+ // show a tooltip if we have a data point
2101
+ var dataPoint = me._dataPointFromXY(mouseX, mouseY);
2102
+ if (dataPoint) {
2103
+ me._showTooltip(dataPoint);
2104
+ }
2105
+ }, delay);
2106
+ }
2107
+ };
2108
+
2109
+ /**
2110
+ * Event handler for touchstart event on mobile devices
2111
+ */
2112
+ Graph3d.prototype._onTouchStart = function(event) {
2113
+ this.touchDown = true;
2114
+
2115
+ var me = this;
2116
+ this.ontouchmove = function (event) {me._onTouchMove(event);};
2117
+ this.ontouchend = function (event) {me._onTouchEnd(event);};
2118
+ G3DaddEventListener(document, 'touchmove', me.ontouchmove);
2119
+ G3DaddEventListener(document, 'touchend', me.ontouchend);
2120
+
2121
+ this._onMouseDown(event);
2122
+ };
2123
+
2124
+ /**
2125
+ * Event handler for touchmove event on mobile devices
2126
+ */
2127
+ Graph3d.prototype._onTouchMove = function(event) {
2128
+ this._onMouseMove(event);
2129
+ };
2130
+
2131
+ /**
2132
+ * Event handler for touchend event on mobile devices
2133
+ */
2134
+ Graph3d.prototype._onTouchEnd = function(event) {
2135
+ this.touchDown = false;
2136
+
2137
+ G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
2138
+ G3DremoveEventListener(document, 'touchend', this.ontouchend);
2139
+
2140
+ this._onMouseUp(event);
2141
+ };
2142
+
2143
+
2144
+ /**
2145
+ * Event handler for mouse wheel event, used to zoom the graph
2146
+ * Code from http://adomas.org/javascript-mouse-wheel/
2147
+ * @param {event} event The event
2148
+ */
2149
+ Graph3d.prototype._onWheel = function(event) {
2150
+ if (!event) /* For IE. */
2151
+ event = window.event;
2152
+
2153
+ // retrieve delta
2154
+ var delta = 0;
2155
+ if (event.wheelDelta) { /* IE/Opera. */
2156
+ delta = event.wheelDelta/120;
2157
+ } else if (event.detail) { /* Mozilla case. */
2158
+ // In Mozilla, sign of delta is different than in IE.
2159
+ // Also, delta is multiple of 3.
2160
+ delta = -event.detail/3;
2161
+ }
2162
+
2163
+ // If delta is nonzero, handle it.
2164
+ // Basically, delta is now positive if wheel was scrolled up,
2165
+ // and negative, if wheel was scrolled down.
2166
+ if (delta) {
2167
+ var oldLength = this.camera.getArmLength();
2168
+ var newLength = oldLength * (1 - delta / 10);
2169
+
2170
+ this.camera.setArmLength(newLength);
2171
+ this.redraw();
2172
+
2173
+ this._hideTooltip();
2174
+ }
2175
+
2176
+ // fire a cameraPositionChange event
2177
+ var parameters = this.getCameraPosition();
2178
+ this.emit('cameraPositionChange', parameters);
2179
+
2180
+ // Prevent default actions caused by mouse wheel.
2181
+ // That might be ugly, but we handle scrolls somehow
2182
+ // anyway, so don't bother here..
2183
+ G3DpreventDefault(event);
2184
+ };
2185
+
2186
+ /**
2187
+ * Test whether a point lies inside given 2D triangle
2188
+ * @param {Point2d} point
2189
+ * @param {Point2d[]} triangle
2190
+ * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
2191
+ * @private
2192
+ */
2193
+ Graph3d.prototype._insideTriangle = function (point, triangle) {
2194
+ var a = triangle[0],
2195
+ b = triangle[1],
2196
+ c = triangle[2];
2197
+
2198
+ function sign (x) {
2199
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
2200
+ }
2201
+
2202
+ var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
2203
+ var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
2204
+ var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
2205
+
2206
+ // each of the three signs must be either equal to each other or zero
2207
+ return (as == 0 || bs == 0 || as == bs) &&
2208
+ (bs == 0 || cs == 0 || bs == cs) &&
2209
+ (as == 0 || cs == 0 || as == cs);
2210
+ };
2211
+
2212
+ /**
2213
+ * Find a data point close to given screen position (x, y)
2214
+ * @param {Number} x
2215
+ * @param {Number} y
2216
+ * @return {Object | null} The closest data point or null if not close to any data point
2217
+ * @private
2218
+ */
2219
+ Graph3d.prototype._dataPointFromXY = function (x, y) {
2220
+ var i,
2221
+ distMax = 100, // px
2222
+ dataPoint = null,
2223
+ closestDataPoint = null,
2224
+ closestDist = null,
2225
+ center = new Point2d(x, y);
2226
+
2227
+ if (this.style === Graph3d.STYLE.BAR ||
2228
+ this.style === Graph3d.STYLE.BARCOLOR ||
2229
+ this.style === Graph3d.STYLE.BARSIZE) {
2230
+ // the data points are ordered from far away to closest
2231
+ for (i = this.dataPoints.length - 1; i >= 0; i--) {
2232
+ dataPoint = this.dataPoints[i];
2233
+ var surfaces = dataPoint.surfaces;
2234
+ if (surfaces) {
2235
+ for (var s = surfaces.length - 1; s >= 0; s--) {
2236
+ // split each surface in two triangles, and see if the center point is inside one of these
2237
+ var surface = surfaces[s];
2238
+ var corners = surface.corners;
2239
+ var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
2240
+ var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
2241
+ if (this._insideTriangle(center, triangle1) ||
2242
+ this._insideTriangle(center, triangle2)) {
2243
+ // return immediately at the first hit
2244
+ return dataPoint;
2245
+ }
2246
+ }
2247
+ }
2248
+ }
2249
+ }
2250
+ else {
2251
+ // find the closest data point, using distance to the center of the point on 2d screen
2252
+ for (i = 0; i < this.dataPoints.length; i++) {
2253
+ dataPoint = this.dataPoints[i];
2254
+ var point = dataPoint.screen;
2255
+ if (point) {
2256
+ var distX = Math.abs(x - point.x);
2257
+ var distY = Math.abs(y - point.y);
2258
+ var dist = Math.sqrt(distX * distX + distY * distY);
2259
+
2260
+ if ((closestDist === null || dist < closestDist) && dist < distMax) {
2261
+ closestDist = dist;
2262
+ closestDataPoint = dataPoint;
2263
+ }
2264
+ }
2265
+ }
2266
+ }
2267
+
2268
+
2269
+ return closestDataPoint;
2270
+ };
2271
+
2272
+ /**
2273
+ * Display a tooltip for given data point
2274
+ * @param {Object} dataPoint
2275
+ * @private
2276
+ */
2277
+ Graph3d.prototype._showTooltip = function (dataPoint) {
2278
+ var content, line, dot;
2279
+
2280
+ if (!this.tooltip) {
2281
+ content = document.createElement('div');
2282
+ content.style.position = 'absolute';
2283
+ content.style.padding = '10px';
2284
+ content.style.border = '1px solid #4d4d4d';
2285
+ content.style.color = '#1a1a1a';
2286
+ content.style.background = 'rgba(255,255,255,0.7)';
2287
+ content.style.borderRadius = '2px';
2288
+ content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
2289
+
2290
+ line = document.createElement('div');
2291
+ line.style.position = 'absolute';
2292
+ line.style.height = '40px';
2293
+ line.style.width = '0';
2294
+ line.style.borderLeft = '1px solid #4d4d4d';
2295
+
2296
+ dot = document.createElement('div');
2297
+ dot.style.position = 'absolute';
2298
+ dot.style.height = '0';
2299
+ dot.style.width = '0';
2300
+ dot.style.border = '5px solid #4d4d4d';
2301
+ dot.style.borderRadius = '5px';
2302
+
2303
+ this.tooltip = {
2304
+ dataPoint: null,
2305
+ dom: {
2306
+ content: content,
2307
+ line: line,
2308
+ dot: dot
2309
+ }
2310
+ };
2311
+ }
2312
+ else {
2313
+ content = this.tooltip.dom.content;
2314
+ line = this.tooltip.dom.line;
2315
+ dot = this.tooltip.dom.dot;
2316
+ }
2317
+
2318
+ this._hideTooltip();
2319
+
2320
+ this.tooltip.dataPoint = dataPoint;
2321
+ if (typeof this.showTooltip === 'function') {
2322
+ content.innerHTML = this.showTooltip(dataPoint.point);
2323
+ }
2324
+ else {
2325
+ content.innerHTML = '<table>' +
2326
+ '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
2327
+ '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
2328
+ '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
2329
+ '</table>';
2330
+ }
2331
+
2332
+ content.style.left = '0';
2333
+ content.style.top = '0';
2334
+ this.frame.appendChild(content);
2335
+ this.frame.appendChild(line);
2336
+ this.frame.appendChild(dot);
2337
+
2338
+ // calculate sizes
2339
+ var contentWidth = content.offsetWidth;
2340
+ var contentHeight = content.offsetHeight;
2341
+ var lineHeight = line.offsetHeight;
2342
+ var dotWidth = dot.offsetWidth;
2343
+ var dotHeight = dot.offsetHeight;
2344
+
2345
+ var left = dataPoint.screen.x - contentWidth / 2;
2346
+ left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
2347
+
2348
+ line.style.left = dataPoint.screen.x + 'px';
2349
+ line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
2350
+ content.style.left = left + 'px';
2351
+ content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
2352
+ dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
2353
+ dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
2354
+ };
2355
+
2356
+ /**
2357
+ * Hide the tooltip when displayed
2358
+ * @private
2359
+ */
2360
+ Graph3d.prototype._hideTooltip = function () {
2361
+ if (this.tooltip) {
2362
+ this.tooltip.dataPoint = null;
2363
+
2364
+ for (var prop in this.tooltip.dom) {
2365
+ if (this.tooltip.dom.hasOwnProperty(prop)) {
2366
+ var elem = this.tooltip.dom[prop];
2367
+ if (elem && elem.parentNode) {
2368
+ elem.parentNode.removeChild(elem);
2369
+ }
2370
+ }
2371
+ }
2372
+ }
2373
+ };
2374
+
2375
+
2376
+ /**
2377
+ * Add and event listener. Works for all browsers
2378
+ * @param {Element} element An html element
2379
+ * @param {string} action The action, for example 'click',
2380
+ * without the prefix 'on'
2381
+ * @param {function} listener The callback function to be executed
2382
+ * @param {boolean} useCapture
2383
+ */
2384
+ G3DaddEventListener = function(element, action, listener, useCapture) {
2385
+ if (element.addEventListener) {
2386
+ if (useCapture === undefined)
2387
+ useCapture = false;
2388
+
2389
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
2390
+ action = 'DOMMouseScroll'; // For Firefox
2391
+ }
2392
+
2393
+ element.addEventListener(action, listener, useCapture);
2394
+ } else {
2395
+ element.attachEvent('on' + action, listener); // IE browsers
2396
+ }
2397
+ };
2398
+
2399
+ /**
2400
+ * Remove an event listener from an element
2401
+ * @param {Element} element An html dom element
2402
+ * @param {string} action The name of the event, for example 'mousedown'
2403
+ * @param {function} listener The listener function
2404
+ * @param {boolean} useCapture
2405
+ */
2406
+ G3DremoveEventListener = function(element, action, listener, useCapture) {
2407
+ if (element.removeEventListener) {
2408
+ // non-IE browsers
2409
+ if (useCapture === undefined)
2410
+ useCapture = false;
2411
+
2412
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
2413
+ action = 'DOMMouseScroll'; // For Firefox
2414
+ }
2415
+
2416
+ element.removeEventListener(action, listener, useCapture);
2417
+ } else {
2418
+ // IE browsers
2419
+ element.detachEvent('on' + action, listener);
2420
+ }
2421
+ };
2422
+
2423
+ /**
2424
+ * Stop event propagation
2425
+ */
2426
+ G3DstopPropagation = function(event) {
2427
+ if (!event)
2428
+ event = window.event;
2429
+
2430
+ if (event.stopPropagation) {
2431
+ event.stopPropagation(); // non-IE browsers
2432
+ }
2433
+ else {
2434
+ event.cancelBubble = true; // IE browsers
2435
+ }
2436
+ };
2437
+
2438
+
2439
+ /**
2440
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
2441
+ */
2442
+ G3DpreventDefault = function (event) {
2443
+ if (!event)
2444
+ event = window.event;
2445
+
2446
+ if (event.preventDefault) {
2447
+ event.preventDefault(); // non-IE browsers
2448
+ }
2449
+ else {
2450
+ event.returnValue = false; // IE browsers
2451
+ }
2452
+ };
2453
+
2454
+
2455
+
2456
+ /**
2457
+ * @prototype Point3d
2458
+ * @param {Number} x
2459
+ * @param {Number} y
2460
+ * @param {Number} z
2461
+ */
2462
+ function Point3d(x, y, z) {
2463
+ this.x = x !== undefined ? x : 0;
2464
+ this.y = y !== undefined ? y : 0;
2465
+ this.z = z !== undefined ? z : 0;
2466
+ };
2467
+
2468
+ /**
2469
+ * Subtract the two provided points, returns a-b
2470
+ * @param {Point3d} a
2471
+ * @param {Point3d} b
2472
+ * @return {Point3d} a-b
2473
+ */
2474
+ Point3d.subtract = function(a, b) {
2475
+ var sub = new Point3d();
2476
+ sub.x = a.x - b.x;
2477
+ sub.y = a.y - b.y;
2478
+ sub.z = a.z - b.z;
2479
+ return sub;
2480
+ };
2481
+
2482
+ /**
2483
+ * Add the two provided points, returns a+b
2484
+ * @param {Point3d} a
2485
+ * @param {Point3d} b
2486
+ * @return {Point3d} a+b
2487
+ */
2488
+ Point3d.add = function(a, b) {
2489
+ var sum = new Point3d();
2490
+ sum.x = a.x + b.x;
2491
+ sum.y = a.y + b.y;
2492
+ sum.z = a.z + b.z;
2493
+ return sum;
2494
+ };
2495
+
2496
+ /**
2497
+ * Calculate the average of two 3d points
2498
+ * @param {Point3d} a
2499
+ * @param {Point3d} b
2500
+ * @return {Point3d} The average, (a+b)/2
2501
+ */
2502
+ Point3d.avg = function(a, b) {
2503
+ return new Point3d(
2504
+ (a.x + b.x) / 2,
2505
+ (a.y + b.y) / 2,
2506
+ (a.z + b.z) / 2
2507
+ );
2508
+ };
2509
+
2510
+ /**
2511
+ * Calculate the cross product of the two provided points, returns axb
2512
+ * Documentation: http://en.wikipedia.org/wiki/Cross_product
2513
+ * @param {Point3d} a
2514
+ * @param {Point3d} b
2515
+ * @return {Point3d} cross product axb
2516
+ */
2517
+ Point3d.crossProduct = function(a, b) {
2518
+ var crossproduct = new Point3d();
2519
+
2520
+ crossproduct.x = a.y * b.z - a.z * b.y;
2521
+ crossproduct.y = a.z * b.x - a.x * b.z;
2522
+ crossproduct.z = a.x * b.y - a.y * b.x;
2523
+
2524
+ return crossproduct;
2525
+ };
2526
+
2527
+
2528
+ /**
2529
+ * Rtrieve the length of the vector (or the distance from this point to the origin
2530
+ * @return {Number} length
2531
+ */
2532
+ Point3d.prototype.length = function() {
2533
+ return Math.sqrt(
2534
+ this.x * this.x +
2535
+ this.y * this.y +
2536
+ this.z * this.z
2537
+ );
2538
+ };
2539
+
2540
+ /**
2541
+ * @prototype Point2d
2542
+ */
2543
+ Point2d = function (x, y) {
2544
+ this.x = x !== undefined ? x : 0;
2545
+ this.y = y !== undefined ? y : 0;
2546
+ };
2547
+
2548
+
2549
+ /**
2550
+ * @class Filter
2551
+ *
2552
+ * @param {DataSet} data The google data table
2553
+ * @param {Number} column The index of the column to be filtered
2554
+ * @param {Graph} graph The graph
2555
+ */
2556
+ function Filter (data, column, graph) {
2557
+ this.data = data;
2558
+ this.column = column;
2559
+ this.graph = graph; // the parent graph
2560
+
2561
+ this.index = undefined;
2562
+ this.value = undefined;
2563
+
2564
+ // read all distinct values and select the first one
2565
+ this.values = graph.getDistinctValues(data.get(), this.column);
2566
+
2567
+ // sort both numeric and string values correctly
2568
+ this.values.sort(function (a, b) {
2569
+ return a > b ? 1 : a < b ? -1 : 0;
2570
+ });
2571
+
2572
+ if (this.values.length > 0) {
2573
+ this.selectValue(0);
2574
+ }
2575
+
2576
+ // create an array with the filtered datapoints. this will be loaded afterwards
2577
+ this.dataPoints = [];
2578
+
2579
+ this.loaded = false;
2580
+ this.onLoadCallback = undefined;
2581
+
2582
+ if (graph.animationPreload) {
2583
+ this.loaded = false;
2584
+ this.loadInBackground();
2585
+ }
2586
+ else {
2587
+ this.loaded = true;
2588
+ }
2589
+ };
2590
+
2591
+
2592
+ /**
2593
+ * Return the label
2594
+ * @return {string} label
2595
+ */
2596
+ Filter.prototype.isLoaded = function() {
2597
+ return this.loaded;
2598
+ };
2599
+
2600
+
2601
+ /**
2602
+ * Return the loaded progress
2603
+ * @return {Number} percentage between 0 and 100
2604
+ */
2605
+ Filter.prototype.getLoadedProgress = function() {
2606
+ var len = this.values.length;
2607
+
2608
+ var i = 0;
2609
+ while (this.dataPoints[i]) {
2610
+ i++;
2611
+ }
2612
+
2613
+ return Math.round(i / len * 100);
2614
+ };
2615
+
2616
+
2617
+ /**
2618
+ * Return the label
2619
+ * @return {string} label
2620
+ */
2621
+ Filter.prototype.getLabel = function() {
2622
+ return this.graph.filterLabel;
2623
+ };
2624
+
2625
+
2626
+ /**
2627
+ * Return the columnIndex of the filter
2628
+ * @return {Number} columnIndex
2629
+ */
2630
+ Filter.prototype.getColumn = function() {
2631
+ return this.column;
2632
+ };
2633
+
2634
+ /**
2635
+ * Return the currently selected value. Returns undefined if there is no selection
2636
+ * @return {*} value
2637
+ */
2638
+ Filter.prototype.getSelectedValue = function() {
2639
+ if (this.index === undefined)
2640
+ return undefined;
2641
+
2642
+ return this.values[this.index];
2643
+ };
2644
+
2645
+ /**
2646
+ * Retrieve all values of the filter
2647
+ * @return {Array} values
2648
+ */
2649
+ Filter.prototype.getValues = function() {
2650
+ return this.values;
2651
+ };
2652
+
2653
+ /**
2654
+ * Retrieve one value of the filter
2655
+ * @param {Number} index
2656
+ * @return {*} value
2657
+ */
2658
+ Filter.prototype.getValue = function(index) {
2659
+ if (index >= this.values.length)
2660
+ throw 'Error: index out of range';
2661
+
2662
+ return this.values[index];
2663
+ };
2664
+
2665
+
2666
+ /**
2667
+ * Retrieve the (filtered) dataPoints for the currently selected filter index
2668
+ * @param {Number} [index] (optional)
2669
+ * @return {Array} dataPoints
2670
+ */
2671
+ Filter.prototype._getDataPoints = function(index) {
2672
+ if (index === undefined)
2673
+ index = this.index;
2674
+
2675
+ if (index === undefined)
2676
+ return [];
2677
+
2678
+ var dataPoints;
2679
+ if (this.dataPoints[index]) {
2680
+ dataPoints = this.dataPoints[index];
2681
+ }
2682
+ else {
2683
+ var f = {};
2684
+ f.column = this.column;
2685
+ f.value = this.values[index];
2686
+
2687
+ var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
2688
+ dataPoints = this.graph._getDataPoints(dataView);
2689
+
2690
+ this.dataPoints[index] = dataPoints;
2691
+ }
2692
+
2693
+ return dataPoints;
2694
+ };
2695
+
2696
+
2697
+
2698
+ /**
2699
+ * Set a callback function when the filter is fully loaded.
2700
+ */
2701
+ Filter.prototype.setOnLoadCallback = function(callback) {
2702
+ this.onLoadCallback = callback;
2703
+ };
2704
+
2705
+
2706
+ /**
2707
+ * Add a value to the list with available values for this filter
2708
+ * No double entries will be created.
2709
+ * @param {Number} index
2710
+ */
2711
+ Filter.prototype.selectValue = function(index) {
2712
+ if (index >= this.values.length)
2713
+ throw 'Error: index out of range';
2714
+
2715
+ this.index = index;
2716
+ this.value = this.values[index];
2717
+ };
2718
+
2719
+ /**
2720
+ * Load all filtered rows in the background one by one
2721
+ * Start this method without providing an index!
2722
+ */
2723
+ Filter.prototype.loadInBackground = function(index) {
2724
+ if (index === undefined)
2725
+ index = 0;
2726
+
2727
+ var frame = this.graph.frame;
2728
+
2729
+ if (index < this.values.length) {
2730
+ var dataPointsTemp = this._getDataPoints(index);
2731
+ //this.graph.redrawInfo(); // TODO: not neat
2732
+
2733
+ // create a progress box
2734
+ if (frame.progress === undefined) {
2735
+ frame.progress = document.createElement('DIV');
2736
+ frame.progress.style.position = 'absolute';
2737
+ frame.progress.style.color = 'gray';
2738
+ frame.appendChild(frame.progress);
2739
+ }
2740
+ var progress = this.getLoadedProgress();
2741
+ frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
2742
+ // TODO: this is no nice solution...
2743
+ frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
2744
+ frame.progress.style.left = Graph3d.px(10);
2745
+
2746
+ var me = this;
2747
+ setTimeout(function() {me.loadInBackground(index+1);}, 10);
2748
+ this.loaded = false;
2749
+ }
2750
+ else {
2751
+ this.loaded = true;
2752
+
2753
+ // remove the progress box
2754
+ if (frame.progress !== undefined) {
2755
+ frame.removeChild(frame.progress);
2756
+ frame.progress = undefined;
2757
+ }
2758
+
2759
+ if (this.onLoadCallback)
2760
+ this.onLoadCallback();
2761
+ }
2762
+ };
2763
+
2764
+
2765
+
2766
+ /**
2767
+ * @prototype StepNumber
2768
+ * The class StepNumber is an iterator for Numbers. You provide a start and end
2769
+ * value, and a best step size. StepNumber itself rounds to fixed values and
2770
+ * a finds the step that best fits the provided step.
2771
+ *
2772
+ * If prettyStep is true, the step size is chosen as close as possible to the
2773
+ * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
2774
+ *
2775
+ * Example usage:
2776
+ * var step = new StepNumber(0, 10, 2.5, true);
2777
+ * step.start();
2778
+ * while (!step.end()) {
2779
+ * alert(step.getCurrent());
2780
+ * step.next();
2781
+ * }
2782
+ *
2783
+ * Version: 1.0
2784
+ *
2785
+ * @param {Number} start The start value
2786
+ * @param {Number} end The end value
2787
+ * @param {Number} step Optional. Step size. Must be a positive value.
2788
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
2789
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2790
+ */
2791
+ StepNumber = function (start, end, step, prettyStep) {
2792
+ // set default values
2793
+ this._start = 0;
2794
+ this._end = 0;
2795
+ this._step = 1;
2796
+ this.prettyStep = true;
2797
+ this.precision = 5;
2798
+
2799
+ this._current = 0;
2800
+ this.setRange(start, end, step, prettyStep);
2801
+ };
2802
+
2803
+ /**
2804
+ * Set a new range: start, end and step.
2805
+ *
2806
+ * @param {Number} start The start value
2807
+ * @param {Number} end The end value
2808
+ * @param {Number} step Optional. Step size. Must be a positive value.
2809
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
2810
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2811
+ */
2812
+ StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
2813
+ this._start = start ? start : 0;
2814
+ this._end = end ? end : 0;
2815
+
2816
+ this.setStep(step, prettyStep);
2817
+ };
2818
+
2819
+ /**
2820
+ * Set a new step size
2821
+ * @param {Number} step New step size. Must be a positive value
2822
+ * @param {boolean} prettyStep Optional. If true, the provided step is rounded
2823
+ * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2824
+ */
2825
+ StepNumber.prototype.setStep = function(step, prettyStep) {
2826
+ if (step === undefined || step <= 0)
2827
+ return;
2828
+
2829
+ if (prettyStep !== undefined)
2830
+ this.prettyStep = prettyStep;
2831
+
2832
+ if (this.prettyStep === true)
2833
+ this._step = StepNumber.calculatePrettyStep(step);
2834
+ else
2835
+ this._step = step;
2836
+ };
2837
+
2838
+ /**
2839
+ * Calculate a nice step size, closest to the desired step size.
2840
+ * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
2841
+ * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
2842
+ * @param {Number} step Desired step size
2843
+ * @return {Number} Nice step size
2844
+ */
2845
+ StepNumber.calculatePrettyStep = function (step) {
2846
+ var log10 = function (x) {return Math.log(x) / Math.LN10;};
2847
+
2848
+ // try three steps (multiple of 1, 2, or 5
2849
+ var step1 = Math.pow(10, Math.round(log10(step))),
2850
+ step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
2851
+ step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
2852
+
2853
+ // choose the best step (closest to minimum step)
2854
+ var prettyStep = step1;
2855
+ if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
2856
+ if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
2857
+
2858
+ // for safety
2859
+ if (prettyStep <= 0) {
2860
+ prettyStep = 1;
2861
+ }
2862
+
2863
+ return prettyStep;
2864
+ };
2865
+
2866
+ /**
2867
+ * returns the current value of the step
2868
+ * @return {Number} current value
2869
+ */
2870
+ StepNumber.prototype.getCurrent = function () {
2871
+ return parseFloat(this._current.toPrecision(this.precision));
2872
+ };
2873
+
2874
+ /**
2875
+ * returns the current step size
2876
+ * @return {Number} current step size
2877
+ */
2878
+ StepNumber.prototype.getStep = function () {
2879
+ return this._step;
2880
+ };
2881
+
2882
+ /**
2883
+ * Set the current value to the largest value smaller than start, which
2884
+ * is a multiple of the step size
2885
+ */
2886
+ StepNumber.prototype.start = function() {
2887
+ this._current = this._start - this._start % this._step;
2888
+ };
2889
+
2890
+ /**
2891
+ * Do a step, add the step size to the current value
2892
+ */
2893
+ StepNumber.prototype.next = function () {
2894
+ this._current += this._step;
2895
+ };
2896
+
2897
+ /**
2898
+ * Returns true whether the end is reached
2899
+ * @return {boolean} True if the current value has passed the end value.
2900
+ */
2901
+ StepNumber.prototype.end = function () {
2902
+ return (this._current > this._end);
2903
+ };
2904
+
2905
+
2906
+ /**
2907
+ * @constructor Slider
2908
+ *
2909
+ * An html slider control with start/stop/prev/next buttons
2910
+ * @param {Element} container The element where the slider will be created
2911
+ * @param {Object} options Available options:
2912
+ * {boolean} visible If true (default) the
2913
+ * slider is visible.
2914
+ */
2915
+ function Slider(container, options) {
2916
+ if (container === undefined) {
2917
+ throw 'Error: No container element defined';
2918
+ }
2919
+ this.container = container;
2920
+ this.visible = (options && options.visible != undefined) ? options.visible : true;
2921
+
2922
+ if (this.visible) {
2923
+ this.frame = document.createElement('DIV');
2924
+ //this.frame.style.backgroundColor = '#E5E5E5';
2925
+ this.frame.style.width = '100%';
2926
+ this.frame.style.position = 'relative';
2927
+ this.container.appendChild(this.frame);
2928
+
2929
+ this.frame.prev = document.createElement('INPUT');
2930
+ this.frame.prev.type = 'BUTTON';
2931
+ this.frame.prev.value = 'Prev';
2932
+ this.frame.appendChild(this.frame.prev);
2933
+
2934
+ this.frame.play = document.createElement('INPUT');
2935
+ this.frame.play.type = 'BUTTON';
2936
+ this.frame.play.value = 'Play';
2937
+ this.frame.appendChild(this.frame.play);
2938
+
2939
+ this.frame.next = document.createElement('INPUT');
2940
+ this.frame.next.type = 'BUTTON';
2941
+ this.frame.next.value = 'Next';
2942
+ this.frame.appendChild(this.frame.next);
2943
+
2944
+ this.frame.bar = document.createElement('INPUT');
2945
+ this.frame.bar.type = 'BUTTON';
2946
+ this.frame.bar.style.position = 'absolute';
2947
+ this.frame.bar.style.border = '1px solid red';
2948
+ this.frame.bar.style.width = '100px';
2949
+ this.frame.bar.style.height = '6px';
2950
+ this.frame.bar.style.borderRadius = '2px';
2951
+ this.frame.bar.style.MozBorderRadius = '2px';
2952
+ this.frame.bar.style.border = '1px solid #7F7F7F';
2953
+ this.frame.bar.style.backgroundColor = '#E5E5E5';
2954
+ this.frame.appendChild(this.frame.bar);
2955
+
2956
+ this.frame.slide = document.createElement('INPUT');
2957
+ this.frame.slide.type = 'BUTTON';
2958
+ this.frame.slide.style.margin = '0px';
2959
+ this.frame.slide.value = ' ';
2960
+ this.frame.slide.style.position = 'relative';
2961
+ this.frame.slide.style.left = '-100px';
2962
+ this.frame.appendChild(this.frame.slide);
2963
+
2964
+ // create events
2965
+ var me = this;
2966
+ this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
2967
+ this.frame.prev.onclick = function (event) {me.prev(event);};
2968
+ this.frame.play.onclick = function (event) {me.togglePlay(event);};
2969
+ this.frame.next.onclick = function (event) {me.next(event);};
2970
+ }
2971
+
2972
+ this.onChangeCallback = undefined;
2973
+
2974
+ this.values = [];
2975
+ this.index = undefined;
2976
+
2977
+ this.playTimeout = undefined;
2978
+ this.playInterval = 1000; // milliseconds
2979
+ this.playLoop = true;
2980
+ };
2981
+
2982
+ /**
2983
+ * Select the previous index
2984
+ */
2985
+ Slider.prototype.prev = function() {
2986
+ var index = this.getIndex();
2987
+ if (index > 0) {
2988
+ index--;
2989
+ this.setIndex(index);
2990
+ }
2991
+ };
2992
+
2993
+ /**
2994
+ * Select the next index
2995
+ */
2996
+ Slider.prototype.next = function() {
2997
+ var index = this.getIndex();
2998
+ if (index < this.values.length - 1) {
2999
+ index++;
3000
+ this.setIndex(index);
3001
+ }
3002
+ };
3003
+
3004
+ /**
3005
+ * Select the next index
3006
+ */
3007
+ Slider.prototype.playNext = function() {
3008
+ var start = new Date();
3009
+
3010
+ var index = this.getIndex();
3011
+ if (index < this.values.length - 1) {
3012
+ index++;
3013
+ this.setIndex(index);
3014
+ }
3015
+ else if (this.playLoop) {
3016
+ // jump to the start
3017
+ index = 0;
3018
+ this.setIndex(index);
3019
+ }
3020
+
3021
+ var end = new Date();
3022
+ var diff = (end - start);
3023
+
3024
+ // calculate how much time it to to set the index and to execute the callback
3025
+ // function.
3026
+ var interval = Math.max(this.playInterval - diff, 0);
3027
+ // document.title = diff // TODO: cleanup
3028
+
3029
+ var me = this;
3030
+ this.playTimeout = setTimeout(function() {me.playNext();}, interval);
3031
+ };
3032
+
3033
+ /**
3034
+ * Toggle start or stop playing
3035
+ */
3036
+ Slider.prototype.togglePlay = function() {
3037
+ if (this.playTimeout === undefined) {
3038
+ this.play();
3039
+ } else {
3040
+ this.stop();
3041
+ }
3042
+ };
3043
+
3044
+ /**
3045
+ * Start playing
3046
+ */
3047
+ Slider.prototype.play = function() {
3048
+ // Test whether already playing
3049
+ if (this.playTimeout) return;
3050
+
3051
+ this.playNext();
3052
+
3053
+ if (this.frame) {
3054
+ this.frame.play.value = 'Stop';
3055
+ }
3056
+ };
3057
+
3058
+ /**
3059
+ * Stop playing
3060
+ */
3061
+ Slider.prototype.stop = function() {
3062
+ clearInterval(this.playTimeout);
3063
+ this.playTimeout = undefined;
3064
+
3065
+ if (this.frame) {
3066
+ this.frame.play.value = 'Play';
3067
+ }
3068
+ };
3069
+
3070
+ /**
3071
+ * Set a callback function which will be triggered when the value of the
3072
+ * slider bar has changed.
3073
+ */
3074
+ Slider.prototype.setOnChangeCallback = function(callback) {
3075
+ this.onChangeCallback = callback;
3076
+ };
3077
+
3078
+ /**
3079
+ * Set the interval for playing the list
3080
+ * @param {Number} interval The interval in milliseconds
3081
+ */
3082
+ Slider.prototype.setPlayInterval = function(interval) {
3083
+ this.playInterval = interval;
3084
+ };
3085
+
3086
+ /**
3087
+ * Retrieve the current play interval
3088
+ * @return {Number} interval The interval in milliseconds
3089
+ */
3090
+ Slider.prototype.getPlayInterval = function(interval) {
3091
+ return this.playInterval;
3092
+ };
3093
+
3094
+ /**
3095
+ * Set looping on or off
3096
+ * @pararm {boolean} doLoop If true, the slider will jump to the start when
3097
+ * the end is passed, and will jump to the end
3098
+ * when the start is passed.
3099
+ */
3100
+ Slider.prototype.setPlayLoop = function(doLoop) {
3101
+ this.playLoop = doLoop;
3102
+ };
3103
+
3104
+
3105
+ /**
3106
+ * Execute the onchange callback function
3107
+ */
3108
+ Slider.prototype.onChange = function() {
3109
+ if (this.onChangeCallback !== undefined) {
3110
+ this.onChangeCallback();
3111
+ }
3112
+ };
3113
+
3114
+ /**
3115
+ * redraw the slider on the correct place
3116
+ */
3117
+ Slider.prototype.redraw = function() {
3118
+ if (this.frame) {
3119
+ // resize the bar
3120
+ this.frame.bar.style.top = (this.frame.clientHeight/2 -
3121
+ this.frame.bar.offsetHeight/2) + 'px';
3122
+ this.frame.bar.style.width = (this.frame.clientWidth -
3123
+ this.frame.prev.clientWidth -
3124
+ this.frame.play.clientWidth -
3125
+ this.frame.next.clientWidth - 30) + 'px';
3126
+
3127
+ // position the slider button
3128
+ var left = this.indexToLeft(this.index);
3129
+ this.frame.slide.style.left = (left) + 'px';
3130
+ }
3131
+ };
3132
+
3133
+
3134
+ /**
3135
+ * Set the list with values for the slider
3136
+ * @param {Array} values A javascript array with values (any type)
3137
+ */
3138
+ Slider.prototype.setValues = function(values) {
3139
+ this.values = values;
3140
+
3141
+ if (this.values.length > 0)
3142
+ this.setIndex(0);
3143
+ else
3144
+ this.index = undefined;
3145
+ };
3146
+
3147
+ /**
3148
+ * Select a value by its index
3149
+ * @param {Number} index
3150
+ */
3151
+ Slider.prototype.setIndex = function(index) {
3152
+ if (index < this.values.length) {
3153
+ this.index = index;
3154
+
3155
+ this.redraw();
3156
+ this.onChange();
3157
+ }
3158
+ else {
3159
+ throw 'Error: index out of range';
3160
+ }
3161
+ };
3162
+
3163
+ /**
3164
+ * retrieve the index of the currently selected vaue
3165
+ * @return {Number} index
3166
+ */
3167
+ Slider.prototype.getIndex = function() {
3168
+ return this.index;
3169
+ };
3170
+
3171
+
3172
+ /**
3173
+ * retrieve the currently selected value
3174
+ * @return {*} value
3175
+ */
3176
+ Slider.prototype.get = function() {
3177
+ return this.values[this.index];
3178
+ };
3179
+
3180
+
3181
+ Slider.prototype._onMouseDown = function(event) {
3182
+ // only react on left mouse button down
3183
+ var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
3184
+ if (!leftButtonDown) return;
3185
+
3186
+ this.startClientX = event.clientX;
3187
+ this.startSlideX = parseFloat(this.frame.slide.style.left);
3188
+
3189
+ this.frame.style.cursor = 'move';
3190
+
3191
+ // add event listeners to handle moving the contents
3192
+ // we store the function onmousemove and onmouseup in the graph, so we can
3193
+ // remove the eventlisteners lateron in the function mouseUp()
3194
+ var me = this;
3195
+ this.onmousemove = function (event) {me._onMouseMove(event);};
3196
+ this.onmouseup = function (event) {me._onMouseUp(event);};
3197
+ G3DaddEventListener(document, 'mousemove', this.onmousemove);
3198
+ G3DaddEventListener(document, 'mouseup', this.onmouseup);
3199
+ G3DpreventDefault(event);
3200
+ };
3201
+
3202
+
3203
+ Slider.prototype.leftToIndex = function (left) {
3204
+ var width = parseFloat(this.frame.bar.style.width) -
3205
+ this.frame.slide.clientWidth - 10;
3206
+ var x = left - 3;
3207
+
3208
+ var index = Math.round(x / width * (this.values.length-1));
3209
+ if (index < 0) index = 0;
3210
+ if (index > this.values.length-1) index = this.values.length-1;
3211
+
3212
+ return index;
3213
+ };
3214
+
3215
+ Slider.prototype.indexToLeft = function (index) {
3216
+ var width = parseFloat(this.frame.bar.style.width) -
3217
+ this.frame.slide.clientWidth - 10;
3218
+
3219
+ var x = index / (this.values.length-1) * width;
3220
+ var left = x + 3;
3221
+
3222
+ return left;
3223
+ };
3224
+
3225
+
3226
+
3227
+ Slider.prototype._onMouseMove = function (event) {
3228
+ var diff = event.clientX - this.startClientX;
3229
+ var x = this.startSlideX + diff;
3230
+
3231
+ var index = this.leftToIndex(x);
3232
+
3233
+ this.setIndex(index);
3234
+
3235
+ G3DpreventDefault();
3236
+ };
3237
+
3238
+
3239
+ Slider.prototype._onMouseUp = function (event) {
3240
+ this.frame.style.cursor = 'auto';
3241
+
3242
+ // remove event listeners
3243
+ G3DremoveEventListener(document, 'mousemove', this.onmousemove);
3244
+ G3DremoveEventListener(document, 'mouseup', this.onmouseup);
3245
+
3246
+ G3DpreventDefault();
3247
+ };
3248
+
3249
+
3250
+
3251
+ /**--------------------------------------------------------------------------**/
3252
+
3253
+
3254
+
3255
+ /**
3256
+ * Retrieve the absolute left value of a DOM element
3257
+ * @param {Element} elem A dom element, for example a div
3258
+ * @return {Number} left The absolute left position of this element
3259
+ * in the browser page.
3260
+ */
3261
+ getAbsoluteLeft = function(elem) {
3262
+ var left = 0;
3263
+ while( elem !== null ) {
3264
+ left += elem.offsetLeft;
3265
+ left -= elem.scrollLeft;
3266
+ elem = elem.offsetParent;
3267
+ }
3268
+ return left;
3269
+ };
3270
+
3271
+ /**
3272
+ * Retrieve the absolute top value of a DOM element
3273
+ * @param {Element} elem A dom element, for example a div
3274
+ * @return {Number} top The absolute top position of this element
3275
+ * in the browser page.
3276
+ */
3277
+ getAbsoluteTop = function(elem) {
3278
+ var top = 0;
3279
+ while( elem !== null ) {
3280
+ top += elem.offsetTop;
3281
+ top -= elem.scrollTop;
3282
+ elem = elem.offsetParent;
3283
+ }
3284
+ return top;
3285
+ };
3286
+
3287
+ /**
3288
+ * Get the horizontal mouse position from a mouse event
3289
+ * @param {Event} event
3290
+ * @return {Number} mouse x
3291
+ */
3292
+ getMouseX = function(event) {
3293
+ if ('clientX' in event) return event.clientX;
3294
+ return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
3295
+ };
3296
+
3297
+ /**
3298
+ * Get the vertical mouse position from a mouse event
3299
+ * @param {Event} event
3300
+ * @return {Number} mouse y
3301
+ */
3302
+ getMouseY = function(event) {
3303
+ if ('clientY' in event) return event.clientY;
3304
+ return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
3305
+ };
3306
+