nx_agraph.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. """
  2. ***************
  3. Graphviz AGraph
  4. ***************
  5. Interface to pygraphviz AGraph class.
  6. Examples
  7. --------
  8. >>> G = nx.complete_graph(5)
  9. >>> A = nx.nx_agraph.to_agraph(G)
  10. >>> H = nx.nx_agraph.from_agraph(A)
  11. See Also
  12. --------
  13. - Pygraphviz: http://pygraphviz.github.io/
  14. - Graphviz: https://www.graphviz.org
  15. - DOT Language: http://www.graphviz.org/doc/info/lang.html
  16. """
  17. import os
  18. import tempfile
  19. import networkx as nx
  20. __all__ = [
  21. "from_agraph",
  22. "to_agraph",
  23. "write_dot",
  24. "read_dot",
  25. "graphviz_layout",
  26. "pygraphviz_layout",
  27. "view_pygraphviz",
  28. ]
  29. def from_agraph(A, create_using=None):
  30. """Returns a NetworkX Graph or DiGraph from a PyGraphviz graph.
  31. Parameters
  32. ----------
  33. A : PyGraphviz AGraph
  34. A graph created with PyGraphviz
  35. create_using : NetworkX graph constructor, optional (default=None)
  36. Graph type to create. If graph instance, then cleared before populated.
  37. If `None`, then the appropriate Graph type is inferred from `A`.
  38. Examples
  39. --------
  40. >>> K5 = nx.complete_graph(5)
  41. >>> A = nx.nx_agraph.to_agraph(K5)
  42. >>> G = nx.nx_agraph.from_agraph(A)
  43. Notes
  44. -----
  45. The Graph G will have a dictionary G.graph_attr containing
  46. the default graphviz attributes for graphs, nodes and edges.
  47. Default node attributes will be in the dictionary G.node_attr
  48. which is keyed by node.
  49. Edge attributes will be returned as edge data in G. With
  50. edge_attr=False the edge data will be the Graphviz edge weight
  51. attribute or the value 1 if no edge weight attribute is found.
  52. """
  53. if create_using is None:
  54. if A.is_directed():
  55. if A.is_strict():
  56. create_using = nx.DiGraph
  57. else:
  58. create_using = nx.MultiDiGraph
  59. else:
  60. if A.is_strict():
  61. create_using = nx.Graph
  62. else:
  63. create_using = nx.MultiGraph
  64. # assign defaults
  65. N = nx.empty_graph(0, create_using)
  66. if A.name is not None:
  67. N.name = A.name
  68. # add graph attributes
  69. N.graph.update(A.graph_attr)
  70. # add nodes, attributes to N.node_attr
  71. for n in A.nodes():
  72. str_attr = {str(k): v for k, v in n.attr.items()}
  73. N.add_node(str(n), **str_attr)
  74. # add edges, assign edge data as dictionary of attributes
  75. for e in A.edges():
  76. u, v = str(e[0]), str(e[1])
  77. attr = dict(e.attr)
  78. str_attr = {str(k): v for k, v in attr.items()}
  79. if not N.is_multigraph():
  80. if e.name is not None:
  81. str_attr["key"] = e.name
  82. N.add_edge(u, v, **str_attr)
  83. else:
  84. N.add_edge(u, v, key=e.name, **str_attr)
  85. # add default attributes for graph, nodes, and edges
  86. # hang them on N.graph_attr
  87. N.graph["graph"] = dict(A.graph_attr)
  88. N.graph["node"] = dict(A.node_attr)
  89. N.graph["edge"] = dict(A.edge_attr)
  90. return N
  91. def to_agraph(N):
  92. """Returns a pygraphviz graph from a NetworkX graph N.
  93. Parameters
  94. ----------
  95. N : NetworkX graph
  96. A graph created with NetworkX
  97. Examples
  98. --------
  99. >>> K5 = nx.complete_graph(5)
  100. >>> A = nx.nx_agraph.to_agraph(K5)
  101. Notes
  102. -----
  103. If N has an dict N.graph_attr an attempt will be made first
  104. to copy properties attached to the graph (see from_agraph)
  105. and then updated with the calling arguments if any.
  106. """
  107. try:
  108. import pygraphviz
  109. except ImportError as err:
  110. raise ImportError(
  111. "requires pygraphviz " "http://pygraphviz.github.io/"
  112. ) from err
  113. directed = N.is_directed()
  114. strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
  115. A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed)
  116. # default graph attributes
  117. A.graph_attr.update(N.graph.get("graph", {}))
  118. A.node_attr.update(N.graph.get("node", {}))
  119. A.edge_attr.update(N.graph.get("edge", {}))
  120. A.graph_attr.update(
  121. (k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge")
  122. )
  123. # add nodes
  124. for n, nodedata in N.nodes(data=True):
  125. A.add_node(n)
  126. # Add node data
  127. a = A.get_node(n)
  128. a.attr.update({k: str(v) for k, v in nodedata.items()})
  129. # loop over edges
  130. if N.is_multigraph():
  131. for u, v, key, edgedata in N.edges(data=True, keys=True):
  132. str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"}
  133. A.add_edge(u, v, key=str(key))
  134. # Add edge data
  135. a = A.get_edge(u, v)
  136. a.attr.update(str_edgedata)
  137. else:
  138. for u, v, edgedata in N.edges(data=True):
  139. str_edgedata = {k: str(v) for k, v in edgedata.items()}
  140. A.add_edge(u, v)
  141. # Add edge data
  142. a = A.get_edge(u, v)
  143. a.attr.update(str_edgedata)
  144. return A
  145. def write_dot(G, path):
  146. """Write NetworkX graph G to Graphviz dot format on path.
  147. Parameters
  148. ----------
  149. G : graph
  150. A networkx graph
  151. path : filename
  152. Filename or file handle to write
  153. Notes
  154. -----
  155. To use a specific graph layout, call ``A.layout`` prior to `write_dot`.
  156. Note that some graphviz layouts are not guaranteed to be deterministic,
  157. see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
  158. """
  159. A = to_agraph(G)
  160. A.write(path)
  161. A.clear()
  162. return
  163. def read_dot(path):
  164. """Returns a NetworkX graph from a dot file on path.
  165. Parameters
  166. ----------
  167. path : file or string
  168. File name or file handle to read.
  169. """
  170. try:
  171. import pygraphviz
  172. except ImportError as err:
  173. raise ImportError(
  174. "read_dot() requires pygraphviz " "http://pygraphviz.github.io/"
  175. ) from err
  176. A = pygraphviz.AGraph(file=path)
  177. gr = from_agraph(A)
  178. A.clear()
  179. return gr
  180. def graphviz_layout(G, prog="neato", root=None, args=""):
  181. """Create node positions for G using Graphviz.
  182. Parameters
  183. ----------
  184. G : NetworkX graph
  185. A graph created with NetworkX
  186. prog : string
  187. Name of Graphviz layout program
  188. root : string, optional
  189. Root node for twopi layout
  190. args : string, optional
  191. Extra arguments to Graphviz layout program
  192. Returns
  193. -------
  194. Dictionary of x, y, positions keyed by node.
  195. Examples
  196. --------
  197. >>> G = nx.petersen_graph()
  198. >>> pos = nx.nx_agraph.graphviz_layout(G)
  199. >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
  200. Notes
  201. -----
  202. This is a wrapper for pygraphviz_layout.
  203. Note that some graphviz layouts are not guaranteed to be deterministic,
  204. see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
  205. """
  206. return pygraphviz_layout(G, prog=prog, root=root, args=args)
  207. def pygraphviz_layout(G, prog="neato", root=None, args=""):
  208. """Create node positions for G using Graphviz.
  209. Parameters
  210. ----------
  211. G : NetworkX graph
  212. A graph created with NetworkX
  213. prog : string
  214. Name of Graphviz layout program
  215. root : string, optional
  216. Root node for twopi layout
  217. args : string, optional
  218. Extra arguments to Graphviz layout program
  219. Returns
  220. -------
  221. node_pos : dict
  222. Dictionary of x, y, positions keyed by node.
  223. Examples
  224. --------
  225. >>> G = nx.petersen_graph()
  226. >>> pos = nx.nx_agraph.graphviz_layout(G)
  227. >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
  228. Notes
  229. -----
  230. If you use complex node objects, they may have the same string
  231. representation and GraphViz could treat them as the same node.
  232. The layout may assign both nodes a single location. See Issue #1568
  233. If this occurs in your case, consider relabeling the nodes just
  234. for the layout computation using something similar to::
  235. >>> H = nx.convert_node_labels_to_integers(G, label_attribute="node_label")
  236. >>> H_layout = nx.nx_agraph.pygraphviz_layout(G, prog="dot")
  237. >>> G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
  238. Note that some graphviz layouts are not guaranteed to be deterministic,
  239. see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
  240. """
  241. try:
  242. import pygraphviz
  243. except ImportError as err:
  244. raise ImportError(
  245. "requires pygraphviz " "http://pygraphviz.github.io/"
  246. ) from err
  247. if root is not None:
  248. args += f"-Groot={root}"
  249. A = to_agraph(G)
  250. A.layout(prog=prog, args=args)
  251. node_pos = {}
  252. for n in G:
  253. node = pygraphviz.Node(A, n)
  254. try:
  255. xs = node.attr["pos"].split(",")
  256. node_pos[n] = tuple(float(x) for x in xs)
  257. except:
  258. print("no position for node", n)
  259. node_pos[n] = (0.0, 0.0)
  260. return node_pos
  261. @nx.utils.open_file(5, "w+b")
  262. def view_pygraphviz(
  263. G, edgelabel=None, prog="dot", args="", suffix="", path=None, show=True
  264. ):
  265. """Views the graph G using the specified layout algorithm.
  266. Parameters
  267. ----------
  268. G : NetworkX graph
  269. The machine to draw.
  270. edgelabel : str, callable, None
  271. If a string, then it specifies the edge attribute to be displayed
  272. on the edge labels. If a callable, then it is called for each
  273. edge and it should return the string to be displayed on the edges.
  274. The function signature of `edgelabel` should be edgelabel(data),
  275. where `data` is the edge attribute dictionary.
  276. prog : string
  277. Name of Graphviz layout program.
  278. args : str
  279. Additional arguments to pass to the Graphviz layout program.
  280. suffix : str
  281. If `filename` is None, we save to a temporary file. The value of
  282. `suffix` will appear at the tail end of the temporary filename.
  283. path : str, None
  284. The filename used to save the image. If None, save to a temporary
  285. file. File formats are the same as those from pygraphviz.agraph.draw.
  286. show : bool, default = True
  287. Whether to display the graph with :mod:`PIL.Image.show`,
  288. default is `True`. If `False`, the rendered graph is still available
  289. at `path`.
  290. Returns
  291. -------
  292. path : str
  293. The filename of the generated image.
  294. A : PyGraphviz graph
  295. The PyGraphviz graph instance used to generate the image.
  296. Notes
  297. -----
  298. If this function is called in succession too quickly, sometimes the
  299. image is not displayed. So you might consider time.sleep(.5) between
  300. calls if you experience problems.
  301. Note that some graphviz layouts are not guaranteed to be deterministic,
  302. see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
  303. """
  304. if not len(G):
  305. raise nx.NetworkXException("An empty graph cannot be drawn.")
  306. # If we are providing default values for graphviz, these must be set
  307. # before any nodes or edges are added to the PyGraphviz graph object.
  308. # The reason for this is that default values only affect incoming objects.
  309. # If you change the default values after the objects have been added,
  310. # then they inherit no value and are set only if explicitly set.
  311. # to_agraph() uses these values.
  312. attrs = ["edge", "node", "graph"]
  313. for attr in attrs:
  314. if attr not in G.graph:
  315. G.graph[attr] = {}
  316. # These are the default values.
  317. edge_attrs = {"fontsize": "10"}
  318. node_attrs = {
  319. "style": "filled",
  320. "fillcolor": "#0000FF40",
  321. "height": "0.75",
  322. "width": "0.75",
  323. "shape": "circle",
  324. }
  325. graph_attrs = {}
  326. def update_attrs(which, attrs):
  327. # Update graph attributes. Return list of those which were added.
  328. added = []
  329. for k, v in attrs.items():
  330. if k not in G.graph[which]:
  331. G.graph[which][k] = v
  332. added.append(k)
  333. def clean_attrs(which, added):
  334. # Remove added attributes
  335. for attr in added:
  336. del G.graph[which][attr]
  337. if not G.graph[which]:
  338. del G.graph[which]
  339. # Update all default values
  340. update_attrs("edge", edge_attrs)
  341. update_attrs("node", node_attrs)
  342. update_attrs("graph", graph_attrs)
  343. # Convert to agraph, so we inherit default values
  344. A = to_agraph(G)
  345. # Remove the default values we added to the original graph.
  346. clean_attrs("edge", edge_attrs)
  347. clean_attrs("node", node_attrs)
  348. clean_attrs("graph", graph_attrs)
  349. # If the user passed in an edgelabel, we update the labels for all edges.
  350. if edgelabel is not None:
  351. if not callable(edgelabel):
  352. def func(data):
  353. return "".join([" ", str(data[edgelabel]), " "])
  354. else:
  355. func = edgelabel
  356. # update all the edge labels
  357. if G.is_multigraph():
  358. for u, v, key, data in G.edges(keys=True, data=True):
  359. # PyGraphviz doesn't convert the key to a string. See #339
  360. edge = A.get_edge(u, v, str(key))
  361. edge.attr["label"] = str(func(data))
  362. else:
  363. for u, v, data in G.edges(data=True):
  364. edge = A.get_edge(u, v)
  365. edge.attr["label"] = str(func(data))
  366. if path is None:
  367. ext = "png"
  368. if suffix:
  369. suffix = f"_{suffix}.{ext}"
  370. else:
  371. suffix = f".{ext}"
  372. path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
  373. else:
  374. # Assume the decorator worked and it is a file-object.
  375. pass
  376. # Write graph to file
  377. A.draw(path=path, format=None, prog=prog, args=args)
  378. path.close()
  379. # Show graph in a new window (depends on platform configuration)
  380. if show:
  381. from PIL import Image
  382. Image.open(path.name).show()
  383. return path.name, A