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  }