view-grapher.js 22 KB

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