JointJS create multiple links between elements

1.7k views Asked by At

I'm trying create a diagram like this using JointJS.

However, when I'm adding multiple links between elements, I'm only seeing 1 link show up. How do I go about adding multiple links with automatically adjusted space between them?

This is the code to add the boxes and links. Note that right now I'm just trying to add 3 links between all of the blocks, but I'm only seeing 1 link appear between each.

var steps = [{title: "Step 1"}, {title: "Step 2"}, {title: "Step 3"}];

steps.forEach(function(step, i){
    var title = step.title;
    var yOffset = i*150 + 50; //offsets first block by 50 in y and all others 150
    var xOffset = 60; //offsets all blocks by 60
    createBlock(title, xOffset, yOffset, i);
});

var blocks = [];

function createBlock(title, x, y, loc) {
    var x = (typeof x !== 'undefined') ?  x : 0;
    var y = (typeof y !== 'undefined') ?  y : 0;

    var newBlock = new joint.shapes.html.Element({
        position: { x: x, y: y },
        size: { width: 170, height: 100 },
        label: title,
        attrs: {
            '.label': {
                text: title,
                'ref-x': .5, 
                'ref-y': .4,
                fill: '#FFFFFF'
            },
        }
    });

    blocks.push(newBlock.id);

    graph.addCell(newBlock);

    if(blocks.length > 1) {
        var link = new joint.shapes.devs.Link({
            source: {
                id: blocks[loc-1],
            },
            target: {
                id: blocks[loc],
            },
        });
        graph.addCell(link);

        var link2 = new joint.shapes.devs.Link({
            source: {
                id: blocks[loc-1],
            },
            target: {
                id: blocks[loc],
            },
        });
        graph.addCell(link2);

        var link3 = new joint.shapes.devs.Link({
            source: {
                id: blocks[loc-1],
            },
            target: {
                id: blocks[loc],
            },
        });
        graph.addCell(link3);
    }
}
2

There are 2 answers

1
Vinay Prabhakaran On BEST ANSWER

all the links are lying on top of each other so you see it as a single one. There is code in the demo of jointjs to see each link in different paths. you could add the below code and see that the links show up in different paths. You will need to change the graph to your graph name in the below three lines

// displaying multiple links between two elements in different paths
      function adjustVertices(graph, cell) {
          // If the cell is a view, find its model.
          cell = cell.model || cell;
          if (cell instanceof joint.dia.Element) {
              _.chain(graph.getConnectedLinks(cell)).groupBy(function(link) {
                  // the key of the group is the model id of the link's source or target, but not our cell id.
                  return _.omit([link.get('source').id, link.get('target').id], cell.id)[0];
              }).each(function(group, key) {
                  // If the member of the group has both source and target model adjust vertices.
                  if (key !== 'undefined') adjustVertices(graph, _.first(group));
              });
              return;
          }

          // The cell is a link. Let's find its source and target models.
          var srcId = cell.get('source').id || cell.previous('source').id;
          var trgId = cell.get('target').id || cell.previous('target').id;

          // If one of the ends is not a model, the link has no siblings.
          if (!srcId || !trgId) return;

          var siblings = _.filter(graph.getLinks(), function(sibling) {

              var _srcId = sibling.get('source').id;
              var _trgId = sibling.get('target').id;

              return (_srcId === srcId && _trgId === trgId) || (_srcId === trgId && _trgId === srcId);
          });

          switch (siblings.length) {

          case 0:
              // The link was removed and had no siblings.
              break;

          case 1:
              // There is only one link between the source and target. No vertices needed.
              cell.unset('vertices');
              break;

          default:

              // There is more than one siblings. We need to create vertices.
              // First of all we'll find the middle point of the link.
              var srcCenter = graph.getCell(srcId).getBBox().center();
              var trgCenter = graph.getCell(trgId).getBBox().center();
              var midPoint = joint.g.line(srcCenter, trgCenter).midpoint();

              // Then find the angle it forms.
              var theta = srcCenter.theta(trgCenter);

              // This is the maximum distance between links
              var gap = 20;

              _.each(siblings, function(sibling, index) {
                  // We want the offset values to be calculated as follows 0, 20, 20, 40, 40, 60, 60 ..
                  var offset = gap * Math.ceil(index / 2);
                  // Now we need the vertices to be placed at points which are 'offset' pixels distant
                  // from the first link and forms a perpendicular angle to it. And as index goes up
                  // alternate left and right.
                  //
                  //  ^  odd indexes
                  //  |
                  //  |---->  index 0 line (straight line between a source center and a target center.
                  //  |
                  //  v  even indexes
                  var sign = index % 2 ? 1 : -1;
                  var angle = joint.g.toRad(theta + sign * 90);
                  // We found the vertex.
                  var vertex = joint.g.point.fromPolar(offset, angle, midPoint);
                  sibling.set('vertices', [{ x: vertex.x, y: vertex.y }]);
              });
          }
      };


      var myAdjustVertices = _.partial(adjustVertices, graph);
      // adjust vertices when a cell is removed or its source/target was changed
      graph.on('add remove change:source change:target', myAdjustVertices);
      // also when an user stops interacting with an element.
      graph.on('cell:pointerup', myAdjustVertices);
0
Lukmanul Hakim On

The core of the solution lies in the adjustVertices function presented below. It accepts a graph and a cell (link or element). For added convenience, the function accepts cell views as well as models.

If cell is a link, it will find all links with the same source and target and then set vertices on them; we will be calling those related links 'siblings'. If cell is an element, we execute our function for each distinct (different source and target) link connected to the element.

function adjustVertices(graph, cell) {

    // if `cell` is a view, find its model
    cell = cell.model || cell;

    if (cell instanceof joint.dia.Element) {
        // `cell` is an element

        _.chain(graph.getConnectedLinks(cell))
            .groupBy(function(link) {

                // the key of the group is the model id of the link's source or target
                // cell id is omitted
                return _.omit([link.source().id, link.target().id], cell.id)[0];
            })
            .each(function(group, key) {

                // if the member of the group has both source and target model
                // then adjust vertices
                if (key !== 'undefined') adjustVertices(graph, _.first(group));
            })
            .value();

        return;
    }

    // `cell` is a link
    // get its source and target model IDs
    var sourceId = cell.get('source').id || cell.previous('source').id;
    var targetId = cell.get('target').id || cell.previous('target').id;

    // if one of the ends is not a model
    // (if the link is pinned to paper at a point)
    // the link is interpreted as having no siblings
    if (!sourceId || !targetId) return;

    // identify link siblings
    var siblings = _.filter(graph.getLinks(), function(sibling) {

        var siblingSourceId = sibling.source().id;
        var siblingTargetId = sibling.target().id;

        // if source and target are the same
        // or if source and target are reversed
        return ((siblingSourceId === sourceId) && (siblingTargetId === targetId))
            || ((siblingSourceId === targetId) && (siblingTargetId === sourceId));
    });

    var numSiblings = siblings.length;
    switch (numSiblings) {

        case 0: {
            // the link has no siblings
            break;

        } case 1: {
            // there is only one link
            // no vertices needed
            cell.unset('vertices');
            break;

        } default: {
            // there are multiple siblings
            // we need to create vertices

            // find the middle point of the link
            var sourceCenter = graph.getCell(sourceId).getBBox().center();
            var targetCenter = graph.getCell(targetId).getBBox().center();
            var midPoint = g.Line(sourceCenter, targetCenter).midpoint();

            // find the angle of the link
            var theta = sourceCenter.theta(targetCenter);

            // constant
            // the maximum distance between two sibling links
            var GAP = 20;

            _.each(siblings, function(sibling, index) {

                // we want offset values to be calculated as 0, 20, 20, 40, 40, 60, 60 ...
                var offset = GAP * Math.ceil(index / 2);

                // place the vertices at points which are `offset` pixels perpendicularly away
                // from the first link
                //
                // as index goes up, alternate left and right
                //
                //  ^  odd indices
                //  |
                //  |---->  index 0 sibling - centerline (between source and target centers)
                //  |
                //  v  even indices
                var sign = ((index % 2) ? 1 : -1);

                // to assure symmetry, if there is an even number of siblings
                // shift all vertices leftward perpendicularly away from the centerline
                if ((numSiblings % 2) === 0) {
                    offset -= ((GAP / 2) * sign);
                }

                // make reverse links count the same as non-reverse
                var reverse = ((theta < 180) ? 1 : -1);

                // we found the vertex
                var angle = g.toRad(theta + (sign * reverse * 90));
                var vertex = g.Point.fromPolar(offset, angle, midPoint);

                // replace vertices array with `vertex`
                sibling.vertices([vertex]);
            });
        }
    }
}

We then attach the necessary event listeners (function bindInteractionEvents). The vertices are recalculated anytime the user translates an element - as well as anytime a link is added/removed or has its source or target changed.

 function bindInteractionEvents(adjustVertices, graph, paper) {

        // bind `graph` to the `adjustVertices` function
        var adjustGraphVertices = _.partial(adjustVertices, graph);

        // adjust vertices when a cell is removed or its source/target was changed
        graph.on('add remove change:source change:target', adjustGraphVertices);

        // adjust vertices when the user stops interacting with an element
        paper.on('cell:pointerup', adjustGraphVertices);
    }