9

I have been working with pygraph on some project. I completed this example, it works fine.

Now, the problem is the following: the graph is drawn in a picture format (gif). What I need is to get the actual coordinates for each node for the graph layout shown on the gif image. How do I do this? I've been trying and trying, but couldn't find solution to this problem. I thought the the problem's solution would be somehow with manipulating one of the two following lines:

gv.layout(gvv,'dot')
gv.render(gvv,'png','europe.png')

Thanks in advance!

Belphegor
  • 4,456
  • 11
  • 34
  • 59
  • Could you elaborate on what you tried ? It is not clear what your problem is. Didn't you manage to get the position from the nodes attributes ? – Anne Dec 19 '12 at 15:54
  • I think it is pretty clear what my problem is - I want to extract the coordinates of the nodes that gv.render is drawing in the file 'europe.png'. I didn't try anything except for "print gv.layout" but that returns 'True'. I found the source of gv.layout but it only returns '_gv.layout' and I can't find this source. So, I don't know really where happens the assignment of the coordinates for each node. I tried 'print gr.nodes' and couldn't find the coordinates there. Any ideas how to extract the coordinates from the gv object? – Belphegor Dec 19 '12 at 18:39
  • Is there any news on this subject? I have asked a similar question specifically for the stack of networkx-pygraphviz-graphviz and have not yet gotten an answer. Belphegor, do you by chance have a solution by now? – Michael Jul 19 '17 at 06:26

7 Answers7

8

You can add the layout information into the graph with :

gv.render(gvv)

and then find out the position of a node getting its attribute pos :

n_france = gv.findnode(gvv, "France")
pos = gv.getv(n_france, "pos")

Then depending on what you want to do, you might need to convert dot coordinates into png image coordinates. You can get useful information from here :

http://www.graphviz.org/faq/#FaqCoordTransformation

It explains in great details the computation from the graph-units to image pixels.

Hope that this is what you are looking for.

Anne
  • 1,270
  • 6
  • 15
  • The link seems dead. I tried to find it on the new website (http://www.graphviz.org/documentation/ ) to edit the question but couldn't find it. You know where that page is ? – kebs May 13 '18 at 08:24
  • I have fixed the link in my answer. – Anne Jun 06 '18 at 06:33
2

I just found a similar solution that works perfectly for my needs

pos = nx.drawing.nx_agraph.graphviz_layout(G, prog='dot', args='-Grankdir=LR')

cheers!

domson
  • 205
  • 2
  • 8
2

Using pydotplus you can load in and parse a dot/gv file and interrogate the data structure pydotplus produces, but this internal representation seems not to initially possess all the node attributes, like pos, unless they were already in the file.
But you can also call .write_dot() to produce a much more verbose dot file version. If you parse this then the resulting data structure seems to have pos of all the nodes (and even pos for the splines)

Note: maybe best to index the nodes by name not by index because any text with square brackets after it in the verbose file will be parsed as a node, so the node list may have spurious extra elements.

In the following (slightly edited) experiment at the Spyder prompt I have a terse dot file interior.gv (that does not have pos for nodes) which I .graph_from_dot_file(), then .write_dot(). Then .graph_from_dot_file() again on the verbose generated file, and so find the pos as required.

import pydotplus as pdp

interior = pdp.graphviz.graph_from_dot_file('interior.gv')

interior.write_dot('interior2.dot')
Out[210]: True

interior2 = pdp.graphviz.graph_from_dot_file('interior2.dot')

interior2.get_nodes()[3].get_pos()
Out[214]: '"213.74,130"'
1

Networkx can do this:

import networkx as nx

def setup_europe():
    G = nx.Graph()

    G.add_edge("Portugal", "Spain")
    G.add_edge("Spain","France")
    G.add_edge("France","Belgium")
    G.add_edge("France","Germany")
    G.add_edge("France","Italy")
    G.add_edge("Belgium","Netherlands")
    G.add_edge("Germany","Belgium")
    G.add_edge("Germany","Netherlands")
    G.add_edge("England","Wales")
    G.add_edge("England","Scotland")
    G.add_edge("Scotland","Wales")
    G.add_edge("Switzerland","Austria")
    G.add_edge("Switzerland","Germany")
    G.add_edge("Switzerland","France")
    G.add_edge("Switzerland","Italy")
    G.add_edge("Austria","Germany")
    G.add_edge("Austria","Italy")
    G.add_edge("Austria","Czech Republic")
    G.add_edge("Austria","Slovakia")
    G.add_edge("Austria","Hungary")
    G.add_edge("Denmark","Germany")
    G.add_edge("Poland","Czech Republic")
    G.add_edge("Poland","Slovakia")
    G.add_edge("Poland","Germany")
    G.add_edge("Czech Republic","Slovakia")
    G.add_edge("Czech Republic","Germany")
    G.add_edge("Slovakia","Hungary")
    return G

G = setup_europe()

Write a dot file:

nx.write_dot(G, '/tmp/out.dot')

Compute the position of the nodes:

pos = nx.pygraphviz_layout(G, prog = 'dot')
print(pos)
# {'Netherlands': (713.86, 167.0), 'Italy': (473.86, 389.0), 'Czech Republic': (100.86, 241.0), 'Portugal': (879.86, 315.0), 'England': (1024.9, 241.0), 'Denmark': (568.86, 167.0), 'Poland': (100.86, 167.0), 'Scotland': (1024.9, 389.0), 'France': (571.86, 315.0), 'Belgium': (713.86, 19.0), 'Austria': (320.86, 167.0), 'Slovakia': (156.86, 315.0), 'Wales': (990.86, 315.0), 'Switzerland': (473.86, 241.0), 'Hungary': (294.86, 241.0), 'Germany': (465.86, 93.0), 'Spain': (879.86, 241.0)}

Render an png:

agraph = nx.to_agraph(G)
agraph.draw("/tmp/europe.png", format = 'png', prog = 'dot')

enter image description here

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thanks unutbu, this certainly solves my problem, though partially. I actually need the coordinates from the layout provided by gv, not by networkx. Thanks for the answer anyway, it also helped me a lot. Any ideas how to do this with gv? – Belphegor Dec 19 '12 at 10:55
  • @Kiril: Hm, sorry. I don't know. – unutbu Dec 19 '12 at 13:37
1

Using just pydot/dot you can do it by generating the SVG and then reading the position of the nodes from the SVG. It is a bit hacky, but works reliably enough

from xml.dom import minidom
import pydot
from io import BytesIO
def extract_node_positions(
    in_dot: pydot.Dot
) -> Dict[str, Tuple[str, float, float]]:
    """
    Extract the x,y positions from a pydot graph by rendering
    Args:
        in_dot: the graph to render

    Returns:
        a list of all the nodes

    Examples:
        >>> g = pydot.Dot()
        >>> g.add_node(pydot.Node("A"))
        >>> g.add_node(pydot.Node("B"))
        >>> g.add_node(pydot.Node("C"))
        >>> g.add_edge(pydot.Edge("A", "B"))
        >>> g.add_edge(pydot.Edge("B", "C"))
        >>> extract_node_positions(g)
        {'A': ('A', 27.0, -157.8), 'B': ('B', 27.0, -85.8), 'C': ('C', 27.0, -13.8)}
    """
    node_mapping = {f'node{i}': k.get_name() 
                    for i, k in enumerate(in_dot.get_nodes(), 1)}
    svg_io = BytesIO(in_dot.create_svg())
    doc = minidom.parse(svg_io)  # parseString also exists
    node_dict = {node_mapping[p.getAttribute('id')]: (c_text.firstChild.data,
                                        float(c_text.getAttribute('x')),
                                        float(c_text.getAttribute('y')))

                 for p in doc.getElementsByTagName("g") if "node" == p.getAttribute('class').lower()
                 for c_text in p.getElementsByTagName('text')
                 }
    doc.unlink()
    return node_dict
kmader
  • 1,319
  • 1
  • 10
  • 13
1

To directly access the results from the Graphviz rendering command (e.g. dot) as binary data string from within Python instead of writing to a file, use the pipe()-method of your Graph or Digraph object:

h = Graph('hello', format='svg')

h.edge('Hello', 'World')

print(h.pipe().decode('utf-8'))

Kai Wang
  • 3,303
  • 1
  • 31
  • 27
0

I struggled with this recently, and the answers here were some help but not quite there. The suggestions about networkx are on the right track. Networkx uses pygraphviz to interface with graphviz, and so you can instead use pygraphviz directly if you wish:

import pygraphviz as pgv
G = pgv.AGraph(strict=False,directed=True)
# add nodes and edges
G.add_edge(1,2)
G.add_edge(2,1)
# generate a layout--this creates `pos` attributes for both nodes and edges
G.layout(prog="dot") 
#change something about the graph that would change the layout, e.g.,
edge = G.get_edge("1", "2")
edge.attr["label"] = "test label"
# create the graph using the layout already generated; note, do not provide `prog`
G.draw("test.pdf")
# compare it to a newly generated layout
G.draw("test2.pdf", prog="dot")

The important part is to not provide prog to the draw command if you want to use the node and edge positions already generated by the layout command. I see now this is stated in the pygraphviz docstring for draw. BTW, it is does the same as prog="neato" with the -n2 argument. Looking at the sources, pygraphviz calls graphviz to generate the layout.