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.

342 lines
13 KiB

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