SvgRenderer.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. /**
  2. * Module to generate a directed graph layout using a Sugiyama algorithm.
  3. * @module dgraph/SvgRenderer
  4. */
  5. import {svgCssUtil} from "./svgCssUtil.js";
  6. let d = document;
  7. /**
  8. * Returns an class to create the SVG of the directed graph.
  9. * @class module:dgraph/SvgRenderer
  10. * @param {Object} args parameters
  11. * @param {String} args.canvas name of HTMLElement to be used as canvas
  12. * @param {Object} args.gridSize grid dimensions
  13. * @param {Number} args.gridSize.meshWidth width of grid mesh
  14. * @param {Number} args.gridSize.meshHeight height of grid mesh
  15. * @param {Boolean} [args.renderGrid] render the grid
  16. * @param {Boolean} [args.invert] render the graph inverted
  17. */
  18. export class SvgRenderer {
  19. constructor(args) {
  20. this.canvas = null;
  21. /**
  22. * reference to svg element
  23. * @member {null|SVGElement}
  24. * @default null
  25. */
  26. this.svg = null;
  27. /**
  28. * svg namespace
  29. * @member {String}
  30. * @default
  31. */
  32. this.svgNs = 'http://www.w3.org/2000/svg';
  33. this.invert = false;
  34. /**
  35. * label prefix of the grid x-lines
  36. * @member {string}
  37. * @default
  38. */
  39. this.gridLabel = 'layer';
  40. /**
  41. * render the grid.
  42. * @member {Boolean}
  43. * @default
  44. */
  45. this.renderGrid = true;
  46. Object.assign(this, args);
  47. this.canvas = d.getElementById(args.canvas);
  48. }
  49. /**
  50. * Creates the SVG canvas.
  51. * @param {Number} width width of svg canvas
  52. * @param {Number} height height of svg canvas
  53. */
  54. initSVG(width, height) {
  55. this.svg = d.createElementNS(this.svgNs, 'svg');
  56. this.svg.setAttribute('version', '1.1');
  57. this.svg.setAttribute('width', width);
  58. this.svg.setAttribute('height', height);
  59. this.svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
  60. this.defs = d.createElementNS(this.svgNs, 'defs'); // holds markers, such as arrow heads
  61. this.svg.appendChild(this.defs);
  62. this.svgRoot = d.createElementNS(this.svgNs, 'g'); // holds all nodes and edges for simple transformation
  63. this.svg.appendChild(this.svgRoot);
  64. this.canvas.appendChild(this.svg);
  65. }
  66. /**
  67. * Creates a svg graph node.
  68. * @param {graphNode} node
  69. * @return {SVGElement} svg group element
  70. */
  71. createNode(node) {
  72. var text, dist, tspan, circle,
  73. group = d.createElementNS(this.svgNs, 'g');
  74. svgCssUtil.add(group, 'node');
  75. group.setAttribute('cx', node.lx);
  76. group.setAttribute('cy', node.ly);
  77. // create circle
  78. circle = d.createElementNS(this.svgNs, 'circle');
  79. circle.setAttribute('r', '5');
  80. group.appendChild(circle);
  81. // create node label
  82. text = d.createElementNS(this.svgNs, 'text');
  83. text.setAttribute('text-anchor', 'middle');
  84. text.setAttribute('x', '0px');
  85. dist = this.invert ? -1 : 1;
  86. tspan = d.createElementNS(this.svgNs, 'tspan');
  87. tspan.setAttribute('x', '0px');
  88. tspan.setAttribute('y', 1.5 * dist + 'em');
  89. tspan.appendChild(d.createTextNode(node.label));
  90. text.appendChild(tspan);
  91. group.appendChild(text);
  92. return group;
  93. }
  94. /**
  95. * Draws the node on the canvas.
  96. * @param {graphNode} node
  97. * @param {Array} nodes graph node list
  98. * @return {graphNode} node
  99. */
  100. drawNode(node, nodes) {
  101. var x, y, shift = 1;
  102. x = (shift + Number(node.getAttribute('cx'))) * this.gridSize.meshWidth;
  103. if (this.invert) {
  104. y = (Number(node.getAttribute('cy')) * -1 + nodes.length) * this.gridSize.meshHeight;
  105. }
  106. else {
  107. y = (shift + Number(node.getAttribute('cy'))) * this.gridSize.meshHeight;
  108. }
  109. node.setAttribute('transform', 'translate(' + x + ',' + y + ')');
  110. this.svgRoot.appendChild(node);
  111. return node;
  112. }
  113. /**
  114. * Draws the grid on the canvas.
  115. * Note: After using compact we can not simply calculate
  116. * the width from the number of nodes per layer.
  117. * The height is calculated from the number of layers.
  118. * @param {Number} width width of the grid
  119. * @param {Number} height height of the grid
  120. */
  121. drawGrid(width, height) {
  122. var i, text, level,
  123. numVertical = width / this.gridSize.meshWidth + 1,
  124. numHorizontal = height / this.gridSize.meshHeight;
  125. for (i = 1; i < numHorizontal; i++) {
  126. this.drawLine(0, i, width, i);
  127. if (i > 0) {
  128. text = d.createElementNS(this.svgNs, 'text');
  129. level = this.invert ? (i + 1) * -1 + numHorizontal : i - 1;
  130. text.setAttribute('text-anchor', 'left');
  131. text.appendChild(d.createTextNode(this.gridLabel + ' ' + level));
  132. text.setAttribute('x', 0);
  133. text.setAttribute('y', i * this.gridSize.meshHeight + 3);
  134. this.svgRoot.appendChild(text);
  135. }
  136. }
  137. for (i = 1; i < numVertical + 1; i++) {
  138. this.drawLine(i, 0, i, height);
  139. }
  140. }
  141. /**
  142. * Draws a line on the canvas.
  143. * @param {Number} x1 x-coord of line start
  144. * @param {Number} y1 y-coord of line start
  145. * @param {Number} x2 x-coord of line end
  146. * @param {Number} y2 y-coord of line end
  147. * @return {Object} SVGLine
  148. */
  149. drawLine(x1, y1, x2, y2) {
  150. var width = this.gridSize.meshWidth,
  151. height = this.gridSize.meshHeight,
  152. line = d.createElementNS(this.svgNs, 'line');
  153. line.setAttribute('x1', x1 * width);
  154. line.setAttribute('y1', y1 * height);
  155. line.setAttribute('x2', x2 * width);
  156. line.setAttribute('y2', y2 * height);
  157. this.svgRoot.appendChild(line);
  158. return line;
  159. }
  160. /**
  161. * Creates a reusable arrow head.
  162. * The arrow head is appended to the SVG defs element as a marker element
  163. * and can be reused.
  164. * @param {String} id id of marker
  165. * @return {Object} SVGMarker
  166. */
  167. createArrowHead(id) {
  168. var p, nodeRadius,
  169. marker = document.createElementNS(this.svgNs, 'marker');
  170. marker.setAttribute('id', id);
  171. marker.setAttribute('markerWidth', 11);
  172. marker.setAttribute('markerHeight', 11);
  173. marker.setAttribute('orient', 'auto');
  174. // start arrow head where circle ends
  175. // TODO: find better method to get x
  176. p = document.createElementNS(this.svgNs, 'polyline');
  177. p.setAttribute('points', '0,0 10,5 0,10 1,5');
  178. nodeRadius = 10; // TODO: find generic solution
  179. marker.setAttribute('refX', nodeRadius + Number(marker.getAttribute('markerWidth')));
  180. marker.setAttribute('refY', marker.getAttribute('markerHeight') / 2);
  181. marker.appendChild(p);
  182. this.defs.appendChild(marker);
  183. return marker;
  184. }
  185. /**
  186. * Draws the edge on the canvas.
  187. * The edge is drawn from node1 to node2 with arrow head.
  188. * @param {graphNode} node1 start node
  189. * @param {graphNode} node2 end node
  190. * @param {Object} arrow SVGMarker
  191. * @param {Array} nodes graph node list
  192. * @return {Object} edge
  193. */
  194. drawEdge(node1, node2, arrow, nodes) {
  195. var x1, x2, y1, y2,
  196. shift = 1,
  197. width = this.gridSize.meshWidth,
  198. height = this.gridSize.meshHeight,
  199. edge = document.createElementNS(this.svgNs, 'polyline');
  200. x1 = (shift + node1.lx) * width;
  201. x2 = (shift + node2.lx) * width;
  202. if (this.invert) {
  203. y1 = (node1.ly * -1 + nodes.length) * height;
  204. y2 = (node2.ly * -1 + nodes.length) * height;
  205. }
  206. else {
  207. y1 = (shift + node1.ly) * height;
  208. y2 = (shift + node2.ly) * height;
  209. }
  210. svgCssUtil.add(edge, 'edge');
  211. edge.setAttribute('points', x1 + ',' + y1 + ' ' + x2 + ',' + y2);
  212. if (!node2.virt) {
  213. edge.setAttribute('marker-end', 'url(#' + arrow.id + ')');
  214. }
  215. this.svgRoot.appendChild(edge);
  216. return edge;
  217. }
  218. /**
  219. * Places the nodes on the canvas.
  220. * @param {Array} nodes graph nodes list
  221. */
  222. placeNodes(nodes) {
  223. var i, numRow = nodes.length,
  224. j, numCol, node, svgNode;
  225. for (i = 0; i < numRow; i++) {
  226. numCol = nodes[i].length;
  227. for (j = 0; j < numCol; j++) {
  228. node = nodes[i][j];
  229. if (!node.virt) {
  230. svgNode = this.createNode(nodes[i][j]);
  231. nodes[i][j].svgNode = this.drawNode(svgNode, nodes); // save back for later reference
  232. }
  233. }
  234. }
  235. }
  236. /**
  237. * Places the edges on the canvas.
  238. * @param {Array} nodes graph node list
  239. */
  240. placeEdges(nodes) {
  241. var i, j, l, z, x, y, adjNode, numCol, node,
  242. numRow = nodes.length,
  243. arrow = this.createArrowHead('arrow');
  244. for (i = 0; i < numRow; i++) {
  245. numCol = nodes[i].length;
  246. for (j = 0; j < numCol; j++) {
  247. node = nodes[i][j];
  248. if (node.trgNodes) {
  249. l = node.trgNodes.length;
  250. node.svgEdges = [];
  251. for (z = 0; z < l; z++) {
  252. x = node.trgNodes[z][0];
  253. y = node.trgNodes[z][1];
  254. adjNode = nodes[y][x];
  255. node.svgEdges[z] = this.drawEdge(node, adjNode, arrow, nodes);
  256. }
  257. }
  258. }
  259. }
  260. }
  261. /**
  262. * Clear the canvas.
  263. */
  264. clearCanvas() {
  265. if (this.canvas) {
  266. this.canvas.innerHTML = '';
  267. }
  268. }
  269. /**
  270. * Render graph as SVG.
  271. * @param {Object} graph
  272. */
  273. render(graph) {
  274. var width = (graph.getGraphWidth() + 1) * this.gridSize.meshWidth,
  275. height = this.gridSize.meshHeight * (graph.numLayer + 1);
  276. this.initSVG(width, height);
  277. if (this.renderGrid) {
  278. this.drawGrid(width, height);
  279. }
  280. this.placeEdges(graph.nodes);
  281. this.placeNodes(graph.nodes);
  282. }
  283. }