Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group nodes within a bounding container #25

Open
TomPoulton opened this issue Feb 13, 2014 · 36 comments
Open

Group nodes within a bounding container #25

TomPoulton opened this issue Feb 13, 2014 · 36 comments

Comments

@TomPoulton
Copy link

Apologies if this is already possible and I've just missed it, but I'm using dagre-d3 to draw our network diagrams (servers and load-balancers) within our provisioning tool so that we can spin up a new environment based on the resources in the diagram.

Dagre-d3 is awesome so far, I've been able to get all the functionality I needed without too much effort, the only thing that's missing is that I'd like to group my servers by subnet (or AWS availability zone, I haven't decided yet). I guess this is just a special case of bounding boxes around a group of nodes where the boxes don't overlap.

If this isn't already a feature that I've missed, is it something that I could do using d3 directly? Any pointers in the right direction would be great!

Thanks

@cpettitt
Copy link
Collaborator

To group the nodes you probably want clustering support (dagrejs/dagre#13). It turns out to be a fair amount of work to add this support - the ticket references some of the papers that describe how to add it. Unfortunately, I haven't had a lot of dedicated time to work on it.

@TomPoulton
Copy link
Author

Haha yeah I figured it might require a little bit of effort to get it to work! No worries, is there some way of faking it in the meantime, maybe by adding the nodes to the graph in order and then manually drawing a rect around them? It wouldn't do "collision detection" or anything fancy like that, but it would serve as a visual aid for now!?

@cpettitt
Copy link
Collaborator

I just re-added forster's constrained ordering algorithm, which gets your part of the way to your goal. With this ordering algorithm all nodes in a subgraph are grouped contiguously in each layer. Furthermore, the ordering of subgraphs are maintained across layers, so that you can't have subgraph A before subgraph B in layer 1 and then subgraph B before subgraph A in layer 2.

This doesn't get you all of the way though. In addition to the above you want all of the nodes to be grouped into a rectangular region across layers. This will likely not be available until I finish support for clusters.

@alienrobotwizard
Copy link

Any update here? I little background. I'm currently updating lipstick (https://github.com/Netflix/Lipstick) to work with dagre instead of relying on graphviz. Things are going great so far: (https://github.com/thedatachef/Lipstick/blob/dagre/lipstick-server/web-app/tossboss-graph-view/js/tossboss-graph-renderer.js) with one real issue. Grouping nodes for a map-reduce job inside a rectangle. If you look (https://github.com/thedatachef/Lipstick/blob/dagre/lipstick-server/web-app/tossboss-graph-view/js/tossboss-graph-renderer.js#L102-L155) you'll see that I add the rectangles manually after the graph layout using d3. Unfortunately, this only works some of the time. Occasionally dagre puts nodes from one subgraph in the same region as another making my rectangles nest.

Now, I'm using a CDigraph for the graph and Digraphs for the subgraphs. Is there something else I should be doing?

Here's some screen shots:

With graphviz (what I want, sans graphviz itself and the ugly paths...):
with-graphviz

And with dagre (what I've got so far):
with-dagre

Thanks!

@alienrobotwizard
Copy link

Here's a block that illustrates things simpler. http://bl.ocks.org/thedatachef/raw/9818190/

Just looking for a workaround or something. If necessary I can spend some time on the renderer code if I knew where to start. Thanks!

@cpettitt
Copy link
Collaborator

You're doing everything right, it's just that clustering support isn't done yet. I have a set of patches on a local branch that gets us closer to clustering support, but I haven't had time to work on them. The main issue seems to be that the position phase algorithm, which has been generally been problematic, doesn't seem to play well with clustering.

I'll take a look through the patches I have and if they aren't too embarrassing (they're proof-of-concept code) I'll clean them up and post them to a branch on github this weekend.

@alienrobotwizard
Copy link

That's good to hear. I've hacked together a workaround that (at least for this gnarly graph: http://bl.ocks.org/thedatachef/raw/9841692/) appears to work. The idea is that, when laying out nodes, group them by cluster. Then, in the post processing, select the clusters, get their bboxes, and run the layout again using just the clusters as nodes. Of course the details are the beast here: https://gist.github.com/thedatachef/9841692

I'm happy to work on the more general solution when you post your branch. Thanks!

@alienrobotwizard
Copy link

Ok, one last comment for now. Theres a problem in my hack related to the minLen variable that the dagre layout uses. That is, if I treat the clusters/groups as nodes and run the layout it sets the number of ranks for an edge to 1, which means the length of the intercluster edges is way too long. If I could set the minLen to a fractional value I think I could go home happy. Thoughts?

@cpettitt
Copy link
Collaborator

minLen itself has to be a whole number because it is used to set up ranks. You could change the rankSep for the inter-cluster edges, which would reduce their length. Take a look at https://github.com/cpettitt/dagre#configuring-the-layout for details.

@cpettitt
Copy link
Collaborator

I've pushed most of what I have for the clustering support to the "cluster" branch for dagre. It doesn't work as it is and there are at least three places that probably need attention:

  1. I suspect something was not right in the way the border segments were added because I have some hacky code that was working on that.
  2. I believe the positioning algorithm was not yielding straight lines for segments where it should. At some point I want to replace it with a slower but more reliable algorithm - the algorithm I'm using is known to have problems, some of which I've worked around already.
  3. IIRC, I don't use any heuristics right now to figure out if an edge belongs inside or outside of a cluster. I think it always gets placed outside of all clusters right now.

@cpettitt
Copy link
Collaborator

Actually, 1 above may have been me trying to hack around the positioning algorithm (2 above). I would suspect the position algorithm first.

@alienrobotwizard
Copy link

Interesting about minLen. In my hacky example rankSep seems to work fairly well only if I set it to a negative number. However, if I try that with the lipstick graph there's cluster overlap again. Ah well.

I'll take a look at the cluster branch. Not sure how helpful I'll actually be since I'll have to read up on the algorithms you're using (and graph layout in general...) so I'm not totally lost. However, this is the last bit preventing me from totally replacing graphviz in lipstick so it's worth the effort on my part. I'll post any real progress here.

@cpettitt
Copy link
Collaborator

Latest code on the clusters branch addresses the bug with addBorderSegment and fixes some issues in ordering for clusters. Two obvious issues remain:

  1. We really need to get edges in the right clusters. Right now dummy nodes for edges are always assigned to the root cluster, which means edges from one node to another in the same cluster will go out of the cluster and then back in. The Sander paper has 2 strategies for how to handle this.
  2. I think I am finally at the point where the position algorithm is borked. Fixing issue 1 may inherently fix this for a number of graphs.

The code on the clusters branch is prototype-grade and needs some doc + tests before I push it to master. This will probably happen after 1 is addressed.

@cpettitt
Copy link
Collaborator

It looks like the output of graphviz most closely corresponds with strategy 2 in Sander's paper. At a very high level, this seems to be the idea:

As usual, add dummy nodes for each rank r from rank(u) + 1 to rank(v) - 1. At each step we try to find the next closest subgraph s such that rank_min(s) <= r <= rank_max(s). We effectively do this search by moving up the nesting graph until we hit the lowest common ancestor of u and v and then move back down towards v. It is possible that multiple subgraphs along this path have overlapping ranks, which is why we need to keep track of where we are on the path from u to v.

I'll take a stab at this sometime next week as time permits. Likely this will be next Saturday.

@cpettitt
Copy link
Collaborator

cpettitt commented Apr 1, 2014

Take a look at the "cluster" branch. For the test cases I've thrown at it, it no longer shows any overlap. A few thoughts:

  1. It looks like graphviz actually uses the equivalent of Sander 1 (not 2 as I thought) for parenting segments. With the current ordered constraint algorithm edges that leave a cluster to enter another will take the longest route possible (from the outside of one of the clusters, instead of the inside).
  2. The cluster branch needs lots of cleanup, doc, test code, perf testing before it gets pulled into master.
  3. Subgraphs of subgraphs have a bug. Working on 2 (e.g. test code) should help knock this one out.

On the positive side, now that we have something that works through all of the phases it is possible to make incremental improvements.

@alienrobotwizard
Copy link

@cpettitt Appears to be working great for 'normal' graphs but still overlap for neurotic cases. See:

http://bl.ocks.org/thedatachef/raw/9915158/ (sane graph)
http://bl.ocks.org/thedatachef/raw/9915049/ (neurotic graph)

I'll try this with lipstick today on some of the more eccentric graphs. I could probably make some of these examples tests for dagre once I have some time to grok your tests. Thanks!

@cpettitt
Copy link
Collaborator

cpettitt commented Apr 1, 2014

@TheDataChef that would be great. I'd probably add those graphs in test/smoke. See https://github.com/cpettitt/dagre/blob/master/test/smoke/smoke-8.dot as an example. The only special consideration is that the nodes have to have some size associated with them since dagre doesn't do that.

To catch the overlapping issue, we could add a test to https://github.com/cpettitt/dagre/blob/master/test/smoke-test.js to check that no clusters bounding box overlaps another that is not part of a parent-child relationship. I don't recall if I have the code for setting up the bounding boxes in dagre or just in a local branch for dagre-d3 on my laptop. If its not in dagre, it needs to be added.

@alienrobotwizard
Copy link

Ok, tried with Lipstick using the cluster branch. Things are a bit...off but there's no overlap.
with-dagre-clusterbranch

Seems like too many dummy nodes are added there? Or maybe the endpoints for the intercluster edges should be at the top and bottom of their nodes but they're on the sides?

I'll add that as a graph to the smoke tests and make a pr so you can take a look at it.

@cpettitt
Copy link
Collaborator

cpettitt commented Apr 2, 2014

@TheDataChef I tried out the neurotic graph, but I'm not getting the same result. Here's what I got: http://bl.ocks.org/cpettitt/9925588/741d7687e1acb47d3779c3c2ab388dc024abbba1.

BTW, here's how I've been testing dagre with dagre-d3:

  1. Git clone dagre, and switch to cluster branch
  2. Git clone dagre-d3 at the same level
  3. In dagre-d3, do "npm install ../dagre && make"

This gives you a new dagre-d3 with the dagre that has cluster support.


I think that in at least some cases it makes sense to put the first (and last?) dummy node in the cluster. That should yield better results for your graph and others.

@cpettitt
Copy link
Collaborator

cpettitt commented Apr 2, 2014

Check this out (your smoke test graph): http://bl.ocks.org/cpettitt/raw/9927284/6cd3388cfa70c6108ccb4f365a5820cbfbe3deab/. You can zoom out and pan to see the whole picture. This is from the cluster3 branch. I suspect this is going to cause problems with nested subgraphs though. It also has problems if edges cross nodes at the same level (nothing to do with the most recent change though). I'm probably going to take a few days to think about how to address some of these problems(and maybe start some cleanup / test / doc work. Happy to answer questions in the meantime.

@alienrobotwizard
Copy link

Is there a local branch of dagre-d3 you're using that draws the clusters? I don't see from your link how the rectangles get there.

@cpettitt
Copy link
Collaborator

cpettitt commented Apr 2, 2014

Yes, sorry, I'll push it to dagre-d3 tonight.

@alienrobotwizard
Copy link

Sweet! I'll just keep using what I have for lipstick (wrapping clusters in a group during drawing and then postrendering the rectangles) since I have some additional code that goes into the label drawing already. I haven't tried every example graph I have yet, but the cluster3 branch has taken care of the ones I've looked at. Thanks!

@alienrobotwizard
Copy link

Trying this with a large lipstick graph and getting issues. The same graph renders fine with graphviz yet I get (using cluster3 branch of dagre, cluster branch of dagre-d3):

Uncaught Error: Graph already has node 'scope-4120' dagre-d3.js:3866

Here's the graph (https://gist.github.com/thedatachef/10426606). I've stripped all the metadata I could. Sorry for the size, if I knew what the issue was I'd replicate with a smaller one.

@cpettitt
Copy link
Collaborator

I'm able to repro and was able to compact this down quite a bit:

digraph {
    subgraph "scope-3865" { "681" }
    subgraph "scope-3896" { "687" }
    subgraph "scope-3900" { "688" }
    subgraph "scope-3916" { "689" }
    subgraph "scope-3925" { "690" }
    subgraph "scope-4142" { "557" }
    subgraph "scope-4205" { "586" }
    subgraph "scope-4239" { "596" }
    subgraph "scope-3971" { "350" "351" "395"
                            "406" "419" "422"
                            "447" "508" "696"
                            "697"              }
}

The error is for a different node (scope-3971), but the error is the same. I don't have a lot of time to look at this tonight, but should be able to give it a thorough investigation this weekend.

@alienrobotwizard
Copy link

Ok, I got that graph to "render" by adding a check for self loops (which may just be treating the symptom):

https://github.com/thedatachef/dagre/commit/7e728f98a5a06dfa73372cad6db42780ebb155ce

But dagre-d3 isn't drawing the cluster for scope-3971 properly. Just contains node 350. Makes me think it's a larger problem. That's probably all I can contribute for now without a deep dive into the code.

@cpettitt
Copy link
Collaborator

Unfortunately I spent most of the weekend sick, but I was able to look at the problem with the large graph. The reported issue is fixed in the cluster4 branch, but be warned - rendering that graph takes about a minute right now and there are some issues with cluster overlap. I'll work on both fronts over the next week.

@cpettitt
Copy link
Collaborator

I now have dagre master rendering "neurotic" graph successfully. It is also able to do this in about 15 seconds, versus the > 50 seconds it takes with the current released version of dagre. There are still some optimization opportunities to be had as well. Dagre-d3 needs to be updated to pick up the new versions of dagre and graphlib - they were massively rewritten for better performance and test coverage.

@alienrobotwizard
Copy link

Thanks! I've pulled down dagre (and graphlib) and tried to run make, but having issues with a clean build. First, with master branch of graphlib:

"TypeError: 'undefined' is not an object (evaluating 'require('graphlib').converter.json')"

If I just use npm's version of graphlib the tests don't pass:

TypeError: 'undefined' is not an object (evaluating 'inputGraph.graph')
at buildLayoutGraph (file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:1387)
at file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:1302
at notime (file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:3663)
at file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:1302
at notime (file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:3663)
at layout (file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:1305)
at Renderer (file:///Users/jacob/Programming/dagre-d3/build/dagre-d3.js:71)
at file:///Users/jacob/Programming/dagre-d3/test/Renderer-test.js:21
at callFn (file:///Users/jacob/Programming/dagre-d3/node_modules/mocha/mocha.js:4387)
at file:///Users/jacob/Programming/dagre-d3/node_modules/mocha/mocha.js:4380
at next (file:///Users/jacob/Programming/dagre-d3/node_modules/mocha/mocha.js:4680)
at file:///Users/jacob/Programming/dagre-d3/node_modules/mocha/mocha.js:4684
at timeslice (file:///Users/jacob/Programming/dagre-d3/node_modules/mocha/mocha.js:5904)

If it's non-trivial I can wait, otherwise any guidance on updating dagre-d3 to use the latest changes?

@cpettitt
Copy link
Collaborator

cpettitt commented Oct 1, 2014

Yeah, I need to update dagre-d3 to the new API - I'm going to try to knock that out in the next day or two. In the meantime, you can use the test console in dagre, if you'd like. You need to do a make build first. It requires dot as an input, but I converted your graph to dot.

@alienrobotwizard
Copy link

Ok, I finally got some time to try it out. For the most part everything I've looked at lays out great, even with multiple levels of nesting! However, I've got one that doesn't layout right:

digraph {

    subgraph job1 {"19" "15" "21" "26" "29"}
    subgraph job2 {"17" "14" "11" "25"}
    subgraph job3 {"18" "33" "34" "13" "3" "2" "10" "1"}
    subgraph job4 {"16" "20" "23" "24" "27" "30"}
    subgraph job5 {"12" "7" "9"}
    subgraph job6 {"22"}
    subgraph job7 {"28" "32" "31"}
    subgraph job8 {"6" "5" "4" "8"}

    33 -> 3
    19 -> 15
    17 -> 14
    18 -> 13
    15 -> 13
    34 -> 33
    16 -> 14
    13 -> 10
    14 -> 11
    11 -> 8
    12 -> 9
    21 -> 19
    20 -> 16
    22 -> 19
    23 -> 20
    24 -> 20
    25 -> 17
    26 -> 21
    27 -> 23
    28 -> 24
    29 -> 26
    3 -> 2
    2 -> 1
    10 ->34
    30 -> 27
    7 -> 5
    6 -> 5
    32 -> 31
    5 -> 4
    31 -> 28
    4 -> 3
    9 -> 7
    8 -> 6
}

Tested with latest (master branch) version of dagre.

@cpettitt
Copy link
Collaborator

Confirmed repro'd and the problem is coming out of the down-right alignment. Here's a variant with some simplification (17 should be in the same cluster as 14):

digraph {
align=DR
    subgraph job1 { "21"}
    subgraph job2 {"17" "14" }
    subgraph job3 {"18"  "13"}

    17 -> 21
    18 -> 13
    21 -> 13
}

@cpettitt
Copy link
Collaborator

cpettitt commented Nov 2, 2014

@TheDataChef I fixed the problem with that graph in v0.6.4. Getting more and more suspicious I'm going to need to use a different positioning algorithm (maybe graphviz's)... Let me know if you find any more broken graphs.

@alienrobotwizard
Copy link

Alright, of the 100 or so widely varied graphs I looked at there's only one that's still got cluster overlap, but I can't reproduce it using the dagre console:

digraph {
          subgraph A {"77" "34" "37" "40" "23" "27" "30" "15" "11" "20" "62" "44" "47"}
          subgraph B {"58" "57" "56" "55" "63" "61" "51" "52" "53" "54" "50"}
          subgraph C {"36"}
          subgraph D {"33" "66" "69" "29" "75"}
          subgraph E {"38" "42" "49" "46"}
          subgraph F {"41"}
          subgraph G {"67" "68" "25" "2" "5" "9" "8" "17" "16" "13" "12"}
          subgraph H {"22" "26" "3" "6" "71" "72" "73" "74" "19" "18" "14" "21" "10"}
          subgraph I {"24" "28"}
          subgraph J {"1" "7" "4" "76" "64" "65"}
          subgraph K {"31"}
          subgraph L {"70" "48" "45"}
          subgraph M {"59" "60"}
          subgraph N {"35" "39" "43" "32"}

          77->62
          35->32
          36->32
          33->29
          34->30
          39->35
          37->34
          38->35
          43->39
          42->38
          41->37
          40->37
          67->68
          67->75
          66->33
          69->33
          68->2
          22->19
          23->20
          24->20
          25->16
          26->73
          26->74
          27->23
          28->24
          29->26
          7->4
          30->27
          6->3
          5->67
          32->28
          4->64
          31->27
          70->40
          71->14
          9->5
          72->14
          8->5
          73->21
          74->22
          75->69
          76->66
          59->56
          58->54
          57->55
          56->54
          19->72
          55->53
          17->13
          18->71
          15->11
          16->12
          13->9
          14->10
          11->7
          12->8
          21->18
          20->15
          64->65
          64->76
          65->1
          62->47
          63->57
          60->59
          61->77
          61->63
          49->46
          48->45
          45->70
          44->40
          47->44
          46->42
          10->6
          51->48
          52->50
          53->51
          54->52
          50->61
}

However, when I render with html in the node labels there's overlap:

cluster-overlap

In the dot graph it's subgraphs A, B, and L. Possibly an issue with the foreignobject tag?

@cpettitt
Copy link
Collaborator

cpettitt commented Nov 3, 2014

The above layout is not right in the down-left alignment. Here's a slightly simplified and tweaked version still showing a problem:

digraph {
align=DL
          subgraph A {"77" "37" "40" "11" "20"}
          subgraph D {"33" "66" "29" "75"}
          subgraph H { "26" }
          subgraph I {"28"}
          subgraph J {"7" "76"}
          subgraph N {"32"}

          33->29
          40->37
          67->75
          28->20
          29->26
          32->28
          76->66
          11->76
          61->77
}

@cpettitt
Copy link
Collaborator

cpettitt commented Nov 3, 2014

Even simpler, more tweaked:

digraph {
align=DL
          subgraph A {"40"}
          subgraph H {"26"}
          subgraph J {"76"}

          40 -> 29
          40->76
          29->26
}

cpettitt added a commit that referenced this issue Dec 15, 2014
This version includes fixes to horizontal position. This fixes the
last failing graph in #25.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants