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.

333 lines
12 KiB

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