3

Ok, don't laugh. I'm trying to make curvy edges that detour around nodes that are between the source and target. I couldn't figure out how to do it (I read http://js.cytoscape.org/#style/bezier-edges, but didn't understand it), so I put fake nodes into empty places on the way from source to target and made a series of edges. The result is pretty ridiculous:

really bad edges

What's the right way to do this? I was aiming for something slightly more elegant, like:

http://twitter.github.io/labella.js/ from http://twitter.github.io/labella.js/

Based on @maxkfranz's advice below I did get a lot closer:

enter image description here enter image description here

but finally decided to give up. It's taking too long. Going back to straight edges. If anyone ever reads this and can describe a way to accomplish my goal in Cytoscape.js or some other tool, I'd love to hear it.

To be clear about what I'm doing before giving up, I:

  • lay out nodes on grid such that there is always an empty grid cell between any two nodes on the same row (this is less than ideal because the empty cells should preferably be as narrow as possible)
  • for every row on the grid layout between source and target nodes:
    • specify a "waypoint" closest to the direct path from source (or the previous waypoint) to target that passes through an empty grid cell,
    • convert row/col coordinates of the waypoint into pixel coordinates,
    • convert those into distance/weight values (based on function described here)
    • use those for unbundled bezier control points.

Here's the most relevant part of my code:

  function waypoints(from, to) {
    let stubPoint = { row: from.data().row, col: from.data().col, 
                      distance: 0, weight: .5, };
    if (from.data().layer === to.data().layer)
      return [stubPoint];
    let fromCol = from.data().col, 
        curCol = fromCol,
        toCol = to.data().col,
        fromRow = from.data().row,
        curRow = fromRow,
        toRow = to.data().row,
        fromX = from.position().x,
        fromY = from.position().y,
        toX = to.position().x,
        toY = to.position().y,
        x = d3.scaleLinear().domain([fromCol,toCol]).range([fromX,toX]),
        y = d3.scaleLinear().domain([fromRow,toRow]).range([fromY,toY]);
    let rowsBetween = _.range(fromRow, toRow).slice(1);
    let edgeLength = pointToPointDist(x(fromCol),y(fromRow),x(toCol),y(toRow));
    let points = rowsBetween.map(
      (nextRow,i) => {
        let colsRemaining = toCol - curCol;
        let colsNow = Math.ceil(colsRemaining / Math.abs(toRow - curRow));
        let nextCol = findEmptyGridCol(nextRow, curCol, curCol + colsNow);
        let curX = x(curCol), curY = y(curRow),
            nextX = x(nextCol), nextY = y(nextRow);
        let [distanceFromEdge, distanceOnEdge] = 
              perpendicular_coords(curX, curY, toX, toY, nextX, nextY);

        let point = {curCol, curRow, 
                      toCol, toRow,
                      nextCol, nextRow,
                      curX, curY, toX, toY, nextX, nextY,
                      edgeLength,
                      colsNow,
                      //wayCol, wayRow,
                      distance:-distanceFromEdge * 2, 
                      weight:distanceOnEdge / edgeLength,
                    };
        curCol = nextCol;
        curRow = nextRow;
        return point;
      });
    if (points.length === 0)
        return [stubPoint];
    return points;
  }
Community
  • 1
  • 1
Sigfried
  • 2,943
  • 3
  • 31
  • 43

1 Answers1

4

The set of control points for an edge is defined by N weights (w1, w2, ... wN) and N respective distances (d1, d2, ... dN).

The weight defines how close a point is to the source or target, and the distance controls how far away from a source-target line the point should be. The sign of the distance controls the side of the source-target line that the curve lies on (i.e the handedness). See the docs for more info: http://js.cytoscape.org/#style/bezier-edges

The following diagram summarises the values for a single point for simplicity, but you could extend this to multiple points:

Weight and distance

Note that the above image assumes edge-distances: node-position, whereas the following diagram assumes edge-distances: intersection (default):

enter image description here

For complex usecases, edge-distances: node-position makes calculations easier -- but you have to be more careful to not specify points in the node body. The end result is almost the same for these examples, so I didn't update the curve. Larger nodes of different sizes would make the difference between these cases more apparent.

A bezier's control point does not intersect the control point. The control point "pulls" the curve towards the point. For quadratic bezier like in Cytoscape, the distance of the curve will be half that of the control point. For more info on beziers, see Wikipedia: https://en.wikipedia.org/wiki/B%C3%A9zier_curve

On the other hand, segment edges will intersect the points you specify, because they're just a series of straight lines: http://js.cytoscape.org/#style/segments-edges

maxkfranz
  • 11,896
  • 1
  • 27
  • 36
  • Thanks, @maxfranz! I was going to ask how to specify control points to make the curve pass through multiple points, but then I found this: http://stackoverflow.com/questions/7715788/find-bezier-control-points-for-curve-passing-through-n-points. The first answer suggests I will need to use multiple connected curves, the second that I might be able to get a good approximation (I think) with one curve. – Sigfried Jan 14 '17 at 12:26
  • @Sigfried You can specify multiple control points per edge, but each point just specifies a single point for a single quadratic bezier. This allows you to chain multiple quadratic beziers together for a single edge. The curves are joined for you automatically. Just remember that the specified points *pull out* the *extrema* of the curves -- they don't specify points *on* the curve. – maxkfranz Jan 14 '17 at 19:39
  • Thanks again, @maxfranz. I'm getting there. Another question: I'm using grid layout for nodes, but I think the control-point/segment distances need to be in pixels. Is there some straightforward way to translate rows/cols into pixels, or do I need to calculate it by finding the ratio between (in the x direction) the col distance of two nodes and their position.x distance? – Sigfried Jan 17 '17 at 11:30
  • You're not going to know end positions until the layout is done. Even simple layouts like grid are more than just a one-line formula. – maxkfranz Jan 25 '17 at 22:52
  • Thanks, @maxfranz. I decided this was taking too long and gave up (described what I learned in question above), but this part, I did end up dealing with by extracting x/y positions from the nodes after layout and using those to calculate control points rather than row/col positions. – Sigfried Jan 26 '17 at 11:31
  • @Sigfried It's not quite as easy as it looks. There are several research papers about this, and ultimately it's probably something best handled on the layout level -- the layout having more context. Possibly, each layout could use a general bundling API for more code re-use. Here's one paper if you're interested : http://vis.stanford.edu/files/2011-DividedEdgeBundling-InfoVis.pdf – maxkfranz Jan 27 '17 at 01:27