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.

302 lines
11 KiB

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