grapher.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056
  1. const grapher = {};
  2. grapher.Graph = class {
  3. constructor(compound) {
  4. this._compound = compound;
  5. this._nodes = new Map();
  6. this._edges = new Map();
  7. this._focusable = new Map();
  8. this._focused = null;
  9. this._children = new Map();
  10. this._children.set('\x00', new Map());
  11. this._parent = new Map();
  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._compound) {
  21. this._parent.set(key, '\x00');
  22. this._children.set(key, new Map());
  23. this._children.get('\x00').set(key, true);
  24. }
  25. }
  26. }
  27. setEdge(edge) {
  28. if (!this._nodes.has(edge.v)) {
  29. throw new Error(`Invalid edge '${JSON.stringify(edge.v)}'.`);
  30. }
  31. if (!this._nodes.has(edge.w)) {
  32. throw new 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._compound) {
  41. throw new Error("Cannot set parent in a non-compound graph");
  42. }
  43. parent = String(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. this._children.get(this._parent.get(node)).delete(node);
  50. this._parent.set(node, parent);
  51. this._children.get(parent).set(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. edge(v, w) {
  64. return this._edges.get(`${v}:${w}`);
  65. }
  66. get edges() {
  67. return this._edges;
  68. }
  69. parent(key) {
  70. if (this._compound) {
  71. const parent = this._parent.get(key);
  72. if (parent !== '\x00') {
  73. return parent;
  74. }
  75. }
  76. return null;
  77. }
  78. children(key) {
  79. key = key === undefined ? '\x00' : key;
  80. if (this._compound) {
  81. const children = this._children.get(key);
  82. if (children) {
  83. return Array.from(children.keys());
  84. }
  85. } else if (key === '\x00') {
  86. return this.nodes.keys();
  87. } else if (this.hasNode(key)) {
  88. return [];
  89. }
  90. return null;
  91. }
  92. build(document) {
  93. const origin = document.getElementById('origin');
  94. const createGroup = (name) => {
  95. const element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  96. element.setAttribute('id', name);
  97. element.setAttribute('class', name);
  98. return element;
  99. };
  100. const clusterGroup = createGroup('clusters');
  101. const edgePathGroup = createGroup('edge-paths');
  102. const edgePathHitTestGroup = createGroup('edge-paths-hit-test');
  103. const edgeLabelGroup = createGroup('edge-labels');
  104. const nodeGroup = createGroup('nodes');
  105. const edgePathGroupDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  106. edgePathGroup.appendChild(edgePathGroupDefs);
  107. const marker = (id) => {
  108. const element = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
  109. element.setAttribute('id', id);
  110. element.setAttribute('viewBox', '0 0 10 10');
  111. element.setAttribute('refX', 9);
  112. element.setAttribute('refY', 5);
  113. element.setAttribute('markerUnits', 'strokeWidth');
  114. element.setAttribute('markerWidth', 8);
  115. element.setAttribute('markerHeight', 6);
  116. element.setAttribute('orient', 'auto');
  117. const markerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  118. markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
  119. markerPath.style.setProperty('stroke-width', 1);
  120. element.appendChild(markerPath);
  121. return element;
  122. };
  123. edgePathHitTestGroup.addEventListener('pointerover', (e) => {
  124. if (this._focused) {
  125. this._focused.blur();
  126. this._focused = null;
  127. }
  128. const edge = this._focusable.get(e.target);
  129. if (edge && edge.focus) {
  130. edge.focus();
  131. this._focused = edge;
  132. e.stopPropagation();
  133. }
  134. });
  135. edgePathHitTestGroup.addEventListener('pointerleave', (e) => {
  136. if (this._focused) {
  137. this._focused.blur();
  138. this._focused = null;
  139. e.stopPropagation();
  140. }
  141. });
  142. edgePathHitTestGroup.addEventListener('click', (e) => {
  143. const edge = this._focusable.get(e.target);
  144. if (edge && edge.activate) {
  145. edge.activate();
  146. e.stopPropagation();
  147. }
  148. });
  149. edgePathGroupDefs.appendChild(marker("arrowhead"));
  150. edgePathGroupDefs.appendChild(marker("arrowhead-select"));
  151. edgePathGroupDefs.appendChild(marker("arrowhead-hover"));
  152. for (const nodeId of this.nodes.keys()) {
  153. const entry = this.node(nodeId);
  154. const node = entry.label;
  155. if (this.children(nodeId).length === 0) {
  156. node.build(document, nodeGroup);
  157. } else {
  158. // cluster
  159. node.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  160. if (node.rx) {
  161. node.rectangle.setAttribute('rx', entry.rx);
  162. }
  163. if (node.ry) {
  164. node.rectangle.setAttribute('ry', entry.ry);
  165. }
  166. node.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  167. node.element.setAttribute('class', 'cluster');
  168. node.element.appendChild(node.rectangle);
  169. clusterGroup.appendChild(node.element);
  170. }
  171. }
  172. this._focusable.clear();
  173. this._focused = null;
  174. for (const edge of this.edges.values()) {
  175. edge.label.build(document, edgePathGroup, edgePathHitTestGroup, edgeLabelGroup);
  176. this._focusable.set(edge.label.hitTest, edge.label);
  177. }
  178. origin.appendChild(clusterGroup);
  179. origin.appendChild(edgePathGroup);
  180. origin.appendChild(edgePathHitTestGroup);
  181. origin.appendChild(edgeLabelGroup);
  182. origin.appendChild(nodeGroup);
  183. for (const edge of this.edges.values()) {
  184. if (edge.label.labelElement) {
  185. const label = edge.label;
  186. const box = label.labelElement.getBBox();
  187. label.width = box.width;
  188. label.height = box.height;
  189. }
  190. }
  191. }
  192. async measure() {
  193. for (const key of this.nodes.keys()) {
  194. const entry = this.node(key);
  195. if (this.children(key).length === 0) {
  196. const node = entry.label;
  197. node.measure();
  198. }
  199. }
  200. }
  201. async layout(worker) {
  202. let nodes = [];
  203. for (const node of this.nodes.values()) {
  204. nodes.push({
  205. v: node.v,
  206. width: node.label.width || 0,
  207. height: node.label.height || 0,
  208. parent: this.parent(node.v) });
  209. }
  210. let edges = [];
  211. for (const edge of this.edges.values()) {
  212. edges.push({
  213. v: edge.v,
  214. w: edge.w,
  215. minlen: edge.label.minlen || 1,
  216. weight: edge.label.weight || 1,
  217. width: edge.label.width || 0,
  218. height: edge.label.height || 0,
  219. labeloffset: edge.label.labeloffset || 10,
  220. labelpos: edge.label.labelpos || 'r'
  221. });
  222. }
  223. const layout = {};
  224. layout.nodesep = 20;
  225. layout.ranksep = 20;
  226. const direction = this.options.direction;
  227. const rotate = edges.length === 0 ? direction === 'vertical' : direction !== 'vertical';
  228. if (rotate) {
  229. layout.rankdir = 'LR';
  230. }
  231. if (edges.length === 0) {
  232. nodes = nodes.reverse(); // rankdir workaround
  233. }
  234. if (nodes.length > 3000) {
  235. layout.ranker = 'longest-path';
  236. }
  237. const state = { /* log: true */ };
  238. if (worker) {
  239. const message = await worker.request({ type: 'dagre.layout', nodes, edges, layout, state }, 2500, 'This large graph layout might take a very long time to complete.');
  240. if (message.type === 'cancel' || message.type === 'terminate') {
  241. return message.type;
  242. }
  243. nodes = message.nodes;
  244. edges = message.edges;
  245. state.log = message.state.log;
  246. } else {
  247. const dagre = await import('./dagre.js');
  248. dagre.layout(nodes, edges, layout, state);
  249. }
  250. if (state.log) {
  251. const fs = await import('fs');
  252. fs.writeFileSync(`dist/test/${this.identifier}.log`, state.log);
  253. }
  254. for (const node of nodes) {
  255. const label = this.node(node.v).label;
  256. label.x = node.x;
  257. label.y = node.y;
  258. if (this.children(node.v).length) {
  259. label.width = node.width;
  260. label.height = node.height;
  261. }
  262. }
  263. for (const edge of edges) {
  264. const label = this.edge(edge.v, edge.w).label;
  265. label.points = edge.points;
  266. if ('x' in edge) {
  267. label.x = edge.x;
  268. label.y = edge.y;
  269. }
  270. }
  271. for (const key of this.nodes.keys()) {
  272. const entry = this.node(key);
  273. if (this.children(key).length === 0) {
  274. const node = entry.label;
  275. node.layout();
  276. }
  277. }
  278. return '';
  279. }
  280. update() {
  281. for (const nodeId of this.nodes.keys()) {
  282. if (this.children(nodeId).length === 0) {
  283. // node
  284. const entry = this.node(nodeId);
  285. const node = entry.label;
  286. node.update();
  287. } else {
  288. // cluster
  289. const entry = this.node(nodeId);
  290. const node = entry.label;
  291. node.element.setAttribute('transform', `translate(${node.x},${node.y})`);
  292. node.rectangle.setAttribute('x', - node.width / 2);
  293. node.rectangle.setAttribute('y', - node.height / 2);
  294. node.rectangle.setAttribute('width', node.width);
  295. node.rectangle.setAttribute('height', node.height);
  296. }
  297. }
  298. for (const edge of this.edges.values()) {
  299. edge.label.update();
  300. }
  301. }
  302. };
  303. grapher.Node = class {
  304. constructor() {
  305. this._blocks = [];
  306. }
  307. header() {
  308. const block = new grapher.Node.Header();
  309. this._blocks.push(block);
  310. return block;
  311. }
  312. list() {
  313. const block = new grapher.ArgumentList();
  314. this._blocks.push(block);
  315. return block;
  316. }
  317. canvas() {
  318. const block = new grapher.Node.Canvas();
  319. this._blocks.push(block);
  320. return block;
  321. }
  322. build(document, parent) {
  323. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  324. if (this.id) {
  325. this.element.setAttribute('id', this.id);
  326. }
  327. this.element.setAttribute('class', this.class ? `node ${this.class}` : 'node');
  328. this.element.style.opacity = 0;
  329. parent.appendChild(this.element);
  330. this.border = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  331. this.border.setAttribute('class', 'node node-border');
  332. for (let i = 0; i < this._blocks.length; i++) {
  333. const block = this._blocks[i];
  334. block.first = i === 0;
  335. block.last = i === this._blocks.length - 1;
  336. block.build(document, this.element);
  337. }
  338. this.element.appendChild(this.border);
  339. }
  340. measure() {
  341. this.height = 0;
  342. for (const block of this._blocks) {
  343. block.measure();
  344. this.height += block.height;
  345. }
  346. this.width = Math.max(...this._blocks.map((block) => block.width));
  347. for (const block of this._blocks) {
  348. block.width = this.width;
  349. }
  350. }
  351. layout() {
  352. let y = 0;
  353. for (const block of this._blocks) {
  354. block.x = 0;
  355. block.y = y;
  356. block.width = this.width;
  357. block.layout();
  358. y += block.height;
  359. }
  360. }
  361. update() {
  362. for (const block of this._blocks) {
  363. block.update();
  364. }
  365. this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, true, true, true, true));
  366. this.element.setAttribute('transform', `translate(${this.x - (this.width / 2)},${this.y - (this.height / 2)})`);
  367. this.element.style.removeProperty('opacity');
  368. }
  369. select() {
  370. if (this.element) {
  371. this.element.classList.add('select');
  372. return [this.element];
  373. }
  374. return [];
  375. }
  376. deselect() {
  377. if (this.element) {
  378. this.element.classList.remove('select');
  379. }
  380. }
  381. static roundedRect(x, y, width, height, r1, r2, r3, r4) {
  382. const radius = 5;
  383. r1 = r1 ? radius : 0;
  384. r2 = r2 ? radius : 0;
  385. r3 = r3 ? radius : 0;
  386. r4 = r4 ? radius : 0;
  387. return `M${x + r1},${y}h${width - r1 - r2}a${r2},${r2} 0 0 1 ${r2},${r2}v${height - r2 - r3}a${r3},${r3} 0 0 1 ${-r3},${r3}h${r3 + r4 - width}a${r4},${r4} 0 0 1 ${-r4},${-r4}v${-height + r4 + r1}a${r1},${r1} 0 0 1 ${r1},${-r1}z`;
  388. }
  389. };
  390. grapher.Node.Header = class {
  391. constructor() {
  392. this._entries = [];
  393. }
  394. add(id, classList, content, tooltip, handler) {
  395. const entry = new grapher.Node.Header.Entry(id, classList, content, tooltip, handler);
  396. this._entries.push(entry);
  397. return entry;
  398. }
  399. build(document, parent) {
  400. this._document = document;
  401. for (const entry of this._entries) {
  402. entry.build(document, parent);
  403. }
  404. if (!this.first) {
  405. this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  406. parent.appendChild(this.line);
  407. }
  408. for (let i = 0; i < this._entries.length; i++) {
  409. const entry = this._entries[i];
  410. if (i !== 0) {
  411. entry.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  412. parent.appendChild(entry.line);
  413. }
  414. }
  415. }
  416. measure() {
  417. this.width = 0;
  418. this.height = 0;
  419. for (const entry of this._entries) {
  420. entry.measure();
  421. this.height = Math.max(this.height, entry.height);
  422. this.width += entry.width;
  423. }
  424. }
  425. layout() {
  426. let x = this.width;
  427. for (let i = this._entries.length - 1; i >= 0; i--) {
  428. const entry = this._entries[i];
  429. if (i > 0) {
  430. x -= entry.width;
  431. entry.x = x;
  432. } else {
  433. entry.x = 0;
  434. entry.width = x;
  435. }
  436. }
  437. }
  438. update() {
  439. for (let i = 0; i < this._entries.length; i++) {
  440. const entry = this._entries[i];
  441. entry.element.setAttribute('transform', `translate(${entry.x},${this.y})`);
  442. const r1 = i === 0 && this.first;
  443. const r2 = i === this._entries.length - 1 && this.first;
  444. const r3 = i === this._entries.length - 1 && this.last;
  445. const r4 = i === 0 && this.last;
  446. entry.path.setAttribute('d', grapher.Node.roundedRect(0, 0, entry.width, entry.height, r1, r2, r3, r4));
  447. entry.text.setAttribute('x', 6);
  448. entry.text.setAttribute('y', entry.ty);
  449. }
  450. for (let i = 1; i < this._entries.length; i++) {
  451. const entry = this._entries[i];
  452. const line = entry.line;
  453. line.setAttribute('class', 'node');
  454. line.setAttribute('x1', entry.x);
  455. line.setAttribute('x2', entry.x);
  456. line.setAttribute('y1', this.y);
  457. line.setAttribute('y2', this.y + this.height);
  458. }
  459. if (this.line) {
  460. this.line.setAttribute('class', 'node');
  461. this.line.setAttribute('x1', 0);
  462. this.line.setAttribute('x2', this.width);
  463. this.line.setAttribute('y1', this.y);
  464. this.line.setAttribute('y2', this.y);
  465. }
  466. }
  467. };
  468. grapher.Node.Header.Entry = class {
  469. constructor(id, classList, content, tooltip, handler) {
  470. this.id = id;
  471. this.classList = classList;
  472. this.content = content;
  473. this.tooltip = tooltip;
  474. this.handler = handler;
  475. this._events = {};
  476. }
  477. on(event, callback) {
  478. this._events[event] = this._events[event] || [];
  479. this._events[event].push(callback);
  480. }
  481. emit(event, data) {
  482. if (this._events && this._events[event]) {
  483. for (const callback of this._events[event]) {
  484. callback(this, data);
  485. }
  486. }
  487. }
  488. build(document, parent) {
  489. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  490. parent.appendChild(this.element);
  491. this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  492. this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  493. this.element.appendChild(this.path);
  494. this.element.appendChild(this.text);
  495. const classList = ['node-item'];
  496. if (this.classList) {
  497. classList.push(...this.classList);
  498. }
  499. this.element.setAttribute('class', classList.join(' '));
  500. if (this.id) {
  501. this.element.setAttribute('id', this.id);
  502. }
  503. if (this._events.click) {
  504. this.element.addEventListener('click', (e) => {
  505. e.stopPropagation();
  506. this.emit('click');
  507. });
  508. }
  509. if (this.tooltip) {
  510. const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
  511. title.textContent = this.tooltip;
  512. this.element.appendChild(title);
  513. }
  514. this.text.textContent = this.content || '\u00A0';
  515. }
  516. measure() {
  517. const yPadding = 4;
  518. const xPadding = 7;
  519. const boundingBox = this.text.getBBox();
  520. this.width = boundingBox.width + xPadding + xPadding;
  521. this.height = boundingBox.height + yPadding + yPadding;
  522. this.tx = xPadding;
  523. this.ty = yPadding - boundingBox.y;
  524. }
  525. layout() {
  526. }
  527. };
  528. grapher.ArgumentList = class {
  529. constructor() {
  530. this._items = [];
  531. this._events = {};
  532. }
  533. argument(name, value) {
  534. return new grapher.Argument(name, value);
  535. }
  536. add(value) {
  537. this._items.push(value);
  538. }
  539. on(event, callback) {
  540. this._events[event] = this._events[event] || [];
  541. this._events[event].push(callback);
  542. }
  543. emit(event, data) {
  544. if (this._events && this._events[event]) {
  545. for (const callback of this._events[event]) {
  546. callback(this, data);
  547. }
  548. }
  549. }
  550. build(document, parent) {
  551. this._document = document;
  552. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  553. this.element.setAttribute('class', 'node-argument-list');
  554. if (this._events.click) {
  555. this.element.addEventListener('click', (e) => {
  556. e.stopPropagation();
  557. this.emit('click');
  558. });
  559. }
  560. this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  561. this.element.appendChild(this.background);
  562. parent.appendChild(this.element);
  563. for (const item of this._items) {
  564. item.build(document, this.element);
  565. }
  566. if (!this.first) {
  567. this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  568. this.line.setAttribute('class', 'node');
  569. this.element.appendChild(this.line);
  570. }
  571. }
  572. measure() {
  573. this.width = 75;
  574. this.height = 3;
  575. for (let i = 0; i < this._items.length; i++) {
  576. const item = this._items[i];
  577. item.measure();
  578. this.height += item.height;
  579. this.width = Math.max(this.width, item.width);
  580. if (item.type === 'node' || item.type === 'node[]') {
  581. if (i === this._items.length - 1) {
  582. this.height += 3;
  583. }
  584. }
  585. }
  586. for (const item of this._items) {
  587. item.width = this.width;
  588. }
  589. this.height += 3;
  590. }
  591. layout() {
  592. let y = 3;
  593. for (const item of this._items) {
  594. item.x = this.x;
  595. item.y = y;
  596. item.width = this.width;
  597. item.layout();
  598. y += item.height;
  599. }
  600. }
  601. update() {
  602. this.element.setAttribute('transform', `translate(${this.x},${this.y})`);
  603. this.background.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, this.first, this.first, this.last, this.last));
  604. for (const item of this._items) {
  605. item.update();
  606. }
  607. if (this.line) {
  608. this.line.setAttribute('x1', 0);
  609. this.line.setAttribute('x2', this.width);
  610. this.line.setAttribute('y1', 0);
  611. this.line.setAttribute('y2', 0);
  612. }
  613. }
  614. };
  615. grapher.Argument = class {
  616. constructor(name, content) {
  617. this.name = name;
  618. this.content = content;
  619. this.tooltip = '';
  620. this.separator = '';
  621. if (content instanceof grapher.Node) {
  622. this.type = 'node';
  623. } else if (Array.isArray(content) && content.every((value) => value instanceof grapher.Node)) {
  624. this.type = 'node[]';
  625. }
  626. }
  627. build(document, parent) {
  628. this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  629. this.element.setAttribute('class', 'node-argument');
  630. this.border = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  631. this.border.setAttribute('rx', 3);
  632. this.border.setAttribute('ry', 3);
  633. this.element.appendChild(this.border);
  634. const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  635. text.setAttribute('xml:space', 'preserve');
  636. if (this.tooltip) {
  637. const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
  638. title.textContent = this.tooltip;
  639. text.appendChild(title);
  640. }
  641. const colon = this.type === 'node' || this.type === 'node[]';
  642. const name = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
  643. name.textContent = colon ? `${this.name}:` : this.name;
  644. if (this.separator.trim() !== '=' && !colon) {
  645. name.style.fontWeight = 'bold';
  646. }
  647. if (this.focus) {
  648. this.element.addEventListener('pointerover', (e) => {
  649. this.focus();
  650. e.stopPropagation();
  651. });
  652. }
  653. if (this.blur) {
  654. this.element.addEventListener('pointerleave', (e) => {
  655. this.blur();
  656. e.stopPropagation();
  657. });
  658. }
  659. if (this.activate) {
  660. this.element.addEventListener('click', (e) => {
  661. this.activate();
  662. e.stopPropagation();
  663. });
  664. }
  665. text.appendChild(name);
  666. this.element.appendChild(text);
  667. parent.appendChild(this.element);
  668. this.text = text;
  669. switch (this.type) {
  670. case 'node': {
  671. const node = this.content;
  672. node.build(document, this.element);
  673. break;
  674. }
  675. case 'node[]': {
  676. for (const node of this.content) {
  677. node.build(document, this.element);
  678. }
  679. break;
  680. }
  681. default: {
  682. const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
  683. tspan.textContent = (this.separator || '') + this.content;
  684. this.text.appendChild(tspan);
  685. break;
  686. }
  687. }
  688. }
  689. measure() {
  690. const yPadding = 1;
  691. const xPadding = 6;
  692. const size = this.text.getBBox();
  693. this.width = xPadding + size.width + xPadding;
  694. this.bottom = yPadding + size.height + yPadding;
  695. this.offset = size.y;
  696. this.height = this.bottom;
  697. if (this.type === 'node') {
  698. const node = this.content;
  699. node.measure();
  700. this.width = Math.max(150, this.width, node.width + (2 * xPadding));
  701. this.height += node.height + yPadding + yPadding + yPadding + yPadding;
  702. } else if (this.type === 'node[]') {
  703. for (const node of this.content) {
  704. node.measure();
  705. this.width = Math.max(150, this.width, node.width + (2 * xPadding));
  706. this.height += node.height + yPadding + yPadding + yPadding + yPadding;
  707. }
  708. }
  709. }
  710. layout() {
  711. const yPadding = 1;
  712. const xPadding = 6;
  713. let y = this.y + this.bottom;
  714. if (this.type === 'node') {
  715. const node = this.content;
  716. node.width = this.width - xPadding - xPadding;
  717. node.layout();
  718. node.x = this.x + xPadding + (node.width / 2);
  719. node.y = y + (node.height / 2) + yPadding + yPadding;
  720. } else if (this.type === 'node[]') {
  721. for (const node of this.content) {
  722. node.width = this.width - xPadding - xPadding;
  723. node.layout();
  724. node.x = this.x + xPadding + (node.width / 2);
  725. node.y = y + (node.height / 2) + yPadding + yPadding;
  726. y += node.height + yPadding + yPadding + yPadding + yPadding;
  727. }
  728. }
  729. }
  730. update() {
  731. const yPadding = 1;
  732. const xPadding = 6;
  733. this.text.setAttribute('x', this.x + xPadding);
  734. this.text.setAttribute('y', this.y + yPadding - this.offset);
  735. this.border.setAttribute('x', this.x + 3);
  736. this.border.setAttribute('y', this.y);
  737. this.border.setAttribute('width', this.width - 6);
  738. this.border.setAttribute('height', this.height);
  739. if (this.type === 'node') {
  740. const node = this.content;
  741. node.update();
  742. } else if (this.type === 'node[]') {
  743. for (const node of this.content) {
  744. node.update();
  745. }
  746. }
  747. }
  748. select() {
  749. if (this.element) {
  750. this.element.classList.add('select');
  751. return [this.element];
  752. }
  753. return [];
  754. }
  755. deselect() {
  756. if (this.element) {
  757. this.element.classList.remove('select');
  758. }
  759. }
  760. };
  761. grapher.Node.Canvas = class {
  762. constructor() {
  763. this.width = 0;
  764. this.height = 80;
  765. }
  766. build(/* document, parent */) {
  767. }
  768. update(/* parent, top, width , first, last */) {
  769. }
  770. };
  771. grapher.Edge = class {
  772. constructor(from, to) {
  773. this.from = from;
  774. this.to = to;
  775. }
  776. build(document, edgePathGroupElement, edgePathHitTestGroupElement, edgeLabelGroupElement) {
  777. const createElement = (name) => {
  778. return document.createElementNS('http://www.w3.org/2000/svg', name);
  779. };
  780. this.element = createElement('path');
  781. if (this.id) {
  782. this.element.setAttribute('id', this.id);
  783. }
  784. this.element.setAttribute('class', this.class ? `edge-path ${this.class}` : 'edge-path');
  785. edgePathGroupElement.appendChild(this.element);
  786. this.hitTest = createElement('path');
  787. edgePathHitTestGroupElement.appendChild(this.hitTest);
  788. if (this.label) {
  789. const tspan = createElement('tspan');
  790. tspan.setAttribute('xml:space', 'preserve');
  791. tspan.setAttribute('dy', '1em');
  792. tspan.setAttribute('x', '1');
  793. tspan.appendChild(document.createTextNode(this.label));
  794. this.labelElement = createElement('text');
  795. this.labelElement.appendChild(tspan);
  796. this.labelElement.style.opacity = 0;
  797. this.labelElement.setAttribute('class', 'edge-label');
  798. if (this.id) {
  799. this.labelElement.setAttribute('id', `edge-label-${this.id}`);
  800. }
  801. edgeLabelGroupElement.appendChild(this.labelElement);
  802. }
  803. }
  804. update() {
  805. const intersectRect = (node, point) => {
  806. const x = node.x;
  807. const y = node.y;
  808. const dx = point.x - x;
  809. const dy = point.y - y;
  810. let h = node.height / 2;
  811. let w = node.width / 2;
  812. if (Math.abs(dy) * w > Math.abs(dx) * h) {
  813. if (dy < 0) {
  814. h = -h;
  815. }
  816. return { x: x + (dy === 0 ? 0 : h * dx / dy), y: y + h };
  817. }
  818. if (dx < 0) {
  819. w = -w;
  820. }
  821. return { x: x + w, y: y + (dx === 0 ? 0 : w * dy / dx) };
  822. };
  823. const curvePath = (edge, tail, head) => {
  824. const points = edge.points.slice(1, edge.points.length - 1);
  825. points.unshift(intersectRect(tail, points[0]));
  826. points.push(intersectRect(head, points[points.length - 1]));
  827. return new grapher.Edge.Curve(points).path.data;
  828. };
  829. const edgePath = curvePath(this, this.from, this.to);
  830. this.element.setAttribute('d', edgePath);
  831. this.hitTest.setAttribute('d', edgePath);
  832. if (this.labelElement) {
  833. this.labelElement.setAttribute('transform', `translate(${this.x - (this.width / 2)},${this.y - (this.height / 2)})`);
  834. this.labelElement.style.opacity = 1;
  835. }
  836. }
  837. select() {
  838. if (this.element) {
  839. if (!this.element.classList.contains('select')) {
  840. const path = this.element;
  841. path.classList.add('select');
  842. this.element = path.cloneNode(true);
  843. path.parentNode.replaceChild(this.element, path);
  844. }
  845. return [this.element];
  846. }
  847. return [];
  848. }
  849. deselect() {
  850. if (this.element && this.element.classList.contains('select')) {
  851. const path = this.element;
  852. path.classList.remove('select');
  853. this.element = path.cloneNode(true);
  854. path.parentNode.replaceChild(this.element, path);
  855. }
  856. }
  857. };
  858. grapher.Edge.Curve = class {
  859. constructor(points) {
  860. this._path = new grapher.Edge.Path();
  861. this._x0 = NaN;
  862. this._x1 = NaN;
  863. this._y0 = NaN;
  864. this._y1 = NaN;
  865. this._state = 0;
  866. for (let i = 0; i < points.length; i++) {
  867. const point = points[i];
  868. this.point(point.x, point.y);
  869. if (i === points.length - 1) {
  870. switch (this._state) {
  871. case 3:
  872. this.curve(this._x1, this._y1);
  873. this._path.lineTo(this._x1, this._y1);
  874. break;
  875. case 2:
  876. this._path.lineTo(this._x1, this._y1);
  877. break;
  878. default:
  879. break;
  880. }
  881. if (this._line || (this._line !== 0 && this._point === 1)) {
  882. this._path.closePath();
  883. }
  884. this._line = 1 - this._line;
  885. }
  886. }
  887. }
  888. get path() {
  889. return this._path;
  890. }
  891. point(x, y) {
  892. x = Number(x);
  893. y = Number(y);
  894. switch (this._state) {
  895. case 0:
  896. this._state = 1;
  897. if (this._line) {
  898. this._path.lineTo(x, y);
  899. } else {
  900. this._path.moveTo(x, y);
  901. }
  902. break;
  903. case 1:
  904. this._state = 2;
  905. break;
  906. case 2:
  907. this._state = 3;
  908. this._path.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
  909. this.curve(x, y);
  910. break;
  911. default:
  912. this.curve(x, y);
  913. break;
  914. }
  915. this._x0 = this._x1;
  916. this._x1 = x;
  917. this._y0 = this._y1;
  918. this._y1 = y;
  919. }
  920. curve(x, y) {
  921. this._path.bezierCurveTo(
  922. (2 * this._x0 + this._x1) / 3,
  923. (2 * this._y0 + this._y1) / 3,
  924. (this._x0 + 2 * this._x1) / 3,
  925. (this._y0 + 2 * this._y1) / 3,
  926. (this._x0 + 4 * this._x1 + x) / 6,
  927. (this._y0 + 4 * this._y1 + y) / 6
  928. );
  929. }
  930. };
  931. grapher.Edge.Path = class {
  932. constructor() {
  933. this._x0 = null;
  934. this._y0 = null;
  935. this._x1 = null;
  936. this._y1 = null;
  937. this._data = '';
  938. }
  939. moveTo(x, y) {
  940. this._x0 = x;
  941. this._x1 = x;
  942. this._y0 = y;
  943. this._y1 = y;
  944. this._data += `M${x},${y}`;
  945. }
  946. lineTo(x, y) {
  947. this._x1 = x;
  948. this._y1 = y;
  949. this._data += `L${x},${y}`;
  950. }
  951. bezierCurveTo(x1, y1, x2, y2, x, y) {
  952. this._x1 = x;
  953. this._y1 = y;
  954. this._data += `C${x1},${y1},${x2},${y2},${x},${y}`;
  955. }
  956. closePath() {
  957. if (this._x1 !== null) {
  958. this._x1 = this._x0;
  959. this._y1 = this._y0;
  960. this._data += "Z";
  961. }
  962. }
  963. get data() {
  964. return this._data;
  965. }
  966. };
  967. export const { Graph, Node, Edge, Argument } = grapher;