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.

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