flowcharts in d3js using dagre-d3 or colajs
Asked Answered
W

1

15

After seeing the quite complex TCP state diagram example of dagre-d3, I figured it would be able to resolve diagrams of similar complexity. In the following diagram, this clearly isn't the case. If the two marked nodes were swapped, all crossings would be resolved. diagram created using dagre and d3.js

Another interesting thing is that how good the graph is solved seems to depend on the order I set the edges in.

The following code

g.setEdge("148570019_1100", "148570020_1100", { label: "" });
g.setEdge("148570019_1100", "148570021_1100", { label: "" });
g.setEdge("148570019_1100", "148570010_1100", { label: "" });

doesn't give the same results as this:

g.setEdge("148570019_1100", "148570010_1100", { label: "" });
g.setEdge("148570019_1100", "148570020_1100", { label: "" });
g.setEdge("148570019_1100", "148570021_1100", { label: "" });

See these two examples:

As suggested, I tried to use cola.js instead, and this is what the same graph looks like: diagram created using cola.js and d3.js

As you see, colajs is better at crossing reduction, but the layout isn't nearly as structured and clear as dagre's. I use the following settings for colajs:

cola.d3adaptor()
      .avoidOverlaps(true)
      .convergenceThreshold(1e-3)
      .symmetricDiffLinkLengths(20)
      .flowLayout('x', 150)
      .size([width, height])
      .nodes(nodes)
      .links(edges)
      .constraints(constraints)
      .jaccardLinkLengths(300);

Is it possible to configure colajs in a way that makes it look more structured, similar to dagre? And is dagre simply not able to solve something like this, or is it possible to configure it in a way it is?

Whinchat answered 10/7, 2015 at 4:53 Comment(11)
can you illustrate (with an image perhaps), what the ideal rendering would be?Yang
@adarren sorry, I wrote it in the text but then forgot to edit the picture: if the two marked nodes were swapped, all crossings would be resolved.Whinchat
thanks for updating your image. I think dagre should be able to handle your scenario, and think it may be related to your data.. are you able to create a jsbin / jsfiddle / etc for your issue?Yang
It sounds like cola.js is more suited to what you need.Nerveless
@LarsKotthoff but doesn't dagre advertise similar features? Is cola.js better for layouting in general, or is it just better at reducing edge crossings?Whinchat
@adarren Sorry, I'm not at work right now so the soonest I can give you example code is on monday. Could you perhaps give some information on mistakes I could have made relating to the data?Whinchat
@Whinchat Dagre uses D3 for its rendering, i.e. it doesn't even have a concept of edge crossings. Cola allows you to take into account what you care about.Nerveless
@LarsKotthoff Maybe you are talking about a different version of dagre, but in the one I'm using (dagre-d3 in browser), it definitively is aware of edges and also uses its own algorithm to reduce edge crossings.Whinchat
Ok, as far as I know github.com/cpettitt/dagre-d3 does the layout entirely in D3, which doesn't consider edge crossings.Nerveless
@LarsKotthoff Altough I am sure that dagre-d3 also handles layout (I debugged some of its code for edge crossing reduction to see why it wasn't doing it's job on this diagram), it seems that the results vary a lot when the edges are set in a different order (see the two examples).Whinchat
@adarren I added two examples, but wasn't able to reproduce the excact diagram as in the screenshot, but the core problem stays the same.Whinchat
D
10

Here is one solution to the problem: http://jsfiddle.net/5u9mzfse/

More or less I was just interested of this actual problem of determining the optimal rendering, how to achieve that algorithmically.

The idea is to make use of the fact that the order of the rendered nodes matter, so you could shuffle the order and find the order which creates best results. The easiest way to do that is to test if the bouning boxes of the lines which the edges form do collide. Here I assume that the edges start and end is good enough estimate for the bounding box.

The edges should be first saved into list

var edgeList = [["10007154_1100","148570017_1100",{"label":""}, ...]

Then

  1. Shuffle the list
  2. Render nodes
  3. Calculate the bounding boxes of the edges from the output
  4. Calculate how many bounding boxes overlap
  5. If the collision count is zero render the output, otherwise continue until max_cnt iterations have been run and select the best so far

The bounding boxes of the edges from the rendered output can be found using something like this:

  var nn = svg.select(".edgePaths");
  var paths = nn[0][0];
  var fc = paths.firstChild;
  var boxes = [];
  while(fc) {
     var path = fc.firstChild.getAttribute("d");
     var coords = path.split(/,|L/).map(function(c) {
         var n = c;
         if((c[0]=="M" || c[0]=="L")) n = c.substring(1);
         return parseFloat(n);
     })
     boxes.push({ left : coords[0], top : coords[1], 
            right : coords[coords.length-2], 
            bottom : coords[coords.length-1]});
     fc = fc.nextSibling;
  }

The you just calculate if the boxes collide, I figured something like this give approximately correct results:

  var collisionCnt = 0;
  boxes.forEach( function(a) {
         // --> test for collisions against other nodes...
         boxes.forEach(function(b) {
             if(a==b) return;
             // test if outside
             if ( (a.right  < b.left) || 
                  (a.left   > b.right) || 
                  (a.top    > b.bottom) || 
                  (a.bottom < b.top) ) {

                  // test if inside
                  if(a.left >= b.left  && a.left <=b.right 
                  || a.right >= b.left  && a.right <=b.right) {
                     if(a.top <= b.top && a.top >= b.bottom) {
                        collisionCnt++;
                     }
                     if(a.bottom <= b.top && a.bottom >= b.bottom) {
                        collisionCnt++;
                     }                 
                  }
             } else {
                collisionCnt++;
             }
         })
      })

Then you know how many edges are crossing each other with this set of edges.

Then after each round check that if this is the best array we have so far, or if there are no collisions exit immediately;

if(collisionCnt==0) {
     optimalArray = list.slice();
     console.log("Iteration cnt ", iter_cnt);
     break; 
 } 
 if(typeof(best_result) == "undefined") {
    best_result = collisionCnt;
 } else {
    if(collisionCnt < best_result) {
        optimalArray = list.slice();
        best_result = collisionCnt;
    }
 }

During testing at least with a simple graph the algorithm required 1-5 rounds to calculate the optimal order of edges so it looks like it might work quite well at least if the diagram is not too large.

Dictatorship answered 20/3, 2016 at 11:32 Comment(3)
Thank you for your effort! It's a shame that dagre can't do this on its own.Whinchat
Yes, if one would look through the source code there might be a place to add this kind of check before rendering phase and create option for itDictatorship
Came back to this answer and realized there could be a problem if the bounding box (left,top) - (right,bottom) coordinates are defined so that left > right or bottom < top so there could be a check for left < right and top < bottom and swap values if necessary. At least that part of the algorithm should be double checked.Dictatorship

© 2022 - 2024 — McMap. All rights reserved.