2
votes

I'm trying to display "cylindrical" digraphs (in left-to-right format), so that they can be drawn on a cylinder. The graphs consist of successive columns of nodes. The rightmost column of nodes (depicted with dashed lines) are stand-ins for the leftmost column of nodes--so that the graph could be printed out, rolled into a cylinder, and with the dashed nodes aligned exactly "underneath" the first column's nodes, it shows a continuous (cylindrical) loop-graph.

I've had some success using "rank" to get the columnar node alignments, and "group" to suggest horizontal node alignment between 1st and last columns. (This Graphviz Dot vertical alignment of nodes was particularly helpful.)

The problem is, I don't want the first and last column's nodes all splayed out vertically (see pic); there should be no more vertical separation between 1st col's nodes (ditto for last col's nodes) than between nodes in any other node column. But the respective 1st/last col nodes must remain horizontally aligned. (I suppose it's trying to leave the horizontal invisible edges in node pairs 1->15 and 13->16 unobstructed by intermediary nodes; but I don't need them unobstructed. Nodes 1 and 13 (and 15 and 16) should have about the same vertical separation as between (say) nodes 5 and 14.)

TL;DR version

  • the graph "flows" left to right, in a sequence of "node columns" (fwiw all edges are pointing rightward, and always only go one step, from column j to column j+1)
  • the rightmost column needs to be a "duplicate" of the leftmost column (reflecting the cylindrical wraparound)--it should look like a copy-and-paste of the leftmost column, at the same vertical height
  • this has to work for more complicated graphs too (like this one; the leftmost and rightmost columns's nodes line up fairly well, but I'm concerned about outliers), and has to come from an automated process (no fine-tuning by hand)
digraph {

node[label=""];
rankdir="LR"
1->8[label="c"]
13->8[label="a"]
8->12[label="b"]
12->10[label="a"]
10->5[label="a"]
10->14[label="b"]
5->2[label="a"]
5->9[label="b"]
14->9[label="a"]
2->4[label="b"]
9->4[label="a"]

// "wraparound" nodes
node[label="", style="dashed"];
4->15[label="a"] // since "a" connected 4 to 1, 15 must be in the same group as 1 (subs for 1)
node[label="", style="dashed"];
4->16[label="c"]

{rank=same; 1[group=g1]; 13[group=g2]}
{rank=same; 8}
{rank=same; 12}
{rank=same; 10}
{rank=same; 5, 14}
{rank=same; 2, 9}
{rank=same; 4}

// new nodes also need to have same rank; groups should pair corresponding nodes, w/ correct edge letter
{rank=same; 15[group=g1]; 16[group=g2]}

edge[style=invis];
1->15
13->16

}
2

2 Answers

2
votes

To answer my own question, it's possible to run dot on a .dot/.gv file, and save the output as text (what includes lots of detailed info on node and edge positions, etc.). This can give a lot of control over node placement, while allowing a certain amount of automation--leaving it to string manipulation routines (I'm using Octave, which has enough functionality).

So, create a preliminary dot file. It will have all the node columns, including the right-hand "wraparound" node column (and it will align the rh column vertically however it wants).

digraph {
rankdir="LR"
1 [label="1"]
4 [label="2"]
10 [label="3"]
12 [label="1", style="dashed"]
13 [label="2", style="dashed"]
14 [label="3", style="dashed"]
node[label=""];
1->2[label="b"]
4->2[label="a"]
4->6[label="b"]
4->9[label="c"]
10->6[label="a"]
2->5[label="c"]
6->11[label="c"]
9->5[label="a"]
9->11[label="b"]
5->7[label="b"]
11->7[label="a"]
7->3[label="a"]
7->8[label="b"]
3->12[label="a"]
3->13[label="b"]
8->13[label="a"]
8->14[label="b"]
{rank=same; 1, 4, 10}
{rank=same; 2, 6, 9}
{rank=same; 5, 11}
{rank=same; 7}
{rank=same; 3, 8}
{rank=same; 12, 13, 14}
}

Preliminary result: pic of preliminary result

Compile it with the dot -Tdot option, which outputs a text file with the node positions (x,y) coordinates listed (in pos="<x>,<y>" format). This annotated file, with dot's optimized position data, is then scraped for for the node positions. Then adjust the positions of the last column's nodes so their heights match their complementary nodes in the first column. All nodes are then recorded in a new dot file with their positions forced (using <node#>[pos="<x>,<y>!"] syntax).

// add this to file
inputscale=72 // this tells neato to use the proper scaling; lower=nodes farther apart
1[pos="27,170!", label="1"];
4[pos="27,94!", label="2"];
10[pos="27,18!", label="3"];
node[label=""];
2[pos="127,151!"];
6[pos="127,37!"];
9[pos="127,94!"];
5[pos="227,121!"];
11[pos="227,67!"];
7[pos="327,94!"];
3[pos="427,121!"];
8[pos="427,67!"];
12[pos="527,170!", label="1", style="dashed"] // aligned with node ID 1
13[pos="527,94!", label="2", style="dashed"] // aligned with node ID 4
14[pos="527,18!", label="3", style="dashed"] // aligned with node ID 10

The result is compiled with neato (it respects pos="<x>,<y>!" in the dot file; AFAICT plain "dot" does not). (Neato is included, I think in general, in the graphviz package.) Neato likes to draw straight edges, so it helps to have the splines option on:

dot -Kneato -Gsplines=true -Gsep=.3 -Tps in_dot_file -o out_graph.ps

Pic of final, fully aligned result: pic of final, fully aligned result

Neato however may put edge symbols on top of edge lines. The most tedious part is probably scraping the pos= data from the intermediary annotated dot file. Maybe there's a better way to do that.

1
votes

(All your node[label=""]; statements are redundant)
If you remove your group attributes, it produces very nearly what you want, but the dashed nodes don't quite line up.
Here is different take on your graph:

digraph {
  node[label=""];
  {
    rank=same
    1 8 12 10 14 9 4 15 [style=dashed]
  }
  {
    rank=same
    13 5 2 16 [style=dashed]
  }

1->8[label="c"]
13->8[label="a"]
8->12[label="b"]
12->10[label="a"]
10->5[label="a"]
10->14[label="b"]
5->2[label="a"]
5->9[label="b"]
14->9[label="a"]
2->4[label="b"]
9->4[label="a"]

// "wraparound" nodes
node[style="dashed"];
4->15[label="a"] 
node[style="dashed"];
4->16[label="c"]

edge[style=invis weight=90];
1 ->13 
15->16
}

Giving: enter image description here