Dot aligns (justifies) all the nodes on the same rank to a common Y value (or common X value if rankdir=LR/RL)
For example the two nodes on the right side are on the same rank and share a common X value:
Is there a (built-in) way to left-justify the two nodes?
This is a great idea, but we did not implement this. I wonder how hard it would be - there might be some code that assumes node center points are on the midline of a rank, but I don’t think that’s baked in very deeply. Think the spline router already depends on a function that determines for each node, the box path from an edge port to the inter-rank space where the spline is going next, so this feature would fit in with that already.
- I have post-processing code that will allow justifying nodes within the confines of a rank, but I did not want to announce it until I was sure the capability did not already exist.
- post-processing is OK, but built-in would be better. I have created an issue (dot - allow nodes to be justified/aligned off of rank centerline (#2359) · Issues · graphviz / graphviz · GitLab) requesting this feature
Here is a gvpr (https://www.graphviz.org/pdf/gvpr.1.pdf) program that will (re)justify/align nodes within their given rank.
use it like this:
- create a dot file as normal
- add this new attribute to all nodes you want justified rankjustify= X where X can be t|b|l|r|c|max|min. Different nodes can have different rankjustify values.
- run dot, gvpr, and neato to produce the desired graph
dot -Tdot myfile.gv | gvpr -cf rankJustify.gvpr | neato -n -Tpng >myfile.png
Here is rankJustify.gvpr:
//
// justify nodes that are on the same rank
//
BEGIN {
int nxt=0, i, vert, rankPos[], OK[], checked[];
string RankDir;
float num, deltaX, deltaY, maxHeight[], maxWidth[];
graph_t aGraph, Root, Parent[];
node_t aNode;
///////////////////////////////////////////////////////////////////////////////
void nodeJustify(node_t aN){
float XX, YY;
int err;
if(checked[aN]==1) return;
checked[aN]=1;
if (! hasAttr(aN, "rankjustify") || aN.rankjustify=="") continue;
XX=aN.X;
YY=aN.Y;
deltaX=0.;
deltaY=0.;
if (vert==1){
deltaY=72.*(maxHeight[aN.Y]-(float)aN.height)/2.; // inches to points
}else{
deltaX=72.*(maxWidth[aN.X]-(float)aN.width)/2.; // inches to points
}
aN.oldPos=aN.pos;
switch(OK[aN.rankjustify]){
case "1":
break;
case "-1":
deltaX=-deltaX;
deltaY=-deltaY;
break;
case "0":
deltaX=0.;
deltaY=0.;
break;
default:
print("// Error:: node ",aN.name, " has invalid rankjustify value (", aN.rankjustify,")");
printf(2, "Error:: node %s, has invalid rankjustify value (%s)\n", aN.name, aN.rankjustify);
continue 2; // exit two levels
break;
}
aN.pos=sprintf("%.2f,%.2f", (aN.X + deltaX), (aN.Y + deltaY));
if (hasAttr(aN,"xlp") && aN.xlp!="") {
sscanf (aN.xlp, "%lf,%lf", &XX, &YY);
aN.xlp=sprintf("%.2f,%.2f", (XX + deltaX), (YY + deltaY));
}
}
///////////////////////////////////////////////////////////////////////////////
void checkaGraph(graph_t aGraph){
for (aNode=fstnode(aGraph);aNode;aNode = nxtnode_sg(aGraph, aNode)){
Parent[aNode]=aGraph;
// first pass through the nodes
// find max width/height for each rank/cluster (based on common Y or X)
if (RankDir=="TB|BT"){
rankPos[aNode.Y]=1;
if (maxHeight[aNode.Y]<aNode.height){
maxHeight[aNode.Y]=aNode.height;
}
} else { // LR or RL
rankPos[aNode.X]=1;
if (maxWidth[aNode.X]<aNode.width)
maxWidth[aNode.X]=aNode.width;
}
}
for (aNode=fstnode(aGraph);aNode;aNode = nxtnode_sg(aGraph, aNode)){
nodeJustify(aNode);
}
}
///////////////////////////////////////////////////////////////////////////////
graph_t graphTraverse(graph_t thisG){
for (aGraph = fstsubg(thisG); aGraph; aGraph = nxtsubg(aGraph)) {
if (match(aGraph.name,"cluster")==0 || (hasAttr(aGraph, "cluster") && aGraph.cluster=="true")){
unset(maxWidth);
unset(maxHeight);
checkaGraph(aGraph);
}
aGraph = graphTraverse(aGraph);
}
return thisG;
} // end of graphTraverse
}
BEG_G{
Root=$G;
if (hasAttr(Root, "layout")){
// other values (including "") cause problems with later execution
Root.layout="neato";
}
if (! hasAttr(Root, "splines") || Root.splines==""){
// dot defaults to true, neato defaults to false
Root.splines="true";
}
// determine acceptable justification values
if (! hasAttr($G, "rankdir") || $.rankdir==""){
RankDir="TB";
}else{
RankDir=$G.rankdir;
}
vert=1;
OK["c"] =0;
if (RankDir=="TB") {
OK["t"] =1;
OK["b"] =-1;
//OK["max"] =1;
//OK["min"] =-1;
OK["max"] =-1;
OK["min"] =1;
OK["l"] =1;
OK["r"] =-1;
} else if (RankDir=="BT"){
OK["t"] =1;
OK["b"] =-1;
//OK["max"] =-1;
//OK["min"] =1;
OK["max"] =1;
OK["min"] =-1;
// do we really want these next two?
OK["l"] =1;
OK["r"] =-1;
} else if (RankDir=="LR"){
vert=0;
OK["l"] =-1;
OK["r"] =1;
//OK["max"] =-1;
//OK["min"] =1;
OK["max"] =1;
OK["min"] =-1;
// do we really want these next two?
OK["t"] =-1;
OK["b"] =1;
} else if (RankDir=="RL"){
vert=0;
OK["l"] =-1;
OK["r"] =1;
//OK["max"] =1;
//OK["min"] =-1;
OK["max"] =-1;
OK["min"] =1;
}
// traverse the graph
// find all clusters
// within each cluster, find all nodes (at highest level
// find max & min Y (or X) within the set of nodes
// then revisit all of the nodes and adjust Y (or X) as desired
// (finally) for all nodes not visited
// find max & min Y (or X) within the set of nodes
// then revisit all of the nodes and adjust Y (or X) as desired
graphTraverse (Root);
unset(maxWidth);
unset(maxHeight);
checkaGraph(Root);
}
And an example of the result:
Hello Steve,
this looks like great work and might solve a problem I have. However, I can’t seem to reconstruct your result with your instructions. I must be doing something wrong. Would you mind posting the dot source for your example chart above?
Thanks!
Below is my input, HOWEVER I have not set rankjustify=t to any of the nodes, I used the command line to do that.
Did you set rankjustify?
f=orgJustify0.gv;T=png; F=`basename -s .gv $f`;dot -Nrankjustify=t $f | gvpr -cf justifyRanks.gvpr | neato -n -T$T >$F.$T
digraph G {
fixedwidth = true;
node [
shape="box",
style="rounded",
penwidth = 1,
width=2.0,
fontname = "Arial",
fontsize = 12
];
edge [
color="#142b30",
arrowhead="vee",
arrowsize=0.75,
penwidth = 2,
weight=1.0
];
A1 [ label = <
<TABLE BORDER="0" CELLSPACING="5">
<TR>
<TD><FONT POINT-SIZE="16">Top Level</FONT></TD>
</TR>
<TR>
<TD><FONT POINT-SIZE="18">Owner</FONT></TD>
</TR>
</TABLE>>
];
B3 [ label = <
<TABLE BORDER="0" CELLSPACING="5">
<TR>
<TD><FONT POINT-SIZE="12">Second Level<BR/>(1)</FONT></TD>
</TR>
<TR>
<TD><FONT POINT-SIZE="14">Owner</FONT></TD>
</TR>
</TABLE>>
];
B4 [ label = <
<TABLE BORDER="0" CELLSPACING="5">
<TR>
<TD><FONT POINT-SIZE="12">Second Level<BR/>(2)</FONT></TD>
</TR>
<TR>
<TD><FONT POINT-SIZE="14">Owner</FONT></TD>
</TR>
</TABLE>>
];
B5 [ label = <
<TABLE BORDER="0" CELLSPACING="5">
<TR>
<TD><FONT POINT-SIZE="12">Second Level<BR/>(3)</FONT></TD>
</TR>
<TR>
<TD><FONT POINT-SIZE="14">Owner</FONT></TD>
</TR>
</TABLE>>
];
B6 [ label = <
<TABLE BORDER="0" CELLSPACING="5">
<TR>
<TD><FONT POINT-SIZE="12">Second Level<BR/>(4)</FONT></TD>
</TR>
<TR>
<TD><FONT POINT-SIZE="14">Owner</FONT></TD>
</TR>
</TABLE>>
];
C4 [ label = <
<TABLE BORDER="0" CELLPADDING="0" ALIGN="LEFT">
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
</TABLE>>
];
C5 [ label = <
<TABLE BORDER="0" CELLPADDING="0" ALIGN="LEFT">
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
</TABLE>>
];
C6 [ label = <
<TABLE BORDER="0" CELLPADDING="0" ALIGN="LEFT">
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
</TABLE>>
];
C7 [ label = <
<TABLE BORDER="0" CELLPADDING="0" ALIGN="LEFT">
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
<TR>
<TD ALIGN="LEFT"><FONT POINT-SIZE="10">Full Name</FONT></TD>
</TR>
</TABLE>>
];
{ rank = same; B3; B4; B5; B6; }
A1 -> B3:n;
A1 -> B4:n;
A1 -> B5:n;
A1 -> B6:n;
{ rank = same; C4; C5; C6; C7; }
B3 -> C4;
B4 -> C5;
B5 -> C6;
B6 -> C7;
}