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.

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