grapher.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. import * as dagre from './dagre.js';
  2. const grapher = {};
  3. grapher.Graph = class {
  4. constructor(compound, layout) {
  5. this._layout = layout;
  6. this._isCompound = compound;
  7. this._nodes = new Map();
  8. this._edges = new Map();
  9. this._children = {};
  10. this._children['\x00'] = {};
  11. this._parent = {};
  12. }
  13. setNode(node) {
  14. const key = node.name;
  15. const value = this._nodes.get(key);
  16. if (value) {
  17. value.label = node;
  18. } else {
  19. this._nodes.set(key, { v: key, label: node });
  20. if (this._isCompound) {
  21. this._parent[key] = '\x00';
  22. this._children[key] = {};
  23. this._children['\x00'][key] = true;
  24. }
  25. }
  26. }
  27. setEdge(edge) {
  28. if (!this._nodes.has(edge.v)) {
  29. throw new grapher.Error(`Invalid edge '${JSON.stringify(edge.v)}'.`);
  30. }
  31. if (!this._nodes.has(edge.w)) {
  32. throw new grapher.Error(`Invalid edge '${JSON.stringify(edge.w)}'.`);
  33. }
  34. const key = `${edge.v}:${edge.w}`;
  35. if (!this._edges.has(key)) {
  36. this._edges.set(key, { v: edge.v, w: edge.w, label: edge });
  37. }
  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 (let 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. get nodes() {
  55. return this._nodes;
  56. }
  57. hasNode(key) {
  58. return this._nodes.has(key);
  59. }
  60. node(key) {
  61. return this._nodes.get(key);
  62. }
  63. get edges() {
  64. return this._edges;
  65. }
  66. parent(key) {
  67. if (this._isCompound) {
  68. const parent = this._parent[key];
  69. if (parent !== '\x00') {
  70. return parent;
  71. }
  72. }
  73. return null;
  74. }
  75. children(key) {
  76. key = key === undefined ? '\x00' : key;
  77. if (this._isCompound) {
  78. const children = this._children[key];
  79. if (children) {
  80. return Object.keys(children);
  81. }
  82. } else if (key === '\x00') {
  83. return this.nodes.keys();
  84. } else if (this.hasNode(key)) {
  85. return [];
  86. }
  87. return null;
  88. }
  89. build(document, origin) {
  90. const createGroup = (name) => {
  91. const element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  92. element.setAttribute('id', name);
  93. element.setAttribute('class', name);
  94. origin.appendChild(element);
  95. return element;
  96. };
  97. const clusterGroup = createGroup('clusters');
  98. const edgePathGroup = createGroup('edge-paths');
  99. const edgeLabelGroup = createGroup('edge-labels');
  100. const nodeGroup = createGroup('nodes');
  101. const edgePathGroupDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  102. edgePathGroup.appendChild(edgePathGroupDefs);
  103. const marker = (id) => {
  104. const element = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
  105. element.setAttribute('id', id);
  106. element.setAttribute('viewBox', '0 0 10 10');
  107. element.setAttribute('refX', 9);
  108. element.setAttribute('refY', 5);
  109. element.setAttribute('markerUnits', 'strokeWidth');
  110. element.setAttribute('markerWidth', 8);
  111. element.setAttribute('markerHeight', 6);
  112. element.setAttribute('orient', 'auto');
  113. const markerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  114. markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
  115. markerPath.style.setProperty('stroke-width', 1);
  116. element.appendChild(markerPath);
  117. return element;
  118. };
  119. edgePathGroupDefs.appendChild(marker("arrowhead"));
  120. edgePathGroupDefs.appendChild(marker("arrowhead-select"));
  121. edgePathGroupDefs.appendChild(marker("arrowhead-hover"));
  122. for (const nodeId of this.nodes.keys()) {
  123. const entry = this.node(nodeId);
  124. const node = entry.label;
  125. if (this.children(nodeId).length == 0) {
  126. node.build(document, nodeGroup);
  127. } else {
  128. // cluster
  129. node.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  130. if (node.rx) {
  131. node.rectangle.setAttribute('rx', entry.rx);
  132. }
  133. if (node.ry) {
  134. node.rectangle.setAttribute('ry', entry.ry);
  135. }
  136. node.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  137. node.element.setAttribute('class', 'cluster');
  138. node.element.appendChild(node.rectangle);
  139. clusterGroup.appendChild(node.element);
  140. }
  141. }
  142. for (const edge of this.edges.values()) {
  143. edge.label.build(document, edgePathGroup, edgeLabelGroup);
  144. }
  145. }
  146. measure() {
  147. for (const key of this.nodes.keys()) {
  148. const entry = this.node(key);
  149. if (this.children(key).length == 0) {
  150. const node = entry.label;
  151. node.measure();
  152. }
  153. }
  154. }
  155. layout() {
  156. dagre.layout(this, this._layout);
  157. for (const key of this.nodes.keys()) {
  158. const entry = this.node(key);
  159. if (this.children(key).length == 0) {
  160. const node = entry.label;
  161. node.layout();
  162. }
  163. }
  164. }
  165. update() {
  166. for (const nodeId of this.nodes.keys()) {
  167. if (this.children(nodeId).length == 0) {
  168. // node
  169. const entry = this.node(nodeId);
  170. const node = entry.label;
  171. node.update();
  172. } else {
  173. // cluster
  174. const entry = this.node(nodeId);
  175. const node = entry.label;
  176. node.element.setAttribute('transform', `translate(${node.x},${node.y})`);
  177. node.rectangle.setAttribute('x', - node.width / 2);
  178. node.rectangle.setAttribute('y', - node.height / 2);
  179. node.rectangle.setAttribute('width', node.width);
  180. node.rectangle.setAttribute('height', node.height);
  181. }
  182. }
  183. for (const edge of this.edges.values()) {
  184. edge.label.update();
  185. }
  186. }
  187. };
  188. grapher.Node = class {
  189. constructor() {
  190. this._blocks = [];
  191. }
  192. header() {
  193. const block = new grapher.Node.Header();
  194. this._blocks.push(block);
  195. return block;
  196. }
  197. list() {
  198. const block = new grapher.Node.List();
  199. this._blocks.push(block);
  200. return block;
  201. }
  202. canvas() {
  203. const block = new grapher.Node.Canvas();
  204. this._blocks.push(block);
  205. return block;
  206. }
  207. build(document, parent) {
  208. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  209. if (this.id) {
  210. this.element.setAttribute('id', this.id);
  211. }
  212. this.element.setAttribute('class', this.class ? `node ${this.class}` : 'node');
  213. this.element.style.opacity = 0;
  214. parent.appendChild(this.element);
  215. this.border = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  216. this.border.setAttribute('class', 'node node-border');
  217. for (let i = 0; i < this._blocks.length; i++) {
  218. const block = this._blocks[i];
  219. block.first = i === 0;
  220. block.last = i === this._blocks.length - 1;
  221. block.build(document, this.element);
  222. }
  223. this.element.appendChild(this.border);
  224. }
  225. measure() {
  226. this.height = 0;
  227. for (const block of this._blocks) {
  228. block.measure();
  229. this.height = this.height + block.height;
  230. }
  231. this.width = Math.max(...this._blocks.map((block) => block.width));
  232. for (const block of this._blocks) {
  233. block.width = this.width;
  234. }
  235. }
  236. layout() {
  237. let y = 0;
  238. for (const block of this._blocks) {
  239. block.x = 0;
  240. block.y = y;
  241. block.width = this.width;
  242. block.layout();
  243. y += block.height;
  244. }
  245. }
  246. update() {
  247. for (const block of this._blocks) {
  248. block.update();
  249. }
  250. this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, true, true, true, true));
  251. this.element.setAttribute('transform', `translate(${this.x - (this.width / 2)},${this.y - (this.height / 2)})`);
  252. this.element.style.removeProperty('opacity');
  253. }
  254. select() {
  255. if (this.element) {
  256. this.element.classList.add('select');
  257. return [ this.element ];
  258. }
  259. return [];
  260. }
  261. deselect() {
  262. if (this.element) {
  263. this.element.classList.remove('select');
  264. }
  265. }
  266. static roundedRect(x, y, width, height, r1, r2, r3, r4) {
  267. const radius = 5;
  268. r1 = r1 ? radius : 0;
  269. r2 = r2 ? radius : 0;
  270. r3 = r3 ? radius : 0;
  271. r4 = r4 ? radius : 0;
  272. return `M${x + r1},${y
  273. }h${width - r1 - r2
  274. }a${r2},${r2} 0 0 1 ${r2},${r2
  275. }v${height - r2 - r3
  276. }a${r3},${r3} 0 0 1 ${-r3},${r3
  277. }h${r3 + r4 - width
  278. }a${r4},${r4} 0 0 1 ${-r4},${-r4
  279. }v${-height + r4 + r1
  280. }a${r1},${r1} 0 0 1 ${r1},${-r1
  281. }z`;
  282. }
  283. };
  284. grapher.Node.Header = class {
  285. constructor() {
  286. this._entries = [];
  287. }
  288. add(id, classList, content, tooltip, handler) {
  289. const entry = new grapher.Node.Header.Entry(id, classList, content, tooltip, handler);
  290. this._entries.push(entry);
  291. return entry;
  292. }
  293. build(document, parent) {
  294. this._document = document;
  295. for (const entry of this._entries) {
  296. entry.build(document, parent);
  297. }
  298. if (!this.first) {
  299. this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  300. parent.appendChild(this.line);
  301. }
  302. for (let i = 0; i < this._entries.length; i++) {
  303. const entry = this._entries[i];
  304. if (i != 0) {
  305. entry.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  306. parent.appendChild(entry.line);
  307. }
  308. }
  309. }
  310. measure() {
  311. this.width = 0;
  312. this.height = 0;
  313. for (const entry of this._entries) {
  314. entry.measure();
  315. this.height = Math.max(this.height, entry.height);
  316. this.width += entry.width;
  317. }
  318. }
  319. layout() {
  320. let x = this.width;
  321. for (let i = this._entries.length - 1; i >= 0; i--) {
  322. const entry = this._entries[i];
  323. if (i > 0) {
  324. x -= entry.width;
  325. entry.x = x;
  326. } else {
  327. entry.x = 0;
  328. entry.width = x;
  329. }
  330. }
  331. }
  332. update() {
  333. for (let i = 0; i < this._entries.length; i++) {
  334. const entry = this._entries[i];
  335. entry.element.setAttribute('transform', `translate(${entry.x},${this.y})`);
  336. const r1 = i == 0 && this.first;
  337. const r2 = i == this._entries.length - 1 && this.first;
  338. const r3 = i == this._entries.length - 1 && this.last;
  339. const r4 = i == 0 && this.last;
  340. entry.path.setAttribute('d', grapher.Node.roundedRect(0, 0, entry.width, entry.height, r1, r2, r3, r4));
  341. entry.text.setAttribute('x', 6);
  342. entry.text.setAttribute('y', entry.ty);
  343. }
  344. for (let i = 1; i < this._entries.length; i++) {
  345. const entry = this._entries[i];
  346. const line = entry.line;
  347. line.setAttribute('class', 'node');
  348. line.setAttribute('x1', entry.x);
  349. line.setAttribute('x2', entry.x);
  350. line.setAttribute('y1', this.y);
  351. line.setAttribute('y2', this.y + this.height);
  352. }
  353. if (this.line) {
  354. this.line.setAttribute('class', 'node');
  355. this.line.setAttribute('x1', 0);
  356. this.line.setAttribute('x2', this.width);
  357. this.line.setAttribute('y1', this.y);
  358. this.line.setAttribute('y2', this.y);
  359. }
  360. }
  361. };
  362. grapher.Node.Header.Entry = class {
  363. constructor(id, classList, content, tooltip, handler) {
  364. this.id = id;
  365. this.classList = classList;
  366. this.content = content;
  367. this.tooltip = tooltip;
  368. this.handler = handler;
  369. this._events = {};
  370. }
  371. on(event, callback) {
  372. this._events[event] = this._events[event] || [];
  373. this._events[event].push(callback);
  374. }
  375. emit(event, data) {
  376. if (this._events && this._events[event]) {
  377. for (const callback of this._events[event]) {
  378. callback(this, data);
  379. }
  380. }
  381. }
  382. build(document, parent) {
  383. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  384. parent.appendChild(this.element);
  385. this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  386. this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  387. this.element.appendChild(this.path);
  388. this.element.appendChild(this.text);
  389. const classList = [ 'node-item' ];
  390. if (this.classList) {
  391. classList.push(...this.classList);
  392. }
  393. this.element.setAttribute('class', classList.join(' '));
  394. if (this.id) {
  395. this.element.setAttribute('id', this.id);
  396. }
  397. if (this._events.click) {
  398. this.element.addEventListener('click', (e) => {
  399. e.stopPropagation();
  400. this.emit('click');
  401. });
  402. }
  403. if (this.tooltip) {
  404. const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
  405. title.textContent = this.tooltip;
  406. this.element.appendChild(title);
  407. }
  408. this.text.textContent = this.content || '\u00A0';
  409. }
  410. measure() {
  411. const yPadding = 4;
  412. const xPadding = 7;
  413. const boundingBox = this.text.getBBox();
  414. this.width = boundingBox.width + xPadding + xPadding;
  415. this.height = boundingBox.height + yPadding + yPadding;
  416. this.tx = xPadding;
  417. this.ty = yPadding - boundingBox.y;
  418. }
  419. layout() {
  420. }
  421. };
  422. grapher.Node.List = class {
  423. constructor() {
  424. this._items = [];
  425. this._events = {};
  426. }
  427. add(name, value, tooltip, separator) {
  428. const item = new grapher.Node.List.Item(name, value, tooltip, separator);
  429. this._items.push(item);
  430. return item;
  431. }
  432. on(event, callback) {
  433. this._events[event] = this._events[event] || [];
  434. this._events[event].push(callback);
  435. }
  436. emit(event, data) {
  437. if (this._events && this._events[event]) {
  438. for (const callback of this._events[event]) {
  439. callback(this, data);
  440. }
  441. }
  442. }
  443. build(document, parent) {
  444. this._document = document;
  445. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  446. this.element.setAttribute('class', 'node-attribute-list');
  447. if (this._events.click) {
  448. this.element.addEventListener('click', (e) => {
  449. e.stopPropagation();
  450. this.emit('click');
  451. });
  452. }
  453. this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  454. this.element.appendChild(this.background);
  455. parent.appendChild(this.element);
  456. for (const item of this._items) {
  457. const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  458. group.setAttribute('class', 'node-attribute');
  459. const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  460. text.setAttribute('xml:space', 'preserve');
  461. if (item.tooltip) {
  462. const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
  463. title.textContent = item.tooltip;
  464. text.appendChild(title);
  465. }
  466. const colon = item.type === 'node' || item.type === 'node[]';
  467. const name = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
  468. name.textContent = colon ? `${item.name}:` : item.name;
  469. if (item.separator.trim() !== '=' && !colon) {
  470. name.style.fontWeight = 'bold';
  471. }
  472. text.appendChild(name);
  473. group.appendChild(text);
  474. this.element.appendChild(group);
  475. item.group = group;
  476. item.text = text;
  477. if (item.type === 'node') {
  478. const node = item.value;
  479. node.build(document, item.group);
  480. } else if (item.type === 'node[]') {
  481. for (const node of item.value) {
  482. node.build(document, item.group);
  483. }
  484. } else {
  485. const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
  486. tspan.textContent = item.separator + item.value;
  487. item.text.appendChild(tspan);
  488. }
  489. }
  490. if (!this.first) {
  491. this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  492. this.line.setAttribute('class', 'node');
  493. this.element.appendChild(this.line);
  494. }
  495. }
  496. measure() {
  497. this.width = 75;
  498. this.height = 3;
  499. const yPadding = 1;
  500. const xPadding = 6;
  501. for (let i = 0; i < this._items.length; i++) {
  502. const item = this._items[i];
  503. const size = item.text.getBBox();
  504. item.width = xPadding + size.width + xPadding;
  505. item.height = yPadding + size.height + yPadding;
  506. item.offset = size.y;
  507. this.height += item.height;
  508. if (item.type === 'node') {
  509. const node = item.value;
  510. node.measure();
  511. this.width = Math.max(150, this.width, node.width + (2 * xPadding));
  512. this.height += node.height + yPadding + yPadding + yPadding + yPadding;
  513. if (i === this._items.length - 1) {
  514. this.height += 3;
  515. }
  516. } else if (item.type === 'node[]') {
  517. for (const node of item.value) {
  518. node.measure();
  519. this.width = Math.max(150, this.width, node.width + (2 * xPadding));
  520. this.height += node.height + yPadding + yPadding + yPadding + yPadding;
  521. }
  522. if (i === this._items.length - 1) {
  523. this.height += 3;
  524. }
  525. }
  526. this.width = Math.max(this.width, item.width);
  527. }
  528. this.height += 3;
  529. }
  530. layout() {
  531. const yPadding = 1;
  532. const xPadding = 6;
  533. let y = 3;
  534. for (const item of this._items) {
  535. item.x = this.x + xPadding;
  536. item.y = y + yPadding - item.offset;
  537. y += item.height;
  538. if (item.type === 'node') {
  539. const node = item.value;
  540. node.width = this.width - xPadding - xPadding;
  541. node.layout();
  542. node.x = this.x + xPadding + (node.width / 2);
  543. node.y = y + (node.height / 2) + yPadding + yPadding;
  544. y += node.height + yPadding + yPadding + yPadding + yPadding;
  545. } else if (item.type === 'node[]') {
  546. for (const node of item.value) {
  547. node.width = this.width - xPadding - xPadding;
  548. node.layout();
  549. node.x = this.x + xPadding + (node.width / 2);
  550. node.y = y + (node.height / 2) + yPadding + yPadding;
  551. y += node.height + yPadding + yPadding + yPadding + yPadding;
  552. }
  553. }
  554. }
  555. }
  556. update() {
  557. this.element.setAttribute('transform', `translate(${this.x},${this.y})`);
  558. this.background.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, this.first, this.first, this.last, this.last));
  559. for (const item of this._items) {
  560. const text = item.text;
  561. text.setAttribute('x', item.x);
  562. text.setAttribute('y', item.y);
  563. if (item.type === 'node') {
  564. const node = item.value;
  565. node.update();
  566. } else if (item.type === 'node[]') {
  567. for (const node of item.value) {
  568. node.update();
  569. }
  570. }
  571. }
  572. if (this.line) {
  573. this.line.setAttribute('x1', 0);
  574. this.line.setAttribute('x2', this.width);
  575. this.line.setAttribute('y1', 0);
  576. this.line.setAttribute('y2', 0);
  577. }
  578. for (const item of this._items) {
  579. if (item.value instanceof grapher.Node) {
  580. const node = item.value;
  581. node.update();
  582. }
  583. }
  584. }
  585. };
  586. grapher.Node.List.Item = class {
  587. constructor(name, value, tooltip, separator) {
  588. this.name = name;
  589. this.value = value;
  590. this.tooltip = tooltip;
  591. this.separator = separator;
  592. if (value instanceof grapher.Node) {
  593. this.type = 'node';
  594. } else if (Array.isArray(value) && value.every((value) => value instanceof grapher.Node)) {
  595. this.type = 'node[]';
  596. }
  597. }
  598. };
  599. grapher.Node.Canvas = class {
  600. constructor() {
  601. this.width = 0;
  602. this.height = 80;
  603. }
  604. build(/* document, parent */) {
  605. }
  606. update(/* parent, top, width , first, last */) {
  607. }
  608. };
  609. grapher.Edge = class {
  610. constructor(from, to) {
  611. this.from = from;
  612. this.to = to;
  613. }
  614. build(document, edgePathGroupElement, edgeLabelGroupElement) {
  615. const createElement = (name) => {
  616. return document.createElementNS('http://www.w3.org/2000/svg', name);
  617. };
  618. this.element = createElement('path');
  619. if (this.id) {
  620. this.element.setAttribute('id', this.id);
  621. }
  622. this.element.setAttribute('class', this.class ? `edge-path ${this.class}` : 'edge-path');
  623. edgePathGroupElement.appendChild(this.element);
  624. this.hitTest = createElement('path');
  625. this.hitTest.setAttribute('class', 'edge-path-hit-test');
  626. this.hitTest.addEventListener('pointerover', () => this.emit('pointerover'));
  627. this.hitTest.addEventListener('pointerleave', () => this.emit('pointerleave'));
  628. this.hitTest.addEventListener('click', () => this.emit('click'));
  629. edgePathGroupElement.appendChild(this.hitTest);
  630. if (this.label) {
  631. const tspan = createElement('tspan');
  632. tspan.setAttribute('xml:space', 'preserve');
  633. tspan.setAttribute('dy', '1em');
  634. tspan.setAttribute('x', '1');
  635. tspan.appendChild(document.createTextNode(this.label));
  636. this.labelElement = createElement('text');
  637. this.labelElement.appendChild(tspan);
  638. this.labelElement.style.opacity = 0;
  639. this.labelElement.setAttribute('class', 'edge-label');
  640. if (this.id) {
  641. this.labelElement.setAttribute('id', `edge-label-${this.id}`);
  642. }
  643. edgeLabelGroupElement.appendChild(this.labelElement);
  644. const edgeBox = this.labelElement.getBBox();
  645. this.width = edgeBox.width;
  646. this.height = edgeBox.height;
  647. }
  648. }
  649. update() {
  650. const intersectRect = (node, point) => {
  651. const x = node.x;
  652. const y = node.y;
  653. const dx = point.x - x;
  654. const dy = point.y - y;
  655. let h = node.height / 2;
  656. let w = node.width / 2;
  657. if (Math.abs(dy) * w > Math.abs(dx) * h) {
  658. if (dy < 0) {
  659. h = -h;
  660. }
  661. return { x: x + (dy === 0 ? 0 : h * dx / dy), y: y + h };
  662. }
  663. if (dx < 0) {
  664. w = -w;
  665. }
  666. return { x: x + w, y: y + (dx === 0 ? 0 : w * dy / dx) };
  667. };
  668. const curvePath = (edge, tail, head) => {
  669. const points = edge.points.slice(1, edge.points.length - 1);
  670. points.unshift(intersectRect(tail, points[0]));
  671. points.push(intersectRect(head, points[points.length - 1]));
  672. return new grapher.Edge.Curve(points).path.data;
  673. };
  674. const edgePath = curvePath(this, this.from, this.to);
  675. this.element.setAttribute('d', edgePath);
  676. this.hitTest.setAttribute('d', edgePath);
  677. if (this.labelElement) {
  678. this.labelElement.setAttribute('transform', `translate(${this.x - (this.width / 2)},${this.y - (this.height / 2)})`);
  679. this.labelElement.style.opacity = 1;
  680. }
  681. }
  682. select() {
  683. if (this.element) {
  684. if (!this.element.classList.contains('select')) {
  685. const path = this.element;
  686. path.classList.add('select');
  687. this.element = path.cloneNode(true);
  688. path.parentNode.replaceChild(this.element, path);
  689. }
  690. return [ this.element ];
  691. }
  692. return [];
  693. }
  694. deselect() {
  695. if (this.element && this.element.classList.contains('select')) {
  696. const path = this.element;
  697. path.classList.remove('select');
  698. this.element = path.cloneNode(true);
  699. path.parentNode.replaceChild(this.element, path);
  700. }
  701. }
  702. };
  703. grapher.Edge.Curve = class {
  704. constructor(points) {
  705. this._path = new grapher.Edge.Path();
  706. this._x0 = NaN;
  707. this._x1 = NaN;
  708. this._y0 = NaN;
  709. this._y1 = NaN;
  710. this._state = 0;
  711. for (let i = 0; i < points.length; i++) {
  712. const point = points[i];
  713. this.point(point.x, point.y);
  714. if (i === points.length - 1) {
  715. switch (this._state) {
  716. case 3:
  717. this.curve(this._x1, this._y1);
  718. this._path.lineTo(this._x1, this._y1);
  719. break;
  720. case 2:
  721. this._path.lineTo(this._x1, this._y1);
  722. break;
  723. default:
  724. break;
  725. }
  726. if (this._line || (this._line !== 0 && this._point === 1)) {
  727. this._path.closePath();
  728. }
  729. this._line = 1 - this._line;
  730. }
  731. }
  732. }
  733. get path() {
  734. return this._path;
  735. }
  736. point(x, y) {
  737. x = +x;
  738. y = +y;
  739. switch (this._state) {
  740. case 0:
  741. this._state = 1;
  742. if (this._line) {
  743. this._path.lineTo(x, y);
  744. } else {
  745. this._path.moveTo(x, y);
  746. }
  747. break;
  748. case 1:
  749. this._state = 2;
  750. break;
  751. case 2:
  752. this._state = 3;
  753. this._path.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
  754. this.curve(x, y);
  755. break;
  756. default:
  757. this.curve(x, y);
  758. break;
  759. }
  760. this._x0 = this._x1;
  761. this._x1 = x;
  762. this._y0 = this._y1;
  763. this._y1 = y;
  764. }
  765. curve(x, y) {
  766. this._path.bezierCurveTo(
  767. (2 * this._x0 + this._x1) / 3,
  768. (2 * this._y0 + this._y1) / 3,
  769. (this._x0 + 2 * this._x1) / 3,
  770. (this._y0 + 2 * this._y1) / 3,
  771. (this._x0 + 4 * this._x1 + x) / 6,
  772. (this._y0 + 4 * this._y1 + y) / 6
  773. );
  774. }
  775. };
  776. grapher.Edge.Path = class {
  777. constructor() {
  778. this._x0 = null;
  779. this._y0 = null;
  780. this._x1 = null;
  781. this._y1 = null;
  782. this._data = '';
  783. }
  784. moveTo(x, y) {
  785. this._data += `M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`;
  786. }
  787. lineTo(x, y) {
  788. this._data += `L${this._x1 = +x},${this._y1 = +y}`;
  789. }
  790. bezierCurveTo(x1, y1, x2, y2, x, y) {
  791. this._data += `C${+x1},${+y1},${+x2},${+y2},${this._x1 = +x},${this._y1 = +y}`;
  792. }
  793. closePath() {
  794. if (this._x1 !== null) {
  795. this._x1 = this._x0;
  796. this._y1 = this._y0;
  797. this._data += "Z";
  798. }
  799. }
  800. get data() {
  801. return this._data;
  802. }
  803. };
  804. export const { Graph, Node, Edge } = grapher;