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.

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