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.

337 lines
12 KiB

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