nx_agraph.py 14 KB

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