nx_pydot.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. """
  2. *****
  3. Pydot
  4. *****
  5. Import and export NetworkX graphs in Graphviz dot format using pydot.
  6. Either this module or nx_agraph can be used to interface with graphviz.
  7. Examples
  8. --------
  9. >>> G = nx.complete_graph(5)
  10. >>> PG = nx.nx_pydot.to_pydot(G)
  11. >>> H = nx.nx_pydot.from_pydot(PG)
  12. See Also
  13. --------
  14. - pydot: https://github.com/erocarrera/pydot
  15. - Graphviz: https://www.graphviz.org
  16. - DOT Language: http://www.graphviz.org/doc/info/lang.html
  17. """
  18. from locale import getpreferredencoding
  19. import networkx as nx
  20. from networkx.utils import open_file
  21. __all__ = [
  22. "write_dot",
  23. "read_dot",
  24. "graphviz_layout",
  25. "pydot_layout",
  26. "to_pydot",
  27. "from_pydot",
  28. ]
  29. @open_file(1, mode="w")
  30. def write_dot(G, path):
  31. """Write NetworkX graph G to Graphviz dot format on path.
  32. Parameters
  33. ----------
  34. G : NetworkX graph
  35. path : string or file
  36. Filename or file handle for data output.
  37. Filenames ending in .gz or .bz2 will be compressed.
  38. """
  39. P = to_pydot(G)
  40. path.write(P.to_string())
  41. return
  42. @open_file(0, mode="r")
  43. @nx._dispatchable(name="pydot_read_dot", graphs=None, returns_graph=True)
  44. def read_dot(path):
  45. """Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the
  46. dot file with the passed path.
  47. If this file contains multiple graphs, only the first such graph is
  48. returned. All graphs _except_ the first are silently ignored.
  49. Parameters
  50. ----------
  51. path : str or file
  52. Filename or file handle to read.
  53. Filenames ending in .gz or .bz2 will be decompressed.
  54. Returns
  55. -------
  56. G : MultiGraph or MultiDiGraph
  57. A :class:`MultiGraph` or :class:`MultiDiGraph`.
  58. Notes
  59. -----
  60. Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a
  61. :class:`MultiGraph`.
  62. """
  63. import pydot
  64. data = path.read()
  65. # List of one or more "pydot.Dot" instances deserialized from this file.
  66. P_list = pydot.graph_from_dot_data(data)
  67. # Convert only the first such instance into a NetworkX graph.
  68. return from_pydot(P_list[0])
  69. @nx._dispatchable(graphs=None, returns_graph=True)
  70. def from_pydot(P):
  71. """Returns a NetworkX graph from a Pydot graph.
  72. Parameters
  73. ----------
  74. P : Pydot graph
  75. A graph created with Pydot
  76. Returns
  77. -------
  78. G : NetworkX multigraph
  79. A MultiGraph or MultiDiGraph.
  80. Examples
  81. --------
  82. >>> K5 = nx.complete_graph(5)
  83. >>> A = nx.nx_pydot.to_pydot(K5)
  84. >>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph
  85. # make a Graph instead of MultiGraph
  86. >>> G = nx.Graph(nx.nx_pydot.from_pydot(A))
  87. """
  88. # NOTE: Pydot v3 expects a dummy argument whereas Pydot v4 doesn't
  89. # Remove the try-except when Pydot v4 becomes the minimum supported version
  90. try:
  91. strict = P.get_strict()
  92. except TypeError:
  93. strict = P.get_strict(None) # pydot bug: get_strict() shouldn't take argument
  94. multiedges = not strict
  95. if P.get_type() == "graph": # undirected
  96. if multiedges:
  97. N = nx.MultiGraph()
  98. else:
  99. N = nx.Graph()
  100. else:
  101. if multiedges:
  102. N = nx.MultiDiGraph()
  103. else:
  104. N = nx.DiGraph()
  105. # assign defaults
  106. name = P.get_name().strip('"')
  107. if name != "":
  108. N.name = name
  109. # add nodes, attributes to N.node_attr
  110. for p in P.get_node_list():
  111. n = p.get_name().strip('"')
  112. if n in ("node", "graph", "edge"):
  113. continue
  114. N.add_node(n, **p.get_attributes())
  115. # add edges
  116. for e in P.get_edge_list():
  117. u = e.get_source()
  118. v = e.get_destination()
  119. attr = e.get_attributes()
  120. s = []
  121. d = []
  122. if isinstance(u, str):
  123. s.append(u.strip('"'))
  124. else:
  125. for unodes in u["nodes"]:
  126. s.append(unodes.strip('"'))
  127. if isinstance(v, str):
  128. d.append(v.strip('"'))
  129. else:
  130. for vnodes in v["nodes"]:
  131. d.append(vnodes.strip('"'))
  132. for source_node in s:
  133. for destination_node in d:
  134. N.add_edge(source_node, destination_node, **attr)
  135. # add default attributes for graph, nodes, edges
  136. pattr = P.get_attributes()
  137. if pattr:
  138. N.graph["graph"] = pattr
  139. try:
  140. N.graph["node"] = P.get_node_defaults()[0]
  141. except (IndexError, TypeError):
  142. pass # N.graph['node']={}
  143. try:
  144. N.graph["edge"] = P.get_edge_defaults()[0]
  145. except (IndexError, TypeError):
  146. pass # N.graph['edge']={}
  147. return N
  148. def to_pydot(N):
  149. """Returns a pydot graph from a NetworkX graph N.
  150. Parameters
  151. ----------
  152. N : NetworkX graph
  153. A graph created with NetworkX
  154. Examples
  155. --------
  156. >>> K5 = nx.complete_graph(5)
  157. >>> P = nx.nx_pydot.to_pydot(K5)
  158. Notes
  159. -----
  160. """
  161. import pydot
  162. # set Graphviz graph type
  163. if N.is_directed():
  164. graph_type = "digraph"
  165. else:
  166. graph_type = "graph"
  167. strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
  168. name = N.name
  169. graph_defaults = N.graph.get("graph", {})
  170. if name == "":
  171. P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults)
  172. else:
  173. P = pydot.Dot(
  174. f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults
  175. )
  176. try:
  177. P.set_node_defaults(**N.graph["node"])
  178. except KeyError:
  179. pass
  180. try:
  181. P.set_edge_defaults(**N.graph["edge"])
  182. except KeyError:
  183. pass
  184. for n, nodedata in N.nodes(data=True):
  185. str_nodedata = {str(k): str(v) for k, v in nodedata.items()}
  186. n = str(n)
  187. p = pydot.Node(n, **str_nodedata)
  188. P.add_node(p)
  189. if N.is_multigraph():
  190. for u, v, key, edgedata in N.edges(data=True, keys=True):
  191. str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"}
  192. u, v = str(u), str(v)
  193. edge = pydot.Edge(u, v, key=str(key), **str_edgedata)
  194. P.add_edge(edge)
  195. else:
  196. for u, v, edgedata in N.edges(data=True):
  197. str_edgedata = {str(k): str(v) for k, v in edgedata.items()}
  198. u, v = str(u), str(v)
  199. edge = pydot.Edge(u, v, **str_edgedata)
  200. P.add_edge(edge)
  201. return P
  202. def graphviz_layout(G, prog="neato", root=None):
  203. """Create node positions using Pydot and Graphviz.
  204. Returns a dictionary of positions keyed by node.
  205. Parameters
  206. ----------
  207. G : NetworkX Graph
  208. The graph for which the layout is computed.
  209. prog : string (default: 'neato')
  210. The name of the GraphViz program to use for layout.
  211. Options depend on GraphViz version but may include:
  212. 'dot', 'twopi', 'fdp', 'sfdp', 'circo'
  213. root : Node from G or None (default: None)
  214. The node of G from which to start some layout algorithms.
  215. Returns
  216. -------
  217. Dictionary of (x, y) positions keyed by node.
  218. Examples
  219. --------
  220. >>> G = nx.complete_graph(4)
  221. >>> pos = nx.nx_pydot.graphviz_layout(G)
  222. >>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot")
  223. Notes
  224. -----
  225. This is a wrapper for pydot_layout.
  226. """
  227. return pydot_layout(G=G, prog=prog, root=root)
  228. def pydot_layout(G, prog="neato", root=None):
  229. """Create node positions using :mod:`pydot` and Graphviz.
  230. Parameters
  231. ----------
  232. G : Graph
  233. NetworkX graph to be laid out.
  234. prog : string (default: 'neato')
  235. Name of the GraphViz command to use for layout.
  236. Options depend on GraphViz version but may include:
  237. 'dot', 'twopi', 'fdp', 'sfdp', 'circo'
  238. root : Node from G or None (default: None)
  239. The node of G from which to start some layout algorithms.
  240. Returns
  241. -------
  242. dict
  243. Dictionary of positions keyed by node.
  244. Examples
  245. --------
  246. >>> G = nx.complete_graph(4)
  247. >>> pos = nx.nx_pydot.pydot_layout(G)
  248. >>> pos = nx.nx_pydot.pydot_layout(G, prog="dot")
  249. Notes
  250. -----
  251. If you use complex node objects, they may have the same string
  252. representation and GraphViz could treat them as the same node.
  253. The layout may assign both nodes a single location. See Issue #1568
  254. If this occurs in your case, consider relabeling the nodes just
  255. for the layout computation using something similar to::
  256. H = nx.convert_node_labels_to_integers(G, label_attribute="node_label")
  257. H_layout = nx.nx_pydot.pydot_layout(H, prog="dot")
  258. G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
  259. """
  260. import pydot
  261. P = to_pydot(G)
  262. if root is not None:
  263. P.set("root", str(root))
  264. # List of low-level bytes comprising a string in the dot language converted
  265. # from the passed graph with the passed external GraphViz command.
  266. D_bytes = P.create_dot(prog=prog)
  267. # Unique string decoded from these bytes with the preferred locale encoding
  268. D = str(D_bytes, encoding=getpreferredencoding())
  269. if D == "": # no data returned
  270. print(f"Graphviz layout with {prog} failed")
  271. print()
  272. print("To debug what happened try:")
  273. print("P = nx.nx_pydot.to_pydot(G)")
  274. print('P.write_dot("file.dot")')
  275. print(f"And then run {prog} on file.dot")
  276. return
  277. # List of one or more "pydot.Dot" instances deserialized from this string.
  278. Q_list = pydot.graph_from_dot_data(D)
  279. assert len(Q_list) == 1
  280. # The first and only such instance, as guaranteed by the above assertion.
  281. Q = Q_list[0]
  282. node_pos = {}
  283. for n in G.nodes():
  284. str_n = str(n)
  285. node = Q.get_node(pydot.quote_id_if_necessary(str_n))
  286. if isinstance(node, list):
  287. node = node[0]
  288. pos = node.get_pos()[1:-1] # strip leading and trailing double quotes
  289. if pos is not None:
  290. xx, yy = pos.split(",")
  291. node_pos[n] = (float(xx), float(yy))
  292. return node_pos