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.

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