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.

281 lines
11 KiB

  1. let util = require('../../util');
  2. import NetworkUtil from '../NetworkUtil';
  3. class View {
  4. constructor(body, canvas) {
  5. this.body = body;
  6. this.canvas = canvas;
  7. this.animationSpeed = 1/this.renderRefreshRate;
  8. this.animationEasingFunction = "easeInOutQuint";
  9. this.easingTime = 0;
  10. this.sourceScale = 0;
  11. this.targetScale = 0;
  12. this.sourceTranslation = 0;
  13. this.targetTranslation = 0;
  14. this.lockedOnNodeId = undefined;
  15. this.lockedOnNodeOffset = undefined;
  16. this.touchTime = 0;
  17. this.viewFunction = undefined;
  18. this.body.emitter.on("fit", this.fit.bind(this));
  19. this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");});
  20. this.body.emitter.on("unlockNode", this.releaseNode.bind(this));
  21. }
  22. setOptions(options = {}) {
  23. this.options = options;
  24. }
  25. /**
  26. * This function zooms out to fit all data on screen based on amount of nodes
  27. * @param {Object} Options
  28. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  29. */
  30. fit(options = {nodes:[]}, initialZoom = false) {
  31. let range;
  32. let zoomLevel;
  33. if (options.nodes === undefined || options.nodes.length === 0) {
  34. options.nodes = this.body.nodeIndices;
  35. }
  36. if (initialZoom === true) {
  37. // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
  38. let positionDefined = 0;
  39. for (let nodeId in this.body.nodes) {
  40. if (this.body.nodes.hasOwnProperty(nodeId)) {
  41. let node = this.body.nodes[nodeId];
  42. if (node.predefinedPosition === true) {
  43. positionDefined += 1;
  44. }
  45. }
  46. }
  47. if (positionDefined > 0.5 * this.body.nodeIndices.length) {
  48. this.fit(options,false);
  49. return;
  50. }
  51. range = NetworkUtil.getRange(this.body.nodes, options.nodes);
  52. let numberOfNodes = this.body.nodeIndices.length;
  53. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  54. // correct for larger canvasses.
  55. let factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
  56. zoomLevel *= factor;
  57. }
  58. else {
  59. this.body.emitter.emit("_resizeNodes");
  60. range = NetworkUtil.getRange(this.body.nodes, options.nodes);
  61. let xDistance = Math.abs(range.maxX - range.minX) * 1.1;
  62. let yDistance = Math.abs(range.maxY - range.minY) * 1.1;
  63. let xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance;
  64. let yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance;
  65. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  66. }
  67. if (zoomLevel > 1.0) {
  68. zoomLevel = 1.0;
  69. }
  70. else if (zoomLevel === 0) {
  71. zoomLevel = 1.0;
  72. }
  73. let center = NetworkUtil.findCenter(range);
  74. let animationOptions = {position: center, scale: zoomLevel, animation: options.animation};
  75. this.moveTo(animationOptions);
  76. }
  77. // animation
  78. /**
  79. * Center a node in view.
  80. *
  81. * @param {Number} nodeId
  82. * @param {Number} [options]
  83. */
  84. focus(nodeId, options = {}) {
  85. if (this.body.nodes[nodeId] !== undefined) {
  86. let nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y};
  87. options.position = nodePosition;
  88. options.lockedOnNode = nodeId;
  89. this.moveTo(options)
  90. }
  91. else {
  92. console.log("Node: " + nodeId + " cannot be found.");
  93. }
  94. }
  95. /**
  96. *
  97. * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
  98. * | options.scale = Number // scale to move to
  99. * | options.position = {x:Number, y:Number} // position to move to
  100. * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to
  101. */
  102. moveTo(options) {
  103. if (options === undefined) {
  104. options = {};
  105. return;
  106. }
  107. if (options.offset === undefined) {options.offset = {x: 0, y: 0}; }
  108. if (options.offset.x === undefined) {options.offset.x = 0; }
  109. if (options.offset.y === undefined) {options.offset.y = 0; }
  110. if (options.scale === undefined) {options.scale = this.body.view.scale; }
  111. if (options.position === undefined) {options.position = this.getViewPosition();}
  112. if (options.animation === undefined) {options.animation = {duration:0}; }
  113. if (options.animation === false ) {options.animation = {duration:0}; }
  114. if (options.animation === true ) {options.animation = {}; }
  115. if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration
  116. if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function
  117. this.animateView(options);
  118. }
  119. /**
  120. *
  121. * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
  122. * | options.time = Number // animation time in milliseconds
  123. * | options.scale = Number // scale to animate to
  124. * | options.position = {x:Number, y:Number} // position to animate to
  125. * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad,
  126. * // easeInCubic, easeOutCubic, easeInOutCubic,
  127. * // easeInQuart, easeOutQuart, easeInOutQuart,
  128. * // easeInQuint, easeOutQuint, easeInOutQuint
  129. */
  130. animateView(options) {
  131. if (options === undefined) {
  132. return;
  133. }
  134. this.animationEasingFunction = options.animation.easingFunction;
  135. // release if something focussed on the node
  136. this.releaseNode();
  137. if (options.locked === true) {
  138. this.lockedOnNodeId = options.lockedOnNode;
  139. this.lockedOnNodeOffset = options.offset;
  140. }
  141. // forcefully complete the old animation if it was still running
  142. if (this.easingTime != 0) {
  143. this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation.
  144. }
  145. this.sourceScale = this.body.view.scale;
  146. this.sourceTranslation = this.body.view.translation;
  147. this.targetScale = options.scale;
  148. // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
  149. // but at least then we'll have the target transition
  150. this.body.view.scale = this.targetScale;
  151. let viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
  152. let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
  153. x: viewCenter.x - options.position.x,
  154. y: viewCenter.y - options.position.y
  155. };
  156. this.targetTranslation = {
  157. x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x,
  158. y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y
  159. };
  160. // if the time is set to 0, don't do an animation
  161. if (options.animation.duration === 0) {
  162. if (this.lockedOnNodeId != undefined) {
  163. this.viewFunction = this._lockedRedraw.bind(this);
  164. this.body.emitter.on("initRedraw", this.viewFunction);
  165. }
  166. else {
  167. this.body.view.scale = this.targetScale;
  168. this.body.view.translation = this.targetTranslation;
  169. this.body.emitter.emit("_requestRedraw");
  170. }
  171. }
  172. else {
  173. this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's
  174. this.animationEasingFunction = options.animation.easingFunction;
  175. this.viewFunction = this._transitionRedraw.bind(this);
  176. this.body.emitter.on("initRedraw", this.viewFunction);
  177. this.body.emitter.emit("_startRendering");
  178. }
  179. }
  180. /**
  181. * used to animate smoothly by hijacking the redraw function.
  182. * @private
  183. */
  184. _lockedRedraw() {
  185. let nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y};
  186. let viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
  187. let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
  188. x: viewCenter.x - nodePosition.x,
  189. y: viewCenter.y - nodePosition.y
  190. };
  191. let sourceTranslation = this.body.view.translation;
  192. let targetTranslation = {
  193. x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x,
  194. y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y
  195. };
  196. this.body.view.translation = targetTranslation;
  197. }
  198. releaseNode() {
  199. if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) {
  200. this.body.emitter.off("initRedraw", this.viewFunction);
  201. this.lockedOnNodeId = undefined;
  202. this.lockedOnNodeOffset = undefined;
  203. }
  204. }
  205. /**
  206. *
  207. * @param easingTime
  208. * @private
  209. */
  210. _transitionRedraw(finished = false) {
  211. this.easingTime += this.animationSpeed;
  212. this.easingTime = finished === true ? 1.0 : this.easingTime;
  213. let progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);
  214. this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress;
  215. this.body.view.translation = {
  216. x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress,
  217. y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress
  218. };
  219. // cleanup
  220. if (this.easingTime >= 1.0) {
  221. this.body.emitter.off("initRedraw", this.viewFunction);
  222. this.easingTime = 0;
  223. if (this.lockedOnNodeId != undefined) {
  224. this.viewFunction = this._lockedRedraw.bind(this);
  225. this.body.emitter.on("initRedraw", this.viewFunction);
  226. }
  227. this.body.emitter.emit("animationFinished");
  228. }
  229. };
  230. getScale() {
  231. return this.body.view.scale;
  232. }
  233. getViewPosition() {
  234. return this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
  235. }
  236. }
  237. export default View;