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.

381 lines
11 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. /**
  2. * Initializes window.requestAnimationFrame() to a usable form.
  3. *
  4. * Specifically, set up this method for the case of running on node.js with jsdom enabled.
  5. *
  6. * NOTES:
  7. *
  8. * * On node.js, when calling this directly outside of this class, `window` is not defined.
  9. * This happens even if jsdom is used.
  10. * * For node.js + jsdom, `window` is available at the moment the constructor is called.
  11. * For this reason, the called is placed within the constructor.
  12. * * Even then, `window.requestAnimationFrame()` is not defined, so it still needs to be added.
  13. * * During unit testing, it happens that the window object is reset during execution, causing
  14. * a runtime error due to missing `requestAnimationFrame()`. This needs to be compensated for,
  15. * see `_requestNextFrame()`.
  16. * * Since this is a global object, it may affect other modules besides `Network`. With normal
  17. * usage, this does not cause any problems. During unit testing, errors may occur. These have
  18. * been compensated for, see comment block in _requestNextFrame().
  19. *
  20. * @private
  21. */
  22. function _initRequestAnimationFrame() {
  23. var func;
  24. if (window !== undefined) {
  25. func = window.requestAnimationFrame
  26. || window.mozRequestAnimationFrame
  27. || window.webkitRequestAnimationFrame
  28. || window.msRequestAnimationFrame;
  29. }
  30. if (func === undefined) {
  31. // window or method not present, setting mock requestAnimationFrame
  32. window.requestAnimationFrame =
  33. function(callback) {
  34. //console.log("Called mock requestAnimationFrame");
  35. callback();
  36. }
  37. } else {
  38. window.requestAnimationFrame = func;
  39. }
  40. }
  41. let util = require('../../util');
  42. class CanvasRenderer {
  43. constructor(body, canvas) {
  44. _initRequestAnimationFrame();
  45. this.body = body;
  46. this.canvas = canvas;
  47. this.redrawRequested = false;
  48. this.renderTimer = undefined;
  49. this.requiresTimeout = true;
  50. this.renderingActive = false;
  51. this.renderRequests = 0;
  52. this.allowRedraw = true;
  53. this.dragging = false;
  54. this.options = {};
  55. this.defaultOptions = {
  56. hideEdgesOnDrag: false,
  57. hideNodesOnDrag: false
  58. };
  59. util.extend(this.options, this.defaultOptions);
  60. this._determineBrowserMethod();
  61. this.bindEventListeners();
  62. }
  63. bindEventListeners() {
  64. this.body.emitter.on("dragStart", () => { this.dragging = true; });
  65. this.body.emitter.on("dragEnd", () => { this.dragging = false; });
  66. this.body.emitter.on("_resizeNodes", () => { this._resizeNodes(); });
  67. this.body.emitter.on("_redraw", () => {
  68. if (this.renderingActive === false) {
  69. this._redraw();
  70. }
  71. });
  72. this.body.emitter.on("_blockRedraw", () => {this.allowRedraw = false;});
  73. this.body.emitter.on("_allowRedraw", () => {this.allowRedraw = true; this.redrawRequested = false;});
  74. this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this));
  75. this.body.emitter.on("_startRendering", () => {
  76. this.renderRequests += 1;
  77. this.renderingActive = true;
  78. this._startRendering();
  79. });
  80. this.body.emitter.on("_stopRendering", () => {
  81. this.renderRequests -= 1;
  82. this.renderingActive = this.renderRequests > 0;
  83. this.renderTimer = undefined;
  84. });
  85. this.body.emitter.on('destroy', () => {
  86. this.renderRequests = 0;
  87. this.allowRedraw = false;
  88. this.renderingActive = false;
  89. if (this.requiresTimeout === true) {
  90. clearTimeout(this.renderTimer);
  91. }
  92. else {
  93. window.cancelAnimationFrame(this.renderTimer);
  94. }
  95. this.body.emitter.off();
  96. });
  97. }
  98. setOptions(options) {
  99. if (options !== undefined) {
  100. let fields = ['hideEdgesOnDrag','hideNodesOnDrag'];
  101. util.selectiveDeepExtend(fields,this.options, options);
  102. }
  103. }
  104. /**
  105. * Prepare the drawing of the next frame.
  106. *
  107. * Calls the callback when the next frame can or will be drawn.
  108. *
  109. * @param delay {number} - timeout case only, wait this number of milliseconds
  110. * @private
  111. */
  112. _requestNextFrame(callback, delay) {
  113. // During unit testing, it happens that the mock window object is reset while
  114. // the next frame is still pending. Then, either 'window' is not present, or
  115. // 'requestAnimationFrame()' is not present because it is not defined on the
  116. // mock window object.
  117. //
  118. // As a consequence, unrelated unit tests may appear to fail, even if the problem
  119. // described happens in the current unit test.
  120. //
  121. // This is not something that will happen in normal operation, but we still need
  122. // to take it into account.
  123. if (window === undefined) return;
  124. let timer;
  125. if (this.requiresTimeout === true) {
  126. // wait given number of milliseconds and perform the animation step function
  127. timer = window.setTimeout(callback, delay);
  128. }
  129. else {
  130. if (window.requestAnimationFrame) {
  131. timer = window.requestAnimationFrame(callback);
  132. }
  133. }
  134. return timer;
  135. }
  136. _startRendering() {
  137. if (this.renderingActive === true) {
  138. if (this.renderTimer === undefined) {
  139. this.renderTimer = this._requestNextFrame(this._renderStep.bind(this), this.simulationInterval);
  140. }
  141. }
  142. }
  143. _renderStep() {
  144. if (this.renderingActive === true) {
  145. // reset the renderTimer so a new scheduled animation step can be set
  146. this.renderTimer = undefined;
  147. if (this.requiresTimeout === true) {
  148. // this schedules a new simulation step
  149. this._startRendering();
  150. }
  151. this._redraw();
  152. if (this.requiresTimeout === false) {
  153. // this schedules a new simulation step
  154. this._startRendering();
  155. }
  156. }
  157. }
  158. /**
  159. * Redraw the network with the current data
  160. * chart will be resized too.
  161. */
  162. redraw() {
  163. this.body.emitter.emit('setSize');
  164. this._redraw();
  165. }
  166. /**
  167. * Redraw the network with the current data
  168. * @param hidden | Used to get the first estimate of the node sizes.
  169. * Only the nodes are drawn after which they are quickly drawn over.
  170. * @private
  171. */
  172. _requestRedraw() {
  173. if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedraw === true) {
  174. this.redrawRequested = true;
  175. this._requestNextFrame(() => {this._redraw(false);}, 0);
  176. }
  177. }
  178. _redraw(hidden = false) {
  179. if (this.allowRedraw === true) {
  180. this.body.emitter.emit("initRedraw");
  181. this.redrawRequested = false;
  182. // when the container div was hidden, this fixes it back up!
  183. if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) {
  184. this.canvas.setSize();
  185. }
  186. this.canvas.setTransform();
  187. let ctx = this.canvas.getContext();
  188. // clear the canvas
  189. let w = this.canvas.frame.canvas.clientWidth;
  190. let h = this.canvas.frame.canvas.clientHeight;
  191. ctx.clearRect(0, 0, w, h);
  192. // if the div is hidden, we stop the redraw here for performance.
  193. if (this.canvas.frame.clientWidth === 0) {
  194. return;
  195. }
  196. // set scaling and translation
  197. ctx.save();
  198. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  199. ctx.scale(this.body.view.scale, this.body.view.scale);
  200. ctx.beginPath();
  201. this.body.emitter.emit("beforeDrawing", ctx);
  202. ctx.closePath();
  203. if (hidden === false) {
  204. if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) {
  205. this._drawEdges(ctx);
  206. }
  207. }
  208. if (this.dragging === false || (this.dragging === true && this.options.hideNodesOnDrag === false)) {
  209. this._drawNodes(ctx, hidden);
  210. }
  211. ctx.beginPath();
  212. this.body.emitter.emit("afterDrawing", ctx);
  213. ctx.closePath();
  214. // restore original scaling and translation
  215. ctx.restore();
  216. if (hidden === true) {
  217. ctx.clearRect(0, 0, w, h);
  218. }
  219. }
  220. }
  221. /**
  222. * Redraw all nodes
  223. *
  224. * @param {CanvasRenderingContext2D} ctx
  225. * @param {Boolean} [alwaysShow]
  226. * @private
  227. */
  228. _resizeNodes() {
  229. this.canvas.setTransform();
  230. let ctx = this.canvas.getContext();
  231. ctx.save();
  232. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  233. ctx.scale(this.body.view.scale, this.body.view.scale);
  234. let nodes = this.body.nodes;
  235. let node;
  236. // resize all nodes
  237. for (let nodeId in nodes) {
  238. if (nodes.hasOwnProperty(nodeId)) {
  239. node = nodes[nodeId];
  240. node.resize(ctx);
  241. node.updateBoundingBox(ctx, node.selected);
  242. }
  243. }
  244. // restore original scaling and translation
  245. ctx.restore();
  246. }
  247. /**
  248. * Redraw all nodes
  249. *
  250. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  251. * @param {Boolean} [alwaysShow]
  252. * @private
  253. */
  254. _drawNodes(ctx, alwaysShow = false) {
  255. let nodes = this.body.nodes;
  256. let nodeIndices = this.body.nodeIndices;
  257. let node;
  258. let selected = [];
  259. let margin = 20;
  260. let topLeft = this.canvas.DOMtoCanvas({x:-margin,y:-margin});
  261. let bottomRight = this.canvas.DOMtoCanvas({
  262. x: this.canvas.frame.canvas.clientWidth+margin,
  263. y: this.canvas.frame.canvas.clientHeight+margin
  264. });
  265. let viewableArea = {top:topLeft.y,left:topLeft.x,bottom:bottomRight.y,right:bottomRight.x};
  266. // draw unselected nodes;
  267. for (let i = 0; i < nodeIndices.length; i++) {
  268. node = nodes[nodeIndices[i]];
  269. // set selected nodes aside
  270. if (node.isSelected()) {
  271. selected.push(nodeIndices[i]);
  272. }
  273. else {
  274. if (alwaysShow === true) {
  275. node.draw(ctx);
  276. }
  277. else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
  278. node.draw(ctx);
  279. }
  280. else {
  281. node.updateBoundingBox(ctx, node.selected);
  282. }
  283. }
  284. }
  285. // draw the selected nodes on top
  286. for (let i = 0; i < selected.length; i++) {
  287. node = nodes[selected[i]];
  288. node.draw(ctx);
  289. }
  290. }
  291. /**
  292. * Redraw all edges
  293. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  294. * @private
  295. */
  296. _drawEdges(ctx) {
  297. let edges = this.body.edges;
  298. let edgeIndices = this.body.edgeIndices;
  299. let edge;
  300. for (let i = 0; i < edgeIndices.length; i++) {
  301. edge = edges[edgeIndices[i]];
  302. if (edge.connected === true) {
  303. edge.draw(ctx);
  304. }
  305. }
  306. }
  307. /**
  308. * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
  309. * some implementations (safari and IE9) did not support requestAnimationFrame
  310. * @private
  311. */
  312. _determineBrowserMethod() {
  313. if (typeof window !== 'undefined') {
  314. let browserType = navigator.userAgent.toLowerCase();
  315. this.requiresTimeout = false;
  316. if (browserType.indexOf('msie 9.0') != -1) { // IE 9
  317. this.requiresTimeout = true;
  318. }
  319. else if (browserType.indexOf('safari') != -1) { // safari
  320. if (browserType.indexOf('chrome') <= -1) {
  321. this.requiresTimeout = true;
  322. }
  323. }
  324. }
  325. else {
  326. this.requiresTimeout = true;
  327. }
  328. }
  329. }
  330. export default CanvasRenderer;