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.

410 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. /**
  43. * The canvas renderer
  44. */
  45. class CanvasRenderer {
  46. /**
  47. * @param {Object} body
  48. * @param {Canvas} canvas
  49. */
  50. constructor(body, canvas) {
  51. _initRequestAnimationFrame();
  52. this.body = body;
  53. this.canvas = canvas;
  54. this.redrawRequested = false;
  55. this.renderTimer = undefined;
  56. this.requiresTimeout = true;
  57. this.renderingActive = false;
  58. this.renderRequests = 0;
  59. this.allowRedraw = true;
  60. this.dragging = false;
  61. this.options = {};
  62. this.defaultOptions = {
  63. hideEdgesOnDrag: false,
  64. hideNodesOnDrag: false
  65. };
  66. util.extend(this.options, this.defaultOptions);
  67. this._determineBrowserMethod();
  68. this.bindEventListeners();
  69. }
  70. /**
  71. * Binds event listeners
  72. */
  73. bindEventListeners() {
  74. this.body.emitter.on("dragStart", () => { this.dragging = true; });
  75. this.body.emitter.on("dragEnd", () => { this.dragging = false; });
  76. this.body.emitter.on("_resizeNodes", () => { this._resizeNodes(); });
  77. this.body.emitter.on("_redraw", () => {
  78. if (this.renderingActive === false) {
  79. this._redraw();
  80. }
  81. });
  82. this.body.emitter.on("_blockRedraw", () => {this.allowRedraw = false;});
  83. this.body.emitter.on("_allowRedraw", () => {this.allowRedraw = true; this.redrawRequested = false;});
  84. this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this));
  85. this.body.emitter.on("_startRendering", () => {
  86. this.renderRequests += 1;
  87. this.renderingActive = true;
  88. this._startRendering();
  89. });
  90. this.body.emitter.on("_stopRendering", () => {
  91. this.renderRequests -= 1;
  92. this.renderingActive = this.renderRequests > 0;
  93. this.renderTimer = undefined;
  94. });
  95. this.body.emitter.on('destroy', () => {
  96. this.renderRequests = 0;
  97. this.allowRedraw = false;
  98. this.renderingActive = false;
  99. if (this.requiresTimeout === true) {
  100. clearTimeout(this.renderTimer);
  101. }
  102. else {
  103. window.cancelAnimationFrame(this.renderTimer);
  104. }
  105. this.body.emitter.off();
  106. });
  107. }
  108. /**
  109. *
  110. * @param {Object} options
  111. */
  112. setOptions(options) {
  113. if (options !== undefined) {
  114. let fields = ['hideEdgesOnDrag','hideNodesOnDrag'];
  115. util.selectiveDeepExtend(fields,this.options, options);
  116. }
  117. }
  118. /**
  119. * Prepare the drawing of the next frame.
  120. *
  121. * Calls the callback when the next frame can or will be drawn.
  122. *
  123. * @param {function} callback
  124. * @param {number} delay - timeout case only, wait this number of milliseconds
  125. * @returns {function|undefined}
  126. * @private
  127. */
  128. _requestNextFrame(callback, delay) {
  129. // During unit testing, it happens that the mock window object is reset while
  130. // the next frame is still pending. Then, either 'window' is not present, or
  131. // 'requestAnimationFrame()' is not present because it is not defined on the
  132. // mock window object.
  133. //
  134. // As a consequence, unrelated unit tests may appear to fail, even if the problem
  135. // described happens in the current unit test.
  136. //
  137. // This is not something that will happen in normal operation, but we still need
  138. // to take it into account.
  139. //
  140. var myWindow = window; // Grab a reference to reduce the possibility that 'window' is reset
  141. // while running this method.
  142. if (myWindow === undefined) return;
  143. let timer;
  144. if (this.requiresTimeout === true) {
  145. // wait given number of milliseconds and perform the animation step function
  146. timer = myWindow.setTimeout(callback, delay);
  147. }
  148. else {
  149. if (myWindow.requestAnimationFrame) {
  150. timer = myWindow.requestAnimationFrame(callback);
  151. }
  152. }
  153. return timer;
  154. }
  155. /**
  156. *
  157. * @private
  158. */
  159. _startRendering() {
  160. if (this.renderingActive === true) {
  161. if (this.renderTimer === undefined) {
  162. this.renderTimer = this._requestNextFrame(this._renderStep.bind(this), this.simulationInterval);
  163. }
  164. }
  165. }
  166. /**
  167. *
  168. * @private
  169. */
  170. _renderStep() {
  171. if (this.renderingActive === true) {
  172. // reset the renderTimer so a new scheduled animation step can be set
  173. this.renderTimer = undefined;
  174. if (this.requiresTimeout === true) {
  175. // this schedules a new simulation step
  176. this._startRendering();
  177. }
  178. this._redraw();
  179. if (this.requiresTimeout === false) {
  180. // this schedules a new simulation step
  181. this._startRendering();
  182. }
  183. }
  184. }
  185. /**
  186. * Redraw the network with the current data
  187. * chart will be resized too.
  188. */
  189. redraw() {
  190. this.body.emitter.emit('setSize');
  191. this._redraw();
  192. }
  193. /**
  194. * Redraw the network with the current data
  195. * @private
  196. */
  197. _requestRedraw() {
  198. if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedraw === true) {
  199. this.redrawRequested = true;
  200. this._requestNextFrame(() => {this._redraw(false);}, 0);
  201. }
  202. }
  203. /**
  204. * Redraw the network with the current data
  205. * @param {boolean} [hidden=false] | Used to get the first estimate of the node sizes.
  206. * Only the nodes are drawn after which they are quickly drawn over.
  207. * @private
  208. */
  209. _redraw(hidden = false) {
  210. if (this.allowRedraw === true) {
  211. this.body.emitter.emit("initRedraw");
  212. this.redrawRequested = false;
  213. // when the container div was hidden, this fixes it back up!
  214. if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) {
  215. this.canvas.setSize();
  216. }
  217. this.canvas.setTransform();
  218. let ctx = this.canvas.getContext();
  219. // clear the canvas
  220. let w = this.canvas.frame.canvas.clientWidth;
  221. let h = this.canvas.frame.canvas.clientHeight;
  222. ctx.clearRect(0, 0, w, h);
  223. // if the div is hidden, we stop the redraw here for performance.
  224. if (this.canvas.frame.clientWidth === 0) {
  225. return;
  226. }
  227. // set scaling and translation
  228. ctx.save();
  229. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  230. ctx.scale(this.body.view.scale, this.body.view.scale);
  231. ctx.beginPath();
  232. this.body.emitter.emit("beforeDrawing", ctx);
  233. ctx.closePath();
  234. if (hidden === false) {
  235. if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) {
  236. this._drawEdges(ctx);
  237. }
  238. }
  239. if (this.dragging === false || (this.dragging === true && this.options.hideNodesOnDrag === false)) {
  240. this._drawNodes(ctx, hidden);
  241. }
  242. ctx.beginPath();
  243. this.body.emitter.emit("afterDrawing", ctx);
  244. ctx.closePath();
  245. // restore original scaling and translation
  246. ctx.restore();
  247. if (hidden === true) {
  248. ctx.clearRect(0, 0, w, h);
  249. }
  250. }
  251. }
  252. /**
  253. * Redraw all nodes
  254. *
  255. * @param {CanvasRenderingContext2D} ctx
  256. * @param {boolean} [alwaysShow]
  257. * @private
  258. */
  259. _resizeNodes() {
  260. this.canvas.setTransform();
  261. let ctx = this.canvas.getContext();
  262. ctx.save();
  263. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  264. ctx.scale(this.body.view.scale, this.body.view.scale);
  265. let nodes = this.body.nodes;
  266. let node;
  267. // resize all nodes
  268. for (let nodeId in nodes) {
  269. if (nodes.hasOwnProperty(nodeId)) {
  270. node = nodes[nodeId];
  271. node.resize(ctx);
  272. node.updateBoundingBox(ctx, node.selected);
  273. }
  274. }
  275. // restore original scaling and translation
  276. ctx.restore();
  277. }
  278. /**
  279. * Redraw all nodes
  280. *
  281. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  282. * @param {boolean} [alwaysShow]
  283. * @private
  284. */
  285. _drawNodes(ctx, alwaysShow = false) {
  286. let nodes = this.body.nodes;
  287. let nodeIndices = this.body.nodeIndices;
  288. let node;
  289. let selected = [];
  290. let margin = 20;
  291. let topLeft = this.canvas.DOMtoCanvas({x:-margin,y:-margin});
  292. let bottomRight = this.canvas.DOMtoCanvas({
  293. x: this.canvas.frame.canvas.clientWidth+margin,
  294. y: this.canvas.frame.canvas.clientHeight+margin
  295. });
  296. let viewableArea = {top:topLeft.y,left:topLeft.x,bottom:bottomRight.y,right:bottomRight.x};
  297. // draw unselected nodes;
  298. for (let i = 0; i < nodeIndices.length; i++) {
  299. node = nodes[nodeIndices[i]];
  300. // set selected nodes aside
  301. if (node.isSelected()) {
  302. selected.push(nodeIndices[i]);
  303. }
  304. else {
  305. if (alwaysShow === true) {
  306. node.draw(ctx);
  307. }
  308. else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
  309. node.draw(ctx);
  310. }
  311. else {
  312. node.updateBoundingBox(ctx, node.selected);
  313. }
  314. }
  315. }
  316. // draw the selected nodes on top
  317. for (let i = 0; i < selected.length; i++) {
  318. node = nodes[selected[i]];
  319. node.draw(ctx);
  320. }
  321. }
  322. /**
  323. * Redraw all edges
  324. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  325. * @private
  326. */
  327. _drawEdges(ctx) {
  328. let edges = this.body.edges;
  329. let edgeIndices = this.body.edgeIndices;
  330. let edge;
  331. for (let i = 0; i < edgeIndices.length; i++) {
  332. edge = edges[edgeIndices[i]];
  333. if (edge.connected === true) {
  334. edge.draw(ctx);
  335. }
  336. }
  337. }
  338. /**
  339. * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
  340. * some implementations (safari and IE9) did not support requestAnimationFrame
  341. * @private
  342. */
  343. _determineBrowserMethod() {
  344. if (typeof window !== 'undefined') {
  345. let browserType = navigator.userAgent.toLowerCase();
  346. this.requiresTimeout = false;
  347. if (browserType.indexOf('msie 9.0') != -1) { // IE 9
  348. this.requiresTimeout = true;
  349. }
  350. else if (browserType.indexOf('safari') != -1) { // safari
  351. if (browserType.indexOf('chrome') <= -1) {
  352. this.requiresTimeout = true;
  353. }
  354. }
  355. }
  356. else {
  357. this.requiresTimeout = true;
  358. }
  359. }
  360. }
  361. export default CanvasRenderer;