Search code examples
nodesgraphvizrank

Graphviz: align nodes on left side instead of centering (With answer)


I made a python script to plot a directory structure using graphviz. In order to render the graph correctly, I use rankdir=LR. However, nodes having the same rank are centered.

See the following image.

Graph result

As you can see, nodes like ".gitignore", ".travis.yml", etc don't have the same width because of their label. I would like to have nodes in the same rank aligned on the left side no matter their width.

I have seen other related posts like this one but I don't want to force nodes width.

Is there any other way to do it?

Thanks in advance for your answers and have a nice day.

P.S.: Here's a sample of my graph code:

strict digraph projetcStructure { 
    graph [overlap=false, splines=ortho, ranksep=0.05]
    edge[arrowhead=none, color=black]
    node[fontname="DejaVu Sans Mono", fontsize=14]
    "e0ec0f89f0745e25b725768b304be71e" [shape=folder, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>TechnicalReport</TD></TR></TABLE>>, color=2]
    "77a7da2a6f864418dd23f49f9c5e9804" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/git-merge.png" SCALE="TRUE"/></TD><TD>.gitignore</TD></TR></TABLE>>, color=1]
    "373d5e485b88f76e79d5bab15a88759e" [shape="point", width=0, height=0]
    "8e58ce33bdaaa1db79bab7080019c6fe" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/travis.png" SCALE="TRUE"/></TD><TD>.travis.yml</TD></TR></TABLE>>, color=1]
    "b56aa426f8ad0d8a7704c909f2473b70" [shape="point", width=0, height=0]
    "98a5f187b3527ca7f00643e7907b3453" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/book.png" SCALE="TRUE"/></TD><TD>LICENSE</TD></TR></TABLE>>, color=1]
    "07adcea039d69fb282b22eebd8daf537" [shape="point", width=0, height=0]
    "ede2c50901cc59eeb8987d630d055b22" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/terminal.png" SCALE="TRUE"/></TD><TD>makeScript.sh</TD></TR></TABLE>>, color=1]
    "8f182d8be99f90432a2bec25a864bf19" [shape="point", width=0, height=0]
    "53ccb88dccecc95c401d180aa34ac0ac" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/note.png" SCALE="TRUE"/></TD><TD>README.rst</TD></TR></TABLE>>, color=1]
    "f6ffc62db5cefc5b4a1eb2e6a3731802" [shape="point", width=0, height=0]
    "3f917d2353bb6c1c156b18a78f23cf71" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file.png" SCALE="TRUE"/></TD><TD>requirements.txt</TD></TR></TABLE>>, color=1]
    "acc1004c581f3b603d2a33092ffcfe61" [shape="point", width=0, height=0]
    "2d05acf91c352a7ac491e18dcf65fc9c" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/settings.png" SCALE="TRUE"/></TD><TD>setup.cfg</TD></TR></TABLE>>, color=1]
    "8e1760a00144fca5272f341da0873674" [shape="point", width=0, height=0]
    "b78eb5bb77cb04f62506859fb1ab76f8" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/terminal.png" SCALE="TRUE"/></TD><TD>test.sh</TD></TR></TABLE>>, color=1]
    "cc60f62b1b3d0cf8c266074b7139b567" [shape="point", width=0, height=0]
    "3512568f6984d40e6353aa13b5029dc3" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/mark-github.png" SCALE="TRUE"/></TD><TD>.github</TD></TR></TABLE>>, color=1]
    "88ae0eb0f89e79ddff8e2b1fe470a488" [shape="point", width=0, height=0]
    "9b43ae74e93bdb3dadace6084e333a27" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>foldertree</TD></TR></TABLE>>, color=1]
    "2f9f172522c65ab785d2c23db909b36a" [shape="point", width=0, height=0]
    "e7925cdfdefd09112b8b3c8b5ddde087" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>generated</TD></TR></TABLE>>, color=1]
    "0b54fbb48c26181df9c8a5fd3bab1049" [shape="point", width=0, height=0]
    "69d5f4cf4e091e008cb1f4de43692b4d" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>TechnicalReportGenerator</TD></TR></TABLE>>, color=1]
    "c4f859f4d369226348a4eb08adab0afc" [shape="point", width=0, height=0]
    "0ab55395ce4ef62f7a9f5f72201ce0ae" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>worklog</TD></TR></TABLE>>, color=1]
    "9b4f2a57b302961cd3509dd307121cfa" [shape="point", width=0, height=0]
    "77a7da2a6f864418dd23f49f9c5e9804" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/git-merge.png" SCALE="TRUE"/></TD><TD>.gitignore</TD></TR></TABLE>>, color=1]
    "8e58ce33bdaaa1db79bab7080019c6fe" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/travis.png" SCALE="TRUE"/></TD><TD>.travis.yml</TD></TR></TABLE>>, color=1]
    "98a5f187b3527ca7f00643e7907b3453" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/book.png" SCALE="TRUE"/></TD><TD>LICENSE</TD></TR></TABLE>>, color=1]
    "ede2c50901cc59eeb8987d630d055b22" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/terminal.png" SCALE="TRUE"/></TD><TD>makeScript.sh</TD></TR></TABLE>>, color=1]
    "53ccb88dccecc95c401d180aa34ac0ac" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/note.png" SCALE="TRUE"/></TD><TD>README.rst</TD></TR></TABLE>>, color=1]
    "3f917d2353bb6c1c156b18a78f23cf71" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file.png" SCALE="TRUE"/></TD><TD>requirements.txt</TD></TR></TABLE>>, color=1]
    "2d05acf91c352a7ac491e18dcf65fc9c" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/settings.png" SCALE="TRUE"/></TD><TD>setup.cfg</TD></TR></TABLE>>, color=1]
    "b78eb5bb77cb04f62506859fb1ab76f8" [shape=note, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/terminal.png" SCALE="TRUE"/></TD><TD>test.sh</TD></TR></TABLE>>, color=1]
    "3512568f6984d40e6353aa13b5029dc3" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/mark-github.png" SCALE="TRUE"/></TD><TD>.github</TD></TR></TABLE>>, color=1]
    "9b43ae74e93bdb3dadace6084e333a27" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>foldertree</TD></TR></TABLE>>, color=1]
    "e7925cdfdefd09112b8b3c8b5ddde087" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>generated</TD></TR></TABLE>>, color=1]
    "69d5f4cf4e091e008cb1f4de43692b4d" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>TechnicalReportGenerator</TD></TR></TABLE>>, color=1]
    "0ab55395ce4ef62f7a9f5f72201ce0ae" [shape=folder, style=filled, colorscheme=ylgnbu3, width=3.7555555555555555, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/foldertree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>worklog</TD></TR></TABLE>>, color=1]
    {rank=same;  "373d5e485b88f76e79d5bab15a88759e" "b56aa426f8ad0d8a7704c909f2473b70" "07adcea039d69fb282b22eebd8daf537" "8f182d8be99f90432a2bec25a864bf19" "f6ffc62db5cefc5b4a1eb2e6a3731802" "acc1004c581f3b603d2a33092ffcfe61" "8e1760a00144fca5272f341da0873674" "cc60f62b1b3d0cf8c266074b7139b567" "88ae0eb0f89e79ddff8e2b1fe470a488" "2f9f172522c65ab785d2c23db909b36a" "0b54fbb48c26181df9c8a5fd3bab1049" "c4f859f4d369226348a4eb08adab0afc" "9b4f2a57b302961cd3509dd307121cfa"}
"e0ec0f89f0745e25b725768b304be71e" -> "373d5e485b88f76e79d5bab15a88759e"
"373d5e485b88f76e79d5bab15a88759e" -> "b56aa426f8ad0d8a7704c909f2473b70"
"b56aa426f8ad0d8a7704c909f2473b70" -> "07adcea039d69fb282b22eebd8daf537"
"07adcea039d69fb282b22eebd8daf537" -> "8f182d8be99f90432a2bec25a864bf19"
"8f182d8be99f90432a2bec25a864bf19" -> "f6ffc62db5cefc5b4a1eb2e6a3731802"
"f6ffc62db5cefc5b4a1eb2e6a3731802" -> "acc1004c581f3b603d2a33092ffcfe61"
"acc1004c581f3b603d2a33092ffcfe61" -> "8e1760a00144fca5272f341da0873674"
"8e1760a00144fca5272f341da0873674" -> "cc60f62b1b3d0cf8c266074b7139b567"
"cc60f62b1b3d0cf8c266074b7139b567" -> "88ae0eb0f89e79ddff8e2b1fe470a488"
"88ae0eb0f89e79ddff8e2b1fe470a488" -> "2f9f172522c65ab785d2c23db909b36a"
"2f9f172522c65ab785d2c23db909b36a" -> "0b54fbb48c26181df9c8a5fd3bab1049"
"0b54fbb48c26181df9c8a5fd3bab1049" -> "c4f859f4d369226348a4eb08adab0afc"
"c4f859f4d369226348a4eb08adab0afc" -> "9b4f2a57b302961cd3509dd307121cfa"
    {rank=same;  "77a7da2a6f864418dd23f49f9c5e9804" "8e58ce33bdaaa1db79bab7080019c6fe" "98a5f187b3527ca7f00643e7907b3453" "ede2c50901cc59eeb8987d630d055b22" "53ccb88dccecc95c401d180aa34ac0ac" "3f917d2353bb6c1c156b18a78f23cf71" "2d05acf91c352a7ac491e18dcf65fc9c" "b78eb5bb77cb04f62506859fb1ab76f8" "3512568f6984d40e6353aa13b5029dc3" "9b43ae74e93bdb3dadace6084e333a27" "e7925cdfdefd09112b8b3c8b5ddde087" "69d5f4cf4e091e008cb1f4de43692b4d" "0ab55395ce4ef62f7a9f5f72201ce0ae"}
"373d5e485b88f76e79d5bab15a88759e" -> "77a7da2a6f864418dd23f49f9c5e9804"
"b56aa426f8ad0d8a7704c909f2473b70" -> "8e58ce33bdaaa1db79bab7080019c6fe"
"07adcea039d69fb282b22eebd8daf537" -> "98a5f187b3527ca7f00643e7907b3453"
"8f182d8be99f90432a2bec25a864bf19" -> "ede2c50901cc59eeb8987d630d055b22"
"f6ffc62db5cefc5b4a1eb2e6a3731802" -> "53ccb88dccecc95c401d180aa34ac0ac"
"acc1004c581f3b603d2a33092ffcfe61" -> "3f917d2353bb6c1c156b18a78f23cf71"
"8e1760a00144fca5272f341da0873674" -> "2d05acf91c352a7ac491e18dcf65fc9c"
"cc60f62b1b3d0cf8c266074b7139b567" -> "b78eb5bb77cb04f62506859fb1ab76f8"
"88ae0eb0f89e79ddff8e2b1fe470a488" -> "3512568f6984d40e6353aa13b5029dc3"
"2f9f172522c65ab785d2c23db909b36a" -> "9b43ae74e93bdb3dadace6084e333a27"
"0b54fbb48c26181df9c8a5fd3bab1049" -> "e7925cdfdefd09112b8b3c8b5ddde087"
"c4f859f4d369226348a4eb08adab0afc" -> "69d5f4cf4e091e008cb1f4de43692b4d"
"9b4f2a57b302961cd3509dd307121cfa" -> "0ab55395ce4ef62f7a9f5f72201ce0ae"

}

[EDIT]

After a few researches and thanks to @TomServo, I have been able to "fix" the nodes width.

I use the following code. ImageFont doc can be found here

font = ImageFont.truetype('DejaVuSansMono.ttf', FONT_SIZE)
width = font.getsize("YOUR STRING TO MEASURE HERE")[0]
# convert pixels to inches and add extra space for icons and margins
# icon size is defined as points which is a 72's of an inch
width = width / GRAPHVIZ_DPI + ((1 / 72) * 20 * 2)

Then the result:

Better graph


Solution

  • Sorry, but I don't think there is a way to achieve the effect you want without fixing the width of the nodes in a rank. You may want to reconsider though because making your nodes the same width really makes the graph much neater:

    strict digraph projetcStructure { 
        graph [overlap=false, splines=ortho, ranksep=0.05, rankdir=LR]
        edge[arrowhead=none, color=black]
        "2f173bfac231270e0418210e8a3552d2" [shape=folder, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/FolderTree/octicons/file-directory.png" SCALE="TRUE"/></TD><TD>TechnicalReport</TD></TR></TABLE>>, color=3, width=2.0]
        "6b9ec0d7104a108a26ba34f9672f35a5" [shape=note, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/FolderTree/octicons/git-merge.png" SCALE="TRUE"/></TD><TD>.gitignore</TD></TR></TABLE>>, color=2, width=2.0]
        "6a20aaea09ab2aac28e4b8f50f46d9e6" [shape="point", width=0, height=0]
        "ad9d05da3ebed612a90ab85a30d039d7" [shape=note, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/FolderTree/octicons/travis.png" SCALE="TRUE"/></TD><TD>.travis.yml</TD></TR></TABLE>>, color=2, width=2.0]
        "2bef36c3d9fd70fec1b1d395ec891948" [shape="point", width=0, height=0]
        "023bdc0f2f34c3d14a028c1190295079" [shape=note, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/FolderTree/octicons/book.png" SCALE="TRUE"/></TD><TD>LICENSE</TD></TR></TABLE>>, color=2, width=2.0]
        "1a7aeec41293922786ed460f863c197c" [shape="point", width=0, height=0]
        "418903823b3792736b96279643175a80" [shape=note, style=filled, colorscheme=ylgnbu3, label=<<TABLE><TR><TD WIDTH="20" HEIGHT="20" FIXEDSIZE="TRUE"><IMG SRC="/mnt/e/Documents/3eme/TechnicalReport/FolderTree/octicons/terminal.png" SCALE="TRUE"/></TD><TD>makeScript.sh</TD></TR></TABLE>>, color=2, width=2.0]
    

    Notice the width=2.0 at the end of the first few nodes. This makes them the same width and cleans up the ragged look a little.

    Also, do put rankdir=LR near the top as shown, and as you mentioned in your question.

    enter image description here

    To "precompute" the width necessary, on a Windows system I would use the Graphics namespace to determine width, something like this:

    private void MeasureStringWidth(PaintEventArgs e)
    {
    
        // Set up string.
        string measureString = "Measure String";
        Font stringFont = new Font("Arial", 16);
    
        // Set maximum width of string.
        int stringWidth = 200;
    
        // Measure string.
        SizeF stringSize = new SizeF();
        stringSize = e.Graphics.MeasureString(measureString, stringFont, stringWidth);
    
        // Draw rectangle representing size of string.
        e.Graphics.DrawRectangle(new Pen(Color.Red, 1), 0.0F, 0.0F, stringSize.Width, stringSize.Height);
    
        // Draw string to screen.
        e.Graphics.DrawString(measureString, stringFont, Brushes.Black, new PointF(0, 0));
    }
    

    Then convert the stringSize.Width value to inches for example. Add a little padding, add a little extra for your included icon graphics and then use that for your uniform width.

    Finally, if you're not on a Windows system, or not using .NET, there may be other ways to measure the width of a string, as in the example with python and tkinter. So don't give up, you can do it!