github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/web/public/graph.mjs (about) 1 var KEY_HEIGHT = 20; 2 var KEY_SPACING = 10; 3 var RANK_GROUP_SPACING = 50; 4 var NODE_PADDING = 5; 5 6 export function Graph() { 7 this._nodes = {}; 8 this._edges = []; 9 }; 10 11 Graph.prototype.setNode = function(id, value) { 12 this._nodes[id] = value; 13 }; 14 15 Graph.prototype.removeNode = function(id) { 16 var node = this._nodes[id]; 17 18 for (var i in node._inEdges) { 19 var edge = node._inEdges[i]; 20 var sourceNode = edge.source.node; 21 var idx = sourceNode._outEdges.indexOf(edge); 22 sourceNode._outEdges.splice(idx, 1); 23 24 var graphIdx = this._edges.indexOf(edge); 25 this._edges.splice(graphIdx, 1); 26 } 27 28 for (var i in node._outEdges) { 29 var edge = node._outEdges[i]; 30 var targetNode = edge.target.node; 31 var idx = targetNode._inEdges.indexOf(edge); 32 targetNode._inEdges.splice(idx, 1); 33 34 var graphIdx = this._edges.indexOf(edge); 35 this._edges.splice(graphIdx, 1); 36 } 37 38 delete this._nodes[id]; 39 } 40 41 Graph.prototype.addEdge = function(sourceId, targetId, key, customData) { 42 var source = this._nodes[sourceId]; 43 if (source === undefined) { 44 throw "source node does not exist: " + sourceId; 45 } 46 47 var target = this._nodes[targetId]; 48 if (target === undefined) { 49 throw "target node does not exist: " + targetId; 50 } 51 52 for (var i in target._inEdges) { 53 if (target._inEdges[i].source.node.id == source.id) { 54 // edge already exists; skip 55 return; 56 } 57 } 58 59 if (source._edgeKeys.indexOf(key) == -1) { 60 source._edgeKeys.push(key); 61 } 62 63 if (target._edgeKeys.indexOf(key) == -1) { 64 target._edgeKeys.push(key); 65 } 66 67 var edgeSource = source._edgeSources[key]; 68 if (!edgeSource) { 69 edgeSource = new EdgeSource(source, key); 70 source._edgeSources[key] = edgeSource; 71 } 72 73 var edgeTarget = target._edgeTargets[key]; 74 if (!edgeTarget) { 75 edgeTarget = new EdgeTarget(target, key); 76 target._edgeTargets[key] = edgeTarget; 77 } 78 79 var edge = new Edge(edgeSource, edgeTarget, key, customData); 80 target._inEdges.push(edge); 81 source._outEdges.push(edge); 82 this._edges.push(edge); 83 } 84 85 Graph.prototype.removeEdge = function(edge) { 86 var inIdx = edge.target.node._inEdges.indexOf(edge); 87 edge.target.node._inEdges.splice(inIdx, 1); 88 89 var outIdx = edge.source.node._outEdges.indexOf(edge); 90 edge.source.node._outEdges.splice(outIdx, 1); 91 92 var graphIdx = this._edges.indexOf(edge); 93 this._edges.splice(graphIdx, 1); 94 } 95 96 Graph.prototype.node = function(id) { 97 return this._nodes[id]; 98 }; 99 100 Graph.prototype.nodes = function() { 101 var nodes = []; 102 103 for (var id in this._nodes) { 104 nodes.push(this._nodes[id]); 105 } 106 107 return nodes; 108 }; 109 110 Graph.prototype.edges = function() { 111 return this._edges; 112 }; 113 114 Graph.prototype.layout = function() { 115 var rankGroups = []; 116 117 for (var i in this._nodes) { 118 var node = this._nodes[i]; 119 120 var rankGroupIdx = node.rank(); 121 var rankGroup = rankGroups[rankGroupIdx]; 122 if (!rankGroup) { 123 rankGroup = new RankGroup(rankGroupIdx); 124 rankGroups[rankGroupIdx] = rankGroup; 125 } 126 127 rankGroup.nodes.push(node); 128 } 129 130 for (var i in this._nodes) { 131 var node = this._nodes[i]; 132 133 var rankGroup = node.rank(); 134 135 var rankGroupOffset = 0; 136 for (var c in rankGroups) { 137 if (c < rankGroup) { 138 rankGroupOffset += rankGroups[c].width() + RANK_GROUP_SPACING; 139 } 140 } 141 142 node._position.x = rankGroupOffset + ((rankGroups[rankGroup].width() - node.width()) / 2); 143 144 node._edgeKeys.sort(function(a, b) { 145 var targetA = node._edgeTargets[a]; 146 var targetB = node._edgeTargets[b]; 147 if (targetA && !targetB) { 148 return -1; 149 } else if (!targetA && targetB) { 150 return 1; 151 } 152 153 if (targetA && targetB) { 154 var introRankA = targetA.rankOfFirstAppearance(); 155 var introRankB = targetB.rankOfFirstAppearance(); 156 if(introRankA < introRankB) { 157 return -1; 158 } else if (introRankA > introRankB) { 159 return 1; 160 } 161 } 162 163 var sourceA = node._edgeSources[a]; 164 var sourceB = node._edgeSources[b]; 165 if (sourceA && !sourceB) { 166 return -1; 167 } else if (!sourceA && sourceB) { 168 return 1; 169 } 170 171 return compareNames(a, b); 172 }); 173 } 174 175 // first pass: initial rough sorting and layout 176 // second pass: detangle now that we know downstream positioning 177 for (var repeat = 0; repeat < 2; repeat++) { 178 for (var c in rankGroups) { 179 rankGroups[c].sortNodes(); 180 rankGroups[c].layout(); 181 } 182 } 183 184 if (window.location.hash != "#untug") { 185 var anyChanged = true; 186 while (anyChanged) { 187 anyChanged = false; 188 for (var c in rankGroups) { 189 if (rankGroups[c].tug()) { 190 anyChanged = true; 191 } 192 } 193 } 194 } 195 } 196 197 Graph.prototype.computeRanks = function() { 198 var forwardNodes = {}; 199 200 for (var n in this._nodes) { 201 var node = this._nodes[n]; 202 203 if (node._inEdges.length == 0) { 204 node._cachedRank = 0; 205 forwardNodes[node.id] = node; 206 } 207 } 208 209 var bottomNodes = {}; 210 211 // walk over all nodes from left to right and determine their rank 212 while (!objectIsEmpty(forwardNodes)) { 213 var nextNodes = {}; 214 215 for (var n in forwardNodes) { 216 var node = forwardNodes[n]; 217 218 if (node._outEdges.length == 0) { 219 bottomNodes[node.id] = node; 220 } 221 222 for (var e in node._outEdges) { 223 var nextNode = node._outEdges[e].target.node; 224 225 // careful: two edges may go to the same node but be from different 226 // ranks, so always destination nodes as far to the right as possible 227 nextNode._cachedRank = Math.max(nextNode._cachedRank, node._cachedRank + 1); 228 229 if (nextNode._cachedRank > 10000) { 230 throw new Error( 231 "Likely infinite loop involving: [" + 232 node.id + "] and [" + 233 nextNode.id + "]"); 234 } 235 236 nextNodes[nextNode.id] = nextNode; 237 } 238 } 239 240 forwardNodes = nextNodes; 241 } 242 243 var backwardNodes = bottomNodes; 244 245 // walk over all nodes from right to left and bring upstream nodes as far 246 // to the right as possible, so that edges aren't passing through ranks 247 while (!objectIsEmpty(backwardNodes)) { 248 var prevNodes = {}; 249 250 for (var n in backwardNodes) { 251 var node = backwardNodes[n]; 252 253 // for all upstream nodes, determine latest possible rank group by 254 // taking the minimum rank of all downstream nodes and placing it in the 255 // rank immediately preceding it 256 for (var e in node._inEdges) { 257 var prevNode = node._inEdges[e].source.node; 258 259 var latestRank = prevNode.latestPossibleRank(); 260 if (latestRank !== undefined) { 261 prevNode._cachedRank = latestRank; 262 } 263 264 prevNodes[prevNode.id] = prevNode; 265 } 266 } 267 268 backwardNodes = prevNodes; 269 } 270 }; 271 272 Graph.prototype.collapseEquivalentNodes = function() { 273 var nodesByRank = []; 274 275 for (var n in this._nodes) { 276 var node = this._nodes[n]; 277 278 var byRank = nodesByRank[node.rank()]; 279 if (byRank === undefined) { 280 byRank = {}; 281 nodesByRank[node.rank()] = byRank; 282 } 283 284 if (node.equivalentBy === undefined) { 285 continue; 286 } 287 288 byEqv = byRank[node.equivalentBy]; 289 if (byEqv === undefined) { 290 byEqv = []; 291 byRank[node.equivalentBy] = byEqv; 292 } 293 294 byEqv.push(node); 295 } 296 297 for (var r in nodesByRank) { 298 var byEqv = nodesByRank[r]; 299 for (var e in byEqv) { 300 var nodes = byEqv[e]; 301 if (nodes.length == 1) { 302 continue; 303 } 304 305 var chosenOne = nodes[0]; 306 for (var i = 1; i < nodes.length; i++) { 307 var loser = nodes[i]; 308 309 for (var ie in loser._inEdges) { 310 var edge = loser._inEdges[ie]; 311 this.addEdge(edge.source.node.id, chosenOne.id, edge.key, edge.customData); 312 } 313 314 for (var oe in loser._outEdges) { 315 var edge = loser._outEdges[oe]; 316 this.addEdge(chosenOne.id, edge.target.node.id, edge.key, edge.customData); 317 } 318 319 this.removeNode(loser.id); 320 } 321 } 322 } 323 } 324 325 Graph.prototype.addSpacingNodes = function() { 326 var edgesToRemove = []; 327 for (var e in this._edges) { 328 var edge = this._edges[e]; 329 var delta = edge.target.node.rank() - edge.source.node.rank(); 330 if (delta > 1) { 331 var upstreamNode = edge.source.node; 332 var downstreamNode = edge.target.node; 333 334 var repeatedNode; 335 var initialCustomData; 336 var finalCustomData; 337 if (edge.source.node.repeatable) { 338 repeatedNode = upstreamNode; 339 initialCustomData = null; 340 finalCustomData = edge.customData; 341 } else { 342 repeatedNode = downstreamNode; 343 initialCustomData = edge.customData; 344 finalCustomData = null; 345 } 346 347 for (var i = 0; i < (delta - 1); i++) { 348 var spacerID = edge.id() + "-spacing-" + i; 349 350 var spacingNode = this.node(spacerID); 351 if (!spacingNode) { 352 spacingNode = repeatedNode.copy(); 353 spacingNode.id = spacerID; 354 spacingNode._cachedRank = upstreamNode.rank() + 1; 355 this.setNode(spacingNode.id, spacingNode); 356 } 357 358 var currentCustomData = (i == 0 ? initialCustomData : null) 359 this.addEdge(upstreamNode.id, spacingNode.id, edge.key, currentCustomData); 360 361 upstreamNode = spacingNode; 362 } 363 364 this.addEdge(upstreamNode.id, edge.target.node.id, edge.key, finalCustomData); 365 366 edgesToRemove.push(edge); 367 } 368 } 369 370 for (var e in edgesToRemove) { 371 this.removeEdge(edgesToRemove[e]); 372 } 373 } 374 375 function Ordering() { 376 this.spaces = []; 377 } 378 379 Ordering.prototype.fill = function(pos, len) { 380 for (var i = pos; i < pos + len; i++) { 381 this.spaces[i] = true; 382 } 383 } 384 385 Ordering.prototype.free = function(pos, len) { 386 for (var i = pos; i < pos + len; i++) { 387 this.spaces[i] = false; 388 } 389 } 390 391 Ordering.prototype.isFree = function(pos, len) { 392 for (var i = pos; i < pos + len; i++) { 393 if (this.spaces[i]) { 394 return false; 395 } 396 } 397 398 return true; 399 } 400 401 function RankGroup(idx) { 402 this.index = idx; 403 this.nodes = []; 404 405 this.ordering = new Ordering(); 406 } 407 408 RankGroup.prototype.sortNodes = function() { 409 var nodes = this.nodes; 410 411 var before = this.nodes.slice(); 412 413 nodes.sort(function(a, b) { 414 if (a._inEdges.length && b._inEdges.length) { 415 // position nodes closer to their upstream sources 416 var compare = a.highestUpstreamSource() - b.highestUpstreamSource(); 417 if (compare != 0) { 418 return compare; 419 } 420 } 421 422 if (a._outEdges.length && b._outEdges.length) { 423 // position nodes closer to their downstream targets 424 var compare = a.highestDownstreamTarget() - b.highestDownstreamTarget(); 425 if (compare != 0) { 426 return compare; 427 } 428 } 429 430 if (a._inEdges.length && b._outEdges.length) { 431 // position nodes closer to their sources than others that are just 432 // closer to their destinations 433 var compare = a.highestUpstreamSource() - b.highestDownstreamTarget(); 434 if (compare != 0) { 435 return compare; 436 } 437 } 438 439 if (a._outEdges.length && b._inEdges.length) { 440 // position nodes closer to their sources than others that are just 441 // closer to their destinations 442 var compare = a.highestDownstreamTarget() - b.highestUpstreamSource(); 443 if (compare != 0) { 444 return compare; 445 } 446 } 447 448 // place nodes that threaded through upstream nodes higher 449 var aPassedThrough = a.passedThroughAnyPreviousNode(); 450 var bPassedThrough = b.passedThroughAnyPreviousNode(); 451 if (aPassedThrough && !bPassedThrough) { 452 return -1; 453 } 454 455 // place nodes that thread through downstream nodes higher 456 var aPassesThrough = a.passesThroughAnyNextNode(); 457 var bPassesThrough = b.passesThroughAnyNextNode(); 458 if (aPassesThrough && !bPassesThrough) { 459 return -1; 460 } 461 462 // place nodes with more out edges higher 463 var byOutEdges = b._outEdges.length - a._outEdges.length; 464 if (byOutEdges != 0) { 465 return byOutEdges; 466 } 467 468 if (!aPassesThrough && bPassesThrough) { 469 return 1; 470 } 471 472 // both are of equivalent; compare names so it's at least deterministic 473 474 a.debugMarked = true; // to aid in debugging (adds .marked css class) 475 b.debugMarked = true; 476 477 return compareNames(a.name, b.name); 478 }); 479 480 var changed = false; 481 482 for (var c in nodes) { 483 if (nodes[c] !== before[c]) { 484 changed = true; 485 } 486 } 487 488 return changed; 489 } 490 491 RankGroup.prototype.mark = function() { 492 for (var i in this.nodes) { 493 this.nodes[i].rankGroupMarked = true; 494 } 495 } 496 497 RankGroup.prototype.width = function() { 498 var width = 0; 499 500 for (var i in this.nodes) { 501 width = Math.max(width, this.nodes[i].width()) 502 } 503 504 return width; 505 } 506 507 RankGroup.prototype.layout = function() { 508 var rollingKeyOffset = 0; 509 510 this.ordering = new Ordering(); 511 512 for (var i in this.nodes) { 513 var node = this.nodes[i]; 514 515 node._keyOffset = rollingKeyOffset; 516 517 this.ordering.fill(rollingKeyOffset, node._edgeKeys.length); 518 519 rollingKeyOffset += Math.max(node._edgeKeys.length, 1); 520 } 521 } 522 523 RankGroup.prototype.tug = function() { 524 var changed = false; 525 526 for (var i = this.nodes.length - 1; i >= 0; i--) { 527 var node = this.nodes[i]; 528 529 var align = node.inAlignment(); 530 if (align !== undefined && node._keyOffset < align && this.ordering.isFree(align, node._edgeKeys.length)) { 531 this.ordering.free(node._keyOffset, node._edgeKeys.length); 532 node._keyOffset = align; 533 this.ordering.fill(node._keyOffset, node._edgeKeys.length); 534 changed = true; 535 } else { 536 align = node.outAlignment(); 537 if (align !== undefined && node._keyOffset < align && this.ordering.isFree(align, node._edgeKeys.length)) { 538 this.ordering.free(node._keyOffset, node._edgeKeys.length); 539 node._keyOffset = align; 540 this.ordering.fill(node._keyOffset, node._edgeKeys.length); 541 changed = true; 542 } 543 } 544 } 545 546 this.nodes.sort(function(a, b) { 547 return a._keyOffset - b._keyOffset; 548 }); 549 550 return changed; 551 } 552 553 export function GraphNode(opts) { 554 // Graph node ID 555 this.id = opts.id; 556 this.name = opts.name; 557 this.icon = opts.icon; 558 this.class = opts.class; 559 this.status = opts.status; 560 this.repeatable = opts.repeatable; 561 this.key = opts.key; 562 this.url = opts.url; 563 this.svg = opts.svg; 564 this.equivalentBy = opts.equivalentBy; 565 566 // DOM element 567 this.label = undefined; 568 569 // [EdgeTarget] 570 this._edgeTargets = {}; 571 572 // [EdgeSource] 573 this._edgeSources = {}; 574 575 this._edgeKeys = []; 576 this._inEdges = []; 577 this._outEdges = []; 578 579 this._cachedRank = -1; 580 this._cachedWidth = 0; 581 582 this._keyOffset = 0; 583 584 // position (determined by graph.layout()) 585 this._position = { 586 x: 0, 587 y: 0 588 }; 589 }; 590 591 GraphNode.prototype.copy = function() { 592 return new GraphNode({ 593 id: this.id, 594 name: this.name, 595 class: this.class, 596 status: this.status, 597 repeatable: this.repeatable, 598 key: this.key, 599 url: this.url, 600 svg: this.svg, 601 equivalentBy: this.equivalentBy 602 }); 603 }; 604 605 GraphNode.prototype.width = function() { 606 if (this._cachedWidth == 0) { 607 var id = this.id; 608 609 var svgNode = this.svg.selectAll("g.node").filter(function(node) { 610 return node.id == id; 611 }) 612 613 var textNode = svgNode.select("text").node(); 614 var imageNode = svgNode.select("image").node(); 615 var iconNode = svgNode.select("use").node(); 616 617 var width = 0; 618 619 if (textNode) { 620 width += textNode.getBBox().width; 621 } 622 if (imageNode) { 623 width += imageNode.getBBox().width; 624 } 625 if (iconNode) { 626 width += Math.max(iconNode.getBBox().width, iconNode.width.baseVal.value); 627 } 628 629 if (textNode && imageNode && iconNode) { 630 width += NODE_PADDING * 2; 631 } 632 if ((textNode && imageNode && !iconNode) 633 || (textNode && !imageNode && iconNode) 634 || (!textNode && imageNode && iconNode)) { 635 width += NODE_PADDING; 636 } 637 638 if (width == 0) { 639 return 0; 640 } 641 642 this._cachedWidth = width; 643 } 644 645 return this._cachedWidth + (NODE_PADDING * 2); 646 } 647 648 GraphNode.prototype.padding = function() { 649 return NODE_PADDING; 650 } 651 652 GraphNode.prototype.pinned = function() { 653 return this.class.includes("pinned"); 654 } 655 656 GraphNode.prototype.has_icon = function() { 657 return typeof this.icon !== 'undefined'; 658 } 659 660 GraphNode.prototype.height = function() { 661 var keys = Math.max(this._edgeKeys.length, 1); 662 return (KEY_HEIGHT * keys) + (KEY_SPACING * (keys - 1)); 663 } 664 665 GraphNode.prototype.position = function() { 666 return { 667 x: this._position.x, 668 y: (KEY_HEIGHT + KEY_SPACING) * this._keyOffset 669 } 670 } 671 672 /* spacing required for firefox to not clip ripple border animation */ 673 GraphNode.prototype.animationRadius = function() { 674 if (this.class.search('job') > -1) { 675 return 70 676 } 677 678 return 0 679 } 680 681 GraphNode.prototype.rank = function() { 682 return this._cachedRank; 683 } 684 685 GraphNode.prototype.latestPossibleRank = function() { 686 var latestRank; 687 688 for (var o in this._outEdges) { 689 var prevTargetNode = this._outEdges[o].target.node; 690 var targetPrecedingRank = prevTargetNode.rank() - 1; 691 692 if (latestRank === undefined) { 693 latestRank = targetPrecedingRank; 694 } else { 695 latestRank = Math.min(latestRank, targetPrecedingRank); 696 } 697 } 698 699 return latestRank; 700 } 701 702 GraphNode.prototype.dependsOn = function(node, stack) { 703 for (var i in this._inEdges) { 704 var source = this._inEdges[i].source.node; 705 706 if (source == node) { 707 return true; 708 } 709 710 if (stack.indexOf(this) != -1) { 711 continue; 712 } 713 714 stack.push(this) 715 716 if (source.dependsOn(node, stack)) { 717 return true; 718 } 719 } 720 721 return false; 722 } 723 724 GraphNode.prototype.highestUpstreamSource = function() { 725 var minY; 726 727 var y; 728 for (var e in this._inEdges) { 729 y = this._inEdges[e].source.effectiveKeyOffset(); 730 731 if (minY === undefined || y < minY) { 732 minY = y; 733 } 734 } 735 736 return minY; 737 }; 738 739 GraphNode.prototype.highestDownstreamTarget = function() { 740 var minY; 741 742 var y; 743 for (var e in this._outEdges) { 744 y = this._outEdges[e].target.effectiveKeyOffset(); 745 746 if (minY === undefined || y < minY) { 747 minY = y; 748 } 749 } 750 751 return minY; 752 }; 753 754 GraphNode.prototype.inAlignment = function() { 755 var minAlignment; 756 757 for (var e in this._inEdges) { 758 var edge = this._inEdges[e]; 759 var offset = edge.source.effectiveKeyOffset(); 760 if (minAlignment === undefined || offset < minAlignment) { 761 minAlignment = offset - this._edgeKeys.indexOf(edge.key); 762 } 763 } 764 765 return minAlignment; 766 }; 767 768 GraphNode.prototype.outAlignment = function() { 769 var minAlignment; 770 771 for (var e in this._outEdges) { 772 var edge = this._outEdges[e]; 773 var offset = edge.target.effectiveKeyOffset(); 774 if (minAlignment === undefined || offset < minAlignment) { 775 minAlignment = offset - this._edgeKeys.indexOf(edge.key); 776 } 777 } 778 779 return minAlignment; 780 }; 781 782 GraphNode.prototype.passedThroughAnyPreviousNode = function() { 783 for (var e in this._inEdges) { 784 var edge = this._inEdges[e]; 785 if (edge.key in edge.source.node._edgeTargets) { 786 return true; 787 } 788 } 789 790 return false; 791 }; 792 793 GraphNode.prototype.passesThroughAnyNextNode = function() { 794 for (var e in this._outEdges) { 795 var edge = this._outEdges[e]; 796 if (edge.key in edge.target.node._edgeSources) { 797 return true; 798 } 799 } 800 801 return false; 802 }; 803 804 function Edge(source, target, key, customData) { 805 this.source = source; 806 this.target = target; 807 this.key = key; 808 this.customData = customData; 809 } 810 811 Edge.prototype.id = function() { 812 return this.source.id() + "-to-" + this.target.id(); 813 } 814 815 Edge.prototype.bezierPoints = function() { 816 var sourcePosition = this.source.position(); 817 var targetPosition = this.target.position(); 818 819 var curvature = 0.5; 820 var point2, point3; 821 822 if (sourcePosition.x > targetPosition.x) { 823 var belowSourceNode = this.source.node.position().y + this.source.node.height(), 824 belowTargetNode = this.target.node.position().y + this.target.node.height(); 825 826 point2 = { 827 x: sourcePosition.x + 100, 828 y: belowSourceNode + 100 829 } 830 831 point3 = { 832 x: targetPosition.x - 100, 833 y: belowTargetNode + 100 834 } 835 } else { 836 var xi = d3.interpolateNumber(sourcePosition.x, targetPosition.x); 837 838 point2 = { 839 x: xi(curvature), 840 y: sourcePosition.y 841 } 842 843 point3 = { 844 x: xi(1 - curvature), 845 y: targetPosition.y 846 } 847 } 848 849 var points = [sourcePosition, point2, point3, targetPosition] 850 return points 851 } 852 853 Edge.prototype.path = function() { 854 const points = this.bezierPoints() 855 return "M" + points[0].x + "," + points[0].y 856 + " C" + points[1].x + "," + points[1].y 857 + " " + points[2].x + "," + points[2].y 858 + " " + points[3].x + "," + points[3].y; 859 } 860 861 function EdgeSource(node, key) { 862 // GraphNode 863 this.node = node; 864 865 // Key 866 this.key = key; 867 }; 868 869 EdgeSource.prototype.width = function() { 870 return 0; 871 } 872 873 EdgeSource.prototype.height = function() { 874 return 0; 875 } 876 877 EdgeSource.prototype.effectiveKeyOffset = function() { 878 return this.node._keyOffset + this.node._edgeKeys.indexOf(this.key); 879 } 880 881 EdgeSource.prototype.id = function() { 882 return this.node.id + "-" + this.key + "-source"; 883 } 884 885 EdgeSource.prototype.position = function() { 886 return { 887 x: this.node.position().x + this.node.width(), 888 y: (KEY_HEIGHT / 2) + this.effectiveKeyOffset() * (KEY_HEIGHT + KEY_SPACING) 889 } 890 }; 891 892 function EdgeTarget(node, key) { 893 // GraphNode 894 this.node = node; 895 896 // Key 897 this.key = key; 898 }; 899 900 EdgeTarget.prototype.width = function() { 901 return 0; 902 } 903 904 EdgeTarget.prototype.height = function() { 905 return 0; 906 } 907 908 EdgeTarget.prototype.effectiveKeyOffset = function() { 909 return this.node._keyOffset + this.node._edgeKeys.indexOf(this.key); 910 } 911 912 EdgeTarget.prototype.rankOfFirstAppearance = function() { 913 if (this._rankOfFirstAppearance !== undefined) { 914 return this._rankOfFirstAppearance; 915 } 916 917 var inEdges = this.node._inEdges; 918 var rank = Infinity; 919 for (var i in inEdges) { 920 var inEdge = inEdges[i]; 921 922 if (inEdge.source.key == this.key) { 923 var upstreamNodeInEdges = inEdge.source.node._inEdges; 924 925 if (upstreamNodeInEdges.length == 0) { 926 rank = inEdge.source.node.rank(); 927 break; 928 } 929 930 var foundUpstreamInEdge = false; 931 for (var j in upstreamNodeInEdges) { 932 var upstreamEdge = upstreamNodeInEdges[j]; 933 934 if (upstreamEdge.target.key == this.key) { 935 foundUpstreamInEdge = true; 936 937 var upstreamRank = upstreamEdge.target.rankOfFirstAppearance() 938 939 if (upstreamRank < rank) { 940 rank = upstreamRank; 941 } 942 } 943 } 944 945 if (!foundUpstreamInEdge) { 946 rank = inEdge.source.node.rank(); 947 break; 948 } 949 } 950 } 951 952 this._rankOfFirstAppearance = rank; 953 954 return rank; 955 } 956 957 EdgeTarget.prototype.id = function() { 958 return this.node.id + "-" + this.key + "-target"; 959 } 960 961 EdgeTarget.prototype.position = function() { 962 return { 963 x: this.node.position().x, 964 y: (KEY_HEIGHT / 2) + this.effectiveKeyOffset() * (KEY_HEIGHT + KEY_SPACING) 965 } 966 }; 967 968 function compareNames(a, b) { 969 var byLength = a.length - b.length; 970 if (byLength != 0) { 971 // place shorter names higher. pretty arbitrary but looks better. 972 return byLength; 973 } 974 975 return a.localeCompare(b); 976 } 977 978 function objectIsEmpty(o) { 979 for (var x in o) { 980 return false; 981 } 982 983 return true; 984 }