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
12 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. if (typeof window === 'undefined') return; // Doing `if (window === undefined)` does not work here!
  141. let timer;
  142. var myWindow = window; // Grab a reference to reduce the possibility that 'window' is reset
  143. // while running this method.
  144. if (this.requiresTimeout === true) {
  145. // wait given number of milliseconds and perform the animation step function
  146. timer = myWindow.setTimeout(callback, delay);
  147. } else {
  148. if (myWindow.requestAnimationFrame) {
  149. timer = myWindow.requestAnimationFrame(callback);
  150. }
  151. }
  152. return timer;
  153. }
  154. /**
  155. *
  156. * @private
  157. */
  158. _startRendering() {
  159. if (this.renderingActive === true) {
  160. if (this.renderTimer === undefined) {
  161. this.renderTimer = this._requestNextFrame(this._renderStep.bind(this), this.simulationInterval);
  162. }
  163. }
  164. }
  165. /**
  166. *
  167. * @private
  168. */
  169. _renderStep() {
  170. if (this.renderingActive === true) {
  171. // reset the renderTimer so a new scheduled animation step can be set
  172. this.renderTimer = undefined;
  173. if (this.requiresTimeout === true) {
  174. // this schedules a new simulation step
  175. this._startRendering();
  176. }
  177. this._redraw();
  178. if (this.requiresTimeout === false) {
  179. // this schedules a new simulation step
  180. this._startRendering();
  181. }
  182. }
  183. }
  184. /**
  185. * Redraw the network with the current data
  186. * chart will be resized too.
  187. */
  188. redraw() {
  189. this.body.emitter.emit('setSize');
  190. this._redraw();
  191. }
  192. /**
  193. * Redraw the network with the current data
  194. * @private
  195. */
  196. _requestRedraw() {
  197. if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedraw === true) {
  198. this.redrawRequested = true;
  199. this._requestNextFrame(() => {this._redraw(false);}, 0);
  200. }
  201. }
  202. /**
  203. * Redraw the network with the current data
  204. * @param {boolean} [hidden=false] | Used to get the first estimate of the node sizes.
  205. * Only the nodes are drawn after which they are quickly drawn over.
  206. * @private
  207. */
  208. _redraw(hidden = false) {
  209. if (this.allowRedraw === true) {
  210. this.body.emitter.emit("initRedraw");
  211. this.redrawRequested = false;
  212. // when the container div was hidden, this fixes it back up!
  213. if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) {
  214. this.canvas.setSize();
  215. }
  216. this.canvas.setTransform();
  217. let ctx = this.canvas.getContext();
  218. // clear the canvas
  219. let w = this.canvas.frame.canvas.clientWidth;
  220. let h = this.canvas.frame.canvas.clientHeight;
  221. ctx.clearRect(0, 0, w, h);
  222. // if the div is hidden, we stop the redraw here for performance.
  223. if (this.canvas.frame.clientWidth === 0) {
  224. return;
  225. }
  226. // set scaling and translation
  227. ctx.save();
  228. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  229. ctx.scale(this.body.view.scale, this.body.view.scale);
  230. ctx.beginPath();
  231. this.body.emitter.emit("beforeDrawing", ctx);
  232. ctx.closePath();
  233. if (hidden === false) {
  234. if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) {
  235. this._drawEdges(ctx);
  236. }
  237. }
  238. if (this.dragging === false || (this.dragging === true && this.options.hideNodesOnDrag === false)) {
  239. this._drawNodes(ctx, hidden);
  240. }
  241. ctx.beginPath();
  242. this.body.emitter.emit("afterDrawing", ctx);
  243. ctx.closePath();
  244. // restore original scaling and translation
  245. ctx.restore();
  246. if (hidden === true) {
  247. ctx.clearRect(0, 0, w, h);
  248. }
  249. }
  250. }
  251. /**
  252. * Redraw all nodes
  253. *
  254. * @param {CanvasRenderingContext2D} ctx
  255. * @param {boolean} [alwaysShow]
  256. * @private
  257. */
  258. _resizeNodes() {
  259. this.canvas.setTransform();
  260. let ctx = this.canvas.getContext();
  261. ctx.save();
  262. ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
  263. ctx.scale(this.body.view.scale, this.body.view.scale);
  264. let nodes = this.body.nodes;
  265. let node;
  266. // resize all nodes
  267. for (let nodeId in nodes) {
  268. if (nodes.hasOwnProperty(nodeId)) {
  269. node = nodes[nodeId];
  270. node.resize(ctx);
  271. node.updateBoundingBox(ctx, node.selected);
  272. }
  273. }
  274. // restore original scaling and translation
  275. ctx.restore();
  276. }
  277. /**
  278. * Redraw all nodes
  279. *
  280. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  281. * @param {boolean} [alwaysShow]
  282. * @private
  283. */
  284. _drawNodes(ctx, alwaysShow = false) {
  285. let nodes = this.body.nodes;
  286. let nodeIndices = this.body.nodeIndices;
  287. let node;
  288. let selected = [];
  289. let margin = 20;
  290. let topLeft = this.canvas.DOMtoCanvas({x:-margin,y:-margin});
  291. let bottomRight = this.canvas.DOMtoCanvas({
  292. x: this.canvas.frame.canvas.clientWidth+margin,
  293. y: this.canvas.frame.canvas.clientHeight+margin
  294. });
  295. let viewableArea = {top:topLeft.y,left:topLeft.x,bottom:bottomRight.y,right:bottomRight.x};
  296. // draw unselected nodes;
  297. for (let i = 0; i < nodeIndices.length; i++) {
  298. node = nodes[nodeIndices[i]];
  299. // set selected nodes aside
  300. if (node.isSelected()) {
  301. selected.push(nodeIndices[i]);
  302. }
  303. else {
  304. if (alwaysShow === true) {
  305. node.draw(ctx);
  306. }
  307. else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
  308. node.draw(ctx);
  309. }
  310. else {
  311. node.updateBoundingBox(ctx, node.selected);
  312. }
  313. }
  314. }
  315. // draw the selected nodes on top
  316. for (let i = 0; i < selected.length; i++) {
  317. node = nodes[selected[i]];
  318. node.draw(ctx);
  319. }
  320. }
  321. /**
  322. * Redraw all edges
  323. * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
  324. * @private
  325. */
  326. _drawEdges(ctx) {
  327. let edges = this.body.edges;
  328. let edgeIndices = this.body.edgeIndices;
  329. let edge;
  330. for (let i = 0; i < edgeIndices.length; i++) {
  331. edge = edges[edgeIndices[i]];
  332. if (edge.connected === true) {
  333. edge.draw(ctx);
  334. }
  335. }
  336. }
  337. /**
  338. * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
  339. * some implementations (safari and IE9) did not support requestAnimationFrame
  340. * @private
  341. */
  342. _determineBrowserMethod() {
  343. if (typeof window !== 'undefined') {
  344. let browserType = navigator.userAgent.toLowerCase();
  345. this.requiresTimeout = false;
  346. if (browserType.indexOf('msie 9.0') != -1) { // IE 9
  347. this.requiresTimeout = true;
  348. }
  349. else if (browserType.indexOf('safari') != -1) { // safari
  350. if (browserType.indexOf('chrome') <= -1) {
  351. this.requiresTimeout = true;
  352. }
  353. }
  354. }
  355. else {
  356. this.requiresTimeout = true;
  357. }
  358. }
  359. }
  360. export default CanvasRenderer;