view-grapher.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. /* jshint esversion: 6 */
  2. /* eslint "indent": [ "error", 4, { "SwitchCase": 1 } ] */
  3. var grapher = grapher || {};
  4. var dagre = dagre || require('dagre');
  5. grapher.Renderer = class {
  6. constructor(document, svgElement) {
  7. this._document = document;
  8. this._svgElement = svgElement;
  9. }
  10. render(graph) {
  11. var svgClusterGroup = this.createElement('g');
  12. svgClusterGroup.setAttribute('id', 'clusters');
  13. svgClusterGroup.setAttribute('class', 'clusters');
  14. this._svgElement.appendChild(svgClusterGroup);
  15. var svgEdgePathGroup = this.createElement('g');
  16. svgEdgePathGroup.setAttribute('id', 'edge-paths');
  17. svgEdgePathGroup.setAttribute('class', 'edge-paths');
  18. this._svgElement.appendChild(svgEdgePathGroup);
  19. var svgEdgeLabelGroup = this.createElement('g');
  20. svgEdgeLabelGroup.setAttribute('id', 'edge-labels');
  21. svgEdgeLabelGroup.setAttribute('class', 'edge-labels');
  22. this._svgElement.appendChild(svgEdgeLabelGroup);
  23. var svgNodeGroup = this.createElement('g');
  24. svgNodeGroup.setAttribute('id', 'nodes');
  25. svgNodeGroup.setAttribute('class', 'nodes');
  26. this._svgElement.appendChild(svgNodeGroup);
  27. var nodeId;
  28. var node;
  29. for (nodeId of graph.nodes()) {
  30. if (graph.children(nodeId).length == 0) {
  31. node = graph.node(nodeId);
  32. var element = this.createElement('g');
  33. if (node.id) {
  34. element.setAttribute('id', node.id);
  35. }
  36. element.setAttribute('class', Object.prototype.hasOwnProperty.call(node, 'class') ? ('node ' + node.class) : 'node');
  37. element.style.opacity = 0;
  38. var container = this.createElement('g');
  39. container.appendChild(node.label);
  40. element.appendChild(container);
  41. svgNodeGroup.appendChild(element);
  42. var nodeBox = node.label.getBBox();
  43. var nodeX = - nodeBox.width / 2;
  44. var nodeY = - nodeBox.height / 2;
  45. container.setAttribute('transform', 'translate(' + nodeX + ',' + nodeY + ')');
  46. node.width = nodeBox.width;
  47. node.height = nodeBox.height;
  48. node.element = element;
  49. }
  50. }
  51. var edgeId;
  52. var edge;
  53. for (edgeId of graph.edges()) {
  54. edge = graph.edge(edgeId);
  55. if (edge.label) {
  56. var tspan = this.createElement('tspan');
  57. tspan.setAttribute('xml:space', 'preserve');
  58. tspan.setAttribute('dy', '1em');
  59. tspan.setAttribute('x', '1');
  60. tspan.appendChild(this._document.createTextNode(edge.label));
  61. var text = this.createElement('text');
  62. text.appendChild(tspan);
  63. var textContainer = this.createElement('g');
  64. textContainer.appendChild(text);
  65. var labelElement = this.createElement('g');
  66. labelElement.style.opacity = 0;
  67. labelElement.setAttribute('class', 'edge-label');
  68. labelElement.appendChild(textContainer);
  69. svgEdgeLabelGroup.appendChild(labelElement);
  70. var edgeBox = textContainer.getBBox();
  71. var edgeX = - edgeBox.width / 2;
  72. var edgeY = - edgeBox.height / 2;
  73. textContainer.setAttribute('transform', 'translate(' + edgeX + ',' + edgeY + ')');
  74. edge.width = edgeBox.width;
  75. edge.height = edgeBox.height;
  76. edge.labelElement = labelElement;
  77. }
  78. }
  79. dagre.layout(graph);
  80. for (nodeId of graph.nodes()) {
  81. if (graph.children(nodeId).length == 0) {
  82. node = graph.node(nodeId);
  83. node.element.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');
  84. node.element.style.opacity = 1;
  85. delete node.element;
  86. }
  87. }
  88. for (edgeId of graph.edges()) {
  89. edge = graph.edge(edgeId);
  90. if (edge.labelElement) {
  91. edge.labelElement.setAttribute('transform', 'translate(' + edge.x + ',' + edge.y + ')');
  92. edge.labelElement.style.opacity = 1;
  93. delete edge.labelElement;
  94. }
  95. }
  96. var edgePathGroupDefs = this.createElement('defs');
  97. svgEdgePathGroup.appendChild(edgePathGroupDefs);
  98. var marker = this.createElement('marker');
  99. marker.setAttribute('id', 'arrowhead-vee');
  100. marker.setAttribute('viewBox', '0 0 10 10');
  101. marker.setAttribute('refX', 9);
  102. marker.setAttribute('refY', 5);
  103. marker.setAttribute('markerUnits', 'strokeWidth');
  104. marker.setAttribute('markerWidth', 8);
  105. marker.setAttribute('markerHeight', 6);
  106. marker.setAttribute('orient', 'auto');
  107. edgePathGroupDefs.appendChild(marker);
  108. var markerPath = this.createElement('path');
  109. markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
  110. markerPath.style.setProperty('stroke-width', 1);
  111. markerPath.style.setProperty('stroke-dasharray', '1,0');
  112. marker.appendChild(markerPath);
  113. for (edgeId of graph.edges()) {
  114. edge = graph.edge(edgeId);
  115. var edgePath = grapher.Renderer._computeCurvePath(edge, graph.node(edgeId.v), graph.node(edgeId.w));
  116. var edgeElement = this.createElement('path');
  117. edgeElement.setAttribute('class', Object.prototype.hasOwnProperty.call(edge, 'class') ? ('edge-path ' + edge.class) : 'edge-path');
  118. edgeElement.setAttribute('d', edgePath);
  119. edgeElement.setAttribute('marker-end', 'url(#arrowhead-vee)');
  120. if (edge.id) {
  121. edgeElement.setAttribute('id', edge.id);
  122. }
  123. svgEdgePathGroup.appendChild(edgeElement);
  124. }
  125. for (nodeId of graph.nodes()) {
  126. if (graph.children(nodeId).length > 0) {
  127. node = graph.node(nodeId);
  128. var nodeElement = this.createElement('g');
  129. nodeElement.setAttribute('class', 'cluster');
  130. nodeElement.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');
  131. var rect = this.createElement('rect');
  132. rect.setAttribute('x', - node.width / 2);
  133. rect.setAttribute('y', - node.height / 2 );
  134. rect.setAttribute('width', node.width);
  135. rect.setAttribute('height', node.height);
  136. if (node.rx) {
  137. rect.setAttribute('rx', node.rx);
  138. }
  139. if (node.ry) {
  140. rect.setAttribute('ry', node.ry);
  141. }
  142. nodeElement.appendChild(rect);
  143. svgClusterGroup.appendChild(nodeElement);
  144. }
  145. }
  146. }
  147. createElement(name) {
  148. return this._document.createElementNS('http://www.w3.org/2000/svg', name);
  149. }
  150. static _computeCurvePath(edge, tail, head) {
  151. var points = edge.points.slice(1, edge.points.length - 1);
  152. points.unshift(grapher.Renderer.intersectRect(tail, points[0]));
  153. points.push(grapher.Renderer.intersectRect(head, points[points.length - 1]));
  154. var path = new Path();
  155. var curve = new Curve(path);
  156. for (var i = 0; i < points.length; i++) {
  157. var point = points[i];
  158. if (i == 0) {
  159. curve.lineStart();
  160. }
  161. curve.point(point.x, point.y);
  162. if (i == points.length - 1) {
  163. curve.lineEnd();
  164. }
  165. }
  166. return path.data;
  167. }
  168. static intersectRect(node, point) {
  169. var x = node.x;
  170. var y = node.y;
  171. var dx = point.x - x;
  172. var dy = point.y - y;
  173. var w = node.width / 2;
  174. var h = node.height / 2;
  175. var sx;
  176. var sy;
  177. if (Math.abs(dy) * w > Math.abs(dx) * h) {
  178. if (dy < 0) {
  179. h = -h;
  180. }
  181. sx = dy === 0 ? 0 : h * dx / dy;
  182. sy = h;
  183. }
  184. else {
  185. if (dx < 0) {
  186. w = -w;
  187. }
  188. sx = w;
  189. sy = dx === 0 ? 0 : w * dy / dx;
  190. }
  191. return {x: x + sx, y: y + sy};
  192. }
  193. };
  194. grapher.NodeElement = class {
  195. constructor(document) {
  196. this._document = document;
  197. this._blocks = [];
  198. }
  199. block(type) {
  200. this._block = null;
  201. switch (type) {
  202. case 'header':
  203. this._block = new grapher.NodeElement.Header(this._document);
  204. break;
  205. case 'list':
  206. this._block = new grapher.NodeElement.List(this._document);
  207. break;
  208. }
  209. this._blocks.push(this._block);
  210. return this._block;
  211. }
  212. format(contextElement) {
  213. var rootElement = this.createElement('g');
  214. contextElement.appendChild(rootElement);
  215. var width = 0;
  216. var height = 0;
  217. var tops = [];
  218. for (var block of this._blocks) {
  219. tops.push(height);
  220. block.layout(rootElement);
  221. if (width < block.width) {
  222. width = block.width;
  223. }
  224. height = height + block.height;
  225. }
  226. for (var i = 0; i < this._blocks.length; i++) {
  227. var top = tops.shift();
  228. this._blocks[i].update(rootElement, top, width, i == 0, i == this._blocks.length - 1);
  229. }
  230. var borderElement = this.createElement('path');
  231. borderElement.setAttribute('class', [ 'node', 'border' ].join(' '));
  232. borderElement.setAttribute('d', grapher.NodeElement.roundedRect(0, 0, width, height, true, true, true, true));
  233. rootElement.appendChild(borderElement);
  234. contextElement.innerHTML = '';
  235. return rootElement;
  236. }
  237. static roundedRect(x, y, width, height, r1, r2, r3, r4) {
  238. var radius = 5;
  239. r1 = r1 ? radius : 0;
  240. r2 = r2 ? radius : 0;
  241. r3 = r3 ? radius : 0;
  242. r4 = r4 ? radius : 0;
  243. return "M" + (x + r1) + "," + y +
  244. "h" + (width - r1 - r2) +
  245. "a" + r2 + "," + r2 + " 0 0 1 " + r2 + "," + r2 +
  246. "v" + (height - r2 - r3) +
  247. "a" + r3 + "," + r3 + " 0 0 1 " + -r3 + "," + r3 +
  248. "h" + (r3 + r4 - width) +
  249. "a" + r4 + "," + r4 + " 0 0 1 " + -r4 + "," + -r4 +
  250. 'v' + (-height + r4 + r1) +
  251. "a" + r1 + "," + r1 + " 0 0 1 " + r1 + "," + -r1 +
  252. "z";
  253. }
  254. createElement(name) {
  255. return this._document.createElementNS('http://www.w3.org/2000/svg', name);
  256. }
  257. };
  258. grapher.NodeElement.Header = class {
  259. constructor(document) {
  260. this._document = document;
  261. this._items = [];
  262. }
  263. add(id, classList, content, tooltip, handler) {
  264. this._items.push({
  265. id: id,
  266. classList: classList,
  267. content: content,
  268. tooltip: tooltip,
  269. handler: handler
  270. });
  271. }
  272. layout(parentElement) {
  273. this._width = 0;
  274. this._height = 0;
  275. this._elements = [];
  276. var x = 0;
  277. var y = 0;
  278. for (var item of this._items) {
  279. var yPadding = 4;
  280. var xPadding = 7;
  281. var element = this.createElement('g');
  282. var classList = [ 'node-item' ];
  283. parentElement.appendChild(element);
  284. var pathElement = this.createElement('path');
  285. var textElement = this.createElement('text');
  286. element.appendChild(pathElement);
  287. element.appendChild(textElement);
  288. if (item.classList) {
  289. classList = classList.concat(item.classList);
  290. }
  291. element.setAttribute('class', classList.join(' '));
  292. if (item.id) {
  293. element.setAttribute('id', item.id);
  294. }
  295. if (item.handler) {
  296. element.addEventListener('click', item.handler);
  297. }
  298. if (item.tooltip) {
  299. var titleElement = this.createElement('title');
  300. titleElement.textContent = item.tooltip;
  301. element.appendChild(titleElement);
  302. }
  303. if (item.content) {
  304. textElement.textContent = item.content;
  305. }
  306. var boundingBox = textElement.getBBox();
  307. var width = boundingBox.width + xPadding + xPadding;
  308. var height = boundingBox.height + yPadding + yPadding;
  309. this._elements.push({
  310. 'group': element,
  311. 'text': textElement,
  312. 'path': pathElement,
  313. 'x': x, 'y': y,
  314. 'width': width, 'height': height,
  315. 'tx': xPadding, 'ty': yPadding - boundingBox.y,
  316. });
  317. x += width;
  318. if (this._height < height) {
  319. this._height = height;
  320. }
  321. if (x > this._width) {
  322. this._width = x;
  323. }
  324. }
  325. }
  326. get width() {
  327. return this._width;
  328. }
  329. get height() {
  330. return this._height;
  331. }
  332. update(parentElement, top, width, first, last) {
  333. var dx = width - this._width;
  334. var i;
  335. var element;
  336. for (i = 0; i < this._elements.length; i++) {
  337. element = this._elements[i];
  338. if (i == 0) {
  339. element.width = element.width + dx;
  340. }
  341. else {
  342. element.x = element.x + dx;
  343. element.tx = element.tx + dx;
  344. }
  345. element.y = element.y + top;
  346. }
  347. for (i = 0; i < this._elements.length; i++) {
  348. element = this._elements[i];
  349. element.group.setAttribute('transform', 'translate(' + element.x + ',' + element.y + ')');
  350. var r1 = i == 0 && first;
  351. var r2 = i == this._elements.length - 1 && first;
  352. var r3 = i == this._elements.length - 1 && last;
  353. var r4 = i == 0 && last;
  354. element.path.setAttribute('d', grapher.NodeElement.roundedRect(0, 0, element.width, element.height, r1, r2, r3, r4));
  355. element.text.setAttribute('x', 6);
  356. element.text.setAttribute('y', element.ty);
  357. }
  358. var lineElement;
  359. for (i = 0; i < this._elements.length; i++) {
  360. element = this._elements[i];
  361. if (i != 0) {
  362. lineElement = this.createElement('line');
  363. lineElement.setAttribute('class', 'node');
  364. lineElement.setAttribute('x1', element.x);
  365. lineElement.setAttribute('x2', element.x);
  366. lineElement.setAttribute('y1', top);
  367. lineElement.setAttribute('y2', top + this._height);
  368. parentElement.appendChild(lineElement);
  369. }
  370. }
  371. if (!first) {
  372. lineElement = this.createElement('line');
  373. lineElement.setAttribute('class', 'node');
  374. lineElement.setAttribute('x1', 0);
  375. lineElement.setAttribute('x2', width);
  376. lineElement.setAttribute('y1', top);
  377. lineElement.setAttribute('y2', top);
  378. parentElement.appendChild(lineElement);
  379. }
  380. }
  381. createElement(name) {
  382. return this._document.createElementNS('http://www.w3.org/2000/svg', name);
  383. }
  384. };
  385. grapher.NodeElement.List = class {
  386. constructor(document) {
  387. this._document = document;
  388. this._items = [];
  389. }
  390. add(id, name, value, tooltip, separator) {
  391. this._items.push({ id: id, name: name, value: value, tooltip: tooltip, separator: separator });
  392. }
  393. get handler() {
  394. return this._handler;
  395. }
  396. set handler(handler) {
  397. this._handler = handler;
  398. }
  399. layout(parentElement) {
  400. this._width = 0;
  401. this._height = 0;
  402. var x = 0;
  403. var y = 0;
  404. this._element = this.createElement('g');
  405. this._element.setAttribute('class', 'node-attribute');
  406. parentElement.appendChild(this._element);
  407. if (this._handler) {
  408. this._element.addEventListener('click', this._handler);
  409. }
  410. this._backgroundElement = this.createElement('path');
  411. this._element.appendChild(this._backgroundElement);
  412. this._element.setAttribute('transform', 'translate(' + x + ',' + y + ')');
  413. this._height += 3;
  414. for (var item of this._items) {
  415. var yPadding = 1;
  416. var xPadding = 6;
  417. var textElement = this.createElement('text');
  418. if (item.id) {
  419. textElement.setAttribute('id', item.id);
  420. }
  421. textElement.setAttribute('xml:space', 'preserve');
  422. this._element.appendChild(textElement);
  423. if (item.tooltip) {
  424. var titleElement = this.createElement('title');
  425. titleElement.textContent = item.tooltip;
  426. textElement.appendChild(titleElement);
  427. }
  428. var textNameElement = this.createElement('tspan');
  429. textNameElement.textContent = item.name;
  430. if (item.separator.trim() != '=') {
  431. textNameElement.style.fontWeight = 'bold';
  432. }
  433. textElement.appendChild(textNameElement);
  434. var textValueElement = this.createElement('tspan');
  435. textValueElement.textContent = item.separator + item.value;
  436. textElement.appendChild(textValueElement);
  437. var size = textElement.getBBox();
  438. var width = xPadding + size.width + xPadding;
  439. if (this._width < width) {
  440. this._width = width;
  441. }
  442. textElement.setAttribute('x', x + xPadding);
  443. textElement.setAttribute('y', this._height + yPadding - size.y);
  444. this._height += yPadding + size.height + yPadding;
  445. }
  446. this._height += 3;
  447. if (this._width < 100) {
  448. this._width = 100;
  449. }
  450. }
  451. get width() {
  452. return this._width;
  453. }
  454. get height() {
  455. return this._height;
  456. }
  457. update(parentElement, top, width , first, last) {
  458. this._element.setAttribute('transform', 'translate(0,' + top + ')');
  459. var r1 = first;
  460. var r2 = first;
  461. var r3 = last;
  462. var r4 = last;
  463. this._backgroundElement.setAttribute('d', grapher.NodeElement.roundedRect(0, 0, width, this._height, r1, r2, r3, r4));
  464. if (!first) {
  465. var lineElement = this.createElement('line');
  466. lineElement.setAttribute('class', 'node');
  467. lineElement.setAttribute('x1', 0);
  468. lineElement.setAttribute('x2', width);
  469. lineElement.setAttribute('y1', 0);
  470. lineElement.setAttribute('y2', 0);
  471. this._element.appendChild(lineElement);
  472. }
  473. }
  474. createElement(name) {
  475. return this._document.createElementNS('http://www.w3.org/2000/svg', name);
  476. }
  477. };
  478. class Path {
  479. constructor() {
  480. this._x0 = null;
  481. this._y0 = null;
  482. this._x1 = null;
  483. this._y1 = null;
  484. this._data = '';
  485. }
  486. moveTo(x, y) {
  487. this._data += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
  488. }
  489. lineTo(x, y) {
  490. this._data += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
  491. }
  492. bezierCurveTo(x1, y1, x2, y2, x, y) {
  493. this._data += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
  494. }
  495. closePath() {
  496. if (this._x1 !== null) {
  497. this._x1 = this._x0;
  498. this._y1 = this._y0;
  499. this._data += "Z";
  500. }
  501. }
  502. get data() {
  503. return this._data;
  504. }
  505. }
  506. class Curve {
  507. constructor(context) {
  508. this._context = context;
  509. }
  510. lineStart() {
  511. this._x0 = NaN;
  512. this._x1 = NaN;
  513. this._y0 = NaN;
  514. this._y1 = NaN;
  515. this._point = 0;
  516. }
  517. lineEnd() {
  518. switch (this._point) {
  519. case 3:
  520. this.curve(this._x1, this._y1);
  521. this._context.lineTo(this._x1, this._y1);
  522. break;
  523. case 2:
  524. this._context.lineTo(this._x1, this._y1);
  525. break;
  526. }
  527. if (this._line || (this._line !== 0 && this._point === 1)) {
  528. this._context.closePath();
  529. }
  530. this._line = 1 - this._line;
  531. }
  532. point(x, y) {
  533. x = +x;
  534. y = +y;
  535. switch (this._point) {
  536. case 0:
  537. this._point = 1;
  538. if (this._line) {
  539. this._context.lineTo(x, y);
  540. }
  541. else {
  542. this._context.moveTo(x, y);
  543. }
  544. break;
  545. case 1:
  546. this._point = 2;
  547. break;
  548. case 2:
  549. this._point = 3;
  550. this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
  551. this.curve(x, y);
  552. break;
  553. default:
  554. this.curve(x, y);
  555. break;
  556. }
  557. this._x0 = this._x1;
  558. this._x1 = x;
  559. this._y0 = this._y1;
  560. this._y1 = y;
  561. }
  562. curve(x, y) {
  563. this._context.bezierCurveTo(
  564. (2 * this._x0 + this._x1) / 3,
  565. (2 * this._y0 + this._y1) / 3,
  566. (this._x0 + 2 * this._x1) / 3,
  567. (this._y0 + 2 * this._y1) / 3,
  568. (this._x0 + 4 * this._x1 + x) / 6,
  569. (this._y0 + 4 * this._y1 + y) / 6
  570. );
  571. }
  572. }
  573. if (typeof module !== 'undefined' && typeof module.exports === 'object') {
  574. module.exports.Renderer = grapher.Renderer;
  575. module.exports.NodeElement = grapher.NodeElement;
  576. }