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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +2 -0
- data/lib/vis/rails/version.rb +1 -1
- data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
- data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
- data/vendor/assets/javascripts/vis.js +3 -3
- data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
- data/vendor/assets/vis/DataSet.js +106 -130
- data/vendor/assets/vis/DataView.js +35 -37
- data/vendor/assets/vis/graph/Edge.js +225 -45
- data/vendor/assets/vis/graph/Graph.js +120 -24
- data/vendor/assets/vis/graph/Node.js +16 -16
- data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
- data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
- data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
- data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
- data/vendor/assets/vis/module/exports.js +2 -3
- data/vendor/assets/vis/timeline/Range.js +93 -80
- data/vendor/assets/vis/timeline/Timeline.js +525 -428
- data/vendor/assets/vis/timeline/component/Component.js +19 -53
- data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
- data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
- data/vendor/assets/vis/timeline/component/Group.js +47 -50
- data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
- data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
- data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
- data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
- data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
- data/vendor/assets/vis/timeline/component/css/item.css +1 -11
- data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
- data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
- data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
- data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
- data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
- data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
- data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
- data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
- data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
- data/vendor/assets/vis/timeline/stack.js +5 -5
- data/vendor/assets/vis/util.js +81 -35
- metadata +7 -4
- data/vendor/assets/vis/timeline/component/Panel.js +0 -170
- 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
|
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
|
+
|