vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3310 lines
96 KiB

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