view-grapher.js 23 KB

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