nx_pydot.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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. import warnings
  19. from locale import getpreferredencoding
  20. import networkx as nx
  21. from networkx.utils import open_file
  22. __all__ = [
  23. "write_dot",
  24. "read_dot",
  25. "graphviz_layout",
  26. "pydot_layout",
  27. "to_pydot",
  28. "from_pydot",
  29. ]
  30. @open_file(1, mode="w")
  31. def write_dot(G, path):
  32. """Write NetworkX graph G to Graphviz dot format on path.
  33. Path can be a string or a file handle.
  34. """
  35. msg = (
  36. "nx.nx_pydot.write_dot depends on the pydot package, which has"
  37. "known issues and is not actively maintained. Consider using"
  38. "nx.nx_agraph.write_dot instead.\n\n"
  39. "See https://github.com/networkx/networkx/issues/5723"
  40. )
  41. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  42. P = to_pydot(G)
  43. path.write(P.to_string())
  44. return
  45. @open_file(0, mode="r")
  46. def read_dot(path):
  47. """Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the
  48. dot file with the passed path.
  49. If this file contains multiple graphs, only the first such graph is
  50. returned. All graphs _except_ the first are silently ignored.
  51. Parameters
  52. ----------
  53. path : str or file
  54. Filename or file handle.
  55. Returns
  56. -------
  57. G : MultiGraph or MultiDiGraph
  58. A :class:`MultiGraph` or :class:`MultiDiGraph`.
  59. Notes
  60. -----
  61. Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a
  62. :class:`MultiGraph`.
  63. """
  64. import pydot
  65. msg = (
  66. "nx.nx_pydot.read_dot depends on the pydot package, which has"
  67. "known issues and is not actively maintained. Consider using"
  68. "nx.nx_agraph.read_dot instead.\n\n"
  69. "See https://github.com/networkx/networkx/issues/5723"
  70. )
  71. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  72. data = path.read()
  73. # List of one or more "pydot.Dot" instances deserialized from this file.
  74. P_list = pydot.graph_from_dot_data(data)
  75. # Convert only the first such instance into a NetworkX graph.
  76. return from_pydot(P_list[0])
  77. def from_pydot(P):
  78. """Returns a NetworkX graph from a Pydot graph.
  79. Parameters
  80. ----------
  81. P : Pydot graph
  82. A graph created with Pydot
  83. Returns
  84. -------
  85. G : NetworkX multigraph
  86. A MultiGraph or MultiDiGraph.
  87. Examples
  88. --------
  89. >>> K5 = nx.complete_graph(5)
  90. >>> A = nx.nx_pydot.to_pydot(K5)
  91. >>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph
  92. # make a Graph instead of MultiGraph
  93. >>> G = nx.Graph(nx.nx_pydot.from_pydot(A))
  94. """
  95. msg = (
  96. "nx.nx_pydot.from_pydot depends on the pydot package, which has"
  97. "known issues and is not actively maintained.\n\n"
  98. "See https://github.com/networkx/networkx/issues/5723"
  99. )
  100. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  101. if P.get_strict(None): # pydot bug: get_strict() shouldn't take argument
  102. multiedges = False
  103. else:
  104. multiedges = True
  105. if P.get_type() == "graph": # undirected
  106. if multiedges:
  107. N = nx.MultiGraph()
  108. else:
  109. N = nx.Graph()
  110. else:
  111. if multiedges:
  112. N = nx.MultiDiGraph()
  113. else:
  114. N = nx.DiGraph()
  115. # assign defaults
  116. name = P.get_name().strip('"')
  117. if name != "":
  118. N.name = name
  119. # add nodes, attributes to N.node_attr
  120. for p in P.get_node_list():
  121. n = p.get_name().strip('"')
  122. if n in ("node", "graph", "edge"):
  123. continue
  124. N.add_node(n, **p.get_attributes())
  125. # add edges
  126. for e in P.get_edge_list():
  127. u = e.get_source()
  128. v = e.get_destination()
  129. attr = e.get_attributes()
  130. s = []
  131. d = []
  132. if isinstance(u, str):
  133. s.append(u.strip('"'))
  134. else:
  135. for unodes in u["nodes"]:
  136. s.append(unodes.strip('"'))
  137. if isinstance(v, str):
  138. d.append(v.strip('"'))
  139. else:
  140. for vnodes in v["nodes"]:
  141. d.append(vnodes.strip('"'))
  142. for source_node in s:
  143. for destination_node in d:
  144. N.add_edge(source_node, destination_node, **attr)
  145. # add default attributes for graph, nodes, edges
  146. pattr = P.get_attributes()
  147. if pattr:
  148. N.graph["graph"] = pattr
  149. try:
  150. N.graph["node"] = P.get_node_defaults()[0]
  151. except (IndexError, TypeError):
  152. pass # N.graph['node']={}
  153. try:
  154. N.graph["edge"] = P.get_edge_defaults()[0]
  155. except (IndexError, TypeError):
  156. pass # N.graph['edge']={}
  157. return N
  158. def _check_colon_quotes(s):
  159. # A quick helper function to check if a string has a colon in it
  160. # and if it is quoted properly with double quotes.
  161. # refer https://github.com/pydot/pydot/issues/258
  162. return ":" in s and (s[0] != '"' or s[-1] != '"')
  163. def to_pydot(N):
  164. """Returns a pydot graph from a NetworkX graph N.
  165. Parameters
  166. ----------
  167. N : NetworkX graph
  168. A graph created with NetworkX
  169. Examples
  170. --------
  171. >>> K5 = nx.complete_graph(5)
  172. >>> P = nx.nx_pydot.to_pydot(K5)
  173. Notes
  174. -----
  175. """
  176. import pydot
  177. msg = (
  178. "nx.nx_pydot.to_pydot depends on the pydot package, which has"
  179. "known issues and is not actively maintained.\n\n"
  180. "See https://github.com/networkx/networkx/issues/5723"
  181. )
  182. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  183. # set Graphviz graph type
  184. if N.is_directed():
  185. graph_type = "digraph"
  186. else:
  187. graph_type = "graph"
  188. strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
  189. name = N.name
  190. graph_defaults = N.graph.get("graph", {})
  191. if name == "":
  192. P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults)
  193. else:
  194. P = pydot.Dot(
  195. f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults
  196. )
  197. try:
  198. P.set_node_defaults(**N.graph["node"])
  199. except KeyError:
  200. pass
  201. try:
  202. P.set_edge_defaults(**N.graph["edge"])
  203. except KeyError:
  204. pass
  205. for n, nodedata in N.nodes(data=True):
  206. str_nodedata = {str(k): str(v) for k, v in nodedata.items()}
  207. # Explicitly catch nodes with ":" in node names or nodedata.
  208. n = str(n)
  209. raise_error = _check_colon_quotes(n) or (
  210. any(
  211. (_check_colon_quotes(k) or _check_colon_quotes(v))
  212. for k, v in str_nodedata.items()
  213. )
  214. )
  215. if raise_error:
  216. raise ValueError(
  217. f'Node names and attributes should not contain ":" unless they are quoted with "".\
  218. For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\
  219. Please refer https://github.com/pydot/pydot/issues/258'
  220. )
  221. p = pydot.Node(n, **str_nodedata)
  222. P.add_node(p)
  223. if N.is_multigraph():
  224. for u, v, key, edgedata in N.edges(data=True, keys=True):
  225. str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"}
  226. u, v = str(u), str(v)
  227. raise_error = (
  228. _check_colon_quotes(u)
  229. or _check_colon_quotes(v)
  230. or (
  231. any(
  232. (_check_colon_quotes(k) or _check_colon_quotes(val))
  233. for k, val in str_edgedata.items()
  234. )
  235. )
  236. )
  237. if raise_error:
  238. raise ValueError(
  239. f'Node names and attributes should not contain ":" unless they are quoted with "".\
  240. For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\
  241. Please refer https://github.com/pydot/pydot/issues/258'
  242. )
  243. edge = pydot.Edge(u, v, key=str(key), **str_edgedata)
  244. P.add_edge(edge)
  245. else:
  246. for u, v, edgedata in N.edges(data=True):
  247. str_edgedata = {str(k): str(v) for k, v in edgedata.items()}
  248. u, v = str(u), str(v)
  249. raise_error = (
  250. _check_colon_quotes(u)
  251. or _check_colon_quotes(v)
  252. or (
  253. any(
  254. (_check_colon_quotes(k) or _check_colon_quotes(val))
  255. for k, val in str_edgedata.items()
  256. )
  257. )
  258. )
  259. if raise_error:
  260. raise ValueError(
  261. f'Node names and attributes should not contain ":" unless they are quoted with "".\
  262. For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\
  263. Please refer https://github.com/pydot/pydot/issues/258'
  264. )
  265. edge = pydot.Edge(u, v, **str_edgedata)
  266. P.add_edge(edge)
  267. return P
  268. def graphviz_layout(G, prog="neato", root=None):
  269. """Create node positions using Pydot and Graphviz.
  270. Returns a dictionary of positions keyed by node.
  271. Parameters
  272. ----------
  273. G : NetworkX Graph
  274. The graph for which the layout is computed.
  275. prog : string (default: 'neato')
  276. The name of the GraphViz program to use for layout.
  277. Options depend on GraphViz version but may include:
  278. 'dot', 'twopi', 'fdp', 'sfdp', 'circo'
  279. root : Node from G or None (default: None)
  280. The node of G from which to start some layout algorithms.
  281. Returns
  282. -------
  283. Dictionary of (x, y) positions keyed by node.
  284. Examples
  285. --------
  286. >>> G = nx.complete_graph(4)
  287. >>> pos = nx.nx_pydot.graphviz_layout(G)
  288. >>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot")
  289. Notes
  290. -----
  291. This is a wrapper for pydot_layout.
  292. """
  293. msg = (
  294. "nx.nx_pydot.graphviz_layout depends on the pydot package, which has"
  295. "known issues and is not actively maintained. Consider using"
  296. "nx.nx_agraph.graphviz_layout instead.\n\n"
  297. "See https://github.com/networkx/networkx/issues/5723"
  298. )
  299. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  300. return pydot_layout(G=G, prog=prog, root=root)
  301. def pydot_layout(G, prog="neato", root=None):
  302. """Create node positions using :mod:`pydot` and Graphviz.
  303. Parameters
  304. ----------
  305. G : Graph
  306. NetworkX graph to be laid out.
  307. prog : string (default: 'neato')
  308. Name of the GraphViz command to use for layout.
  309. Options depend on GraphViz version but may include:
  310. 'dot', 'twopi', 'fdp', 'sfdp', 'circo'
  311. root : Node from G or None (default: None)
  312. The node of G from which to start some layout algorithms.
  313. Returns
  314. -------
  315. dict
  316. Dictionary of positions keyed by node.
  317. Examples
  318. --------
  319. >>> G = nx.complete_graph(4)
  320. >>> pos = nx.nx_pydot.pydot_layout(G)
  321. >>> pos = nx.nx_pydot.pydot_layout(G, prog="dot")
  322. Notes
  323. -----
  324. If you use complex node objects, they may have the same string
  325. representation and GraphViz could treat them as the same node.
  326. The layout may assign both nodes a single location. See Issue #1568
  327. If this occurs in your case, consider relabeling the nodes just
  328. for the layout computation using something similar to::
  329. H = nx.convert_node_labels_to_integers(G, label_attribute='node_label')
  330. H_layout = nx.nx_pydot.pydot_layout(G, prog='dot')
  331. G_layout = {H.nodes[n]['node_label']: p for n, p in H_layout.items()}
  332. """
  333. import pydot
  334. msg = (
  335. "nx.nx_pydot.pydot_layout depends on the pydot package, which has"
  336. "known issues and is not actively maintained.\n\n"
  337. "See https://github.com/networkx/networkx/issues/5723"
  338. )
  339. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  340. P = to_pydot(G)
  341. if root is not None:
  342. P.set("root", str(root))
  343. # List of low-level bytes comprising a string in the dot language converted
  344. # from the passed graph with the passed external GraphViz command.
  345. D_bytes = P.create_dot(prog=prog)
  346. # Unique string decoded from these bytes with the preferred locale encoding
  347. D = str(D_bytes, encoding=getpreferredencoding())
  348. if D == "": # no data returned
  349. print(f"Graphviz layout with {prog} failed")
  350. print()
  351. print("To debug what happened try:")
  352. print("P = nx.nx_pydot.to_pydot(G)")
  353. print('P.write_dot("file.dot")')
  354. print(f"And then run {prog} on file.dot")
  355. return
  356. # List of one or more "pydot.Dot" instances deserialized from this string.
  357. Q_list = pydot.graph_from_dot_data(D)
  358. assert len(Q_list) == 1
  359. # The first and only such instance, as guaranteed by the above assertion.
  360. Q = Q_list[0]
  361. node_pos = {}
  362. for n in G.nodes():
  363. str_n = str(n)
  364. # Explicitly catch nodes with ":" in node names or nodedata.
  365. if _check_colon_quotes(str_n):
  366. raise ValueError(
  367. f'Node names and node attributes should not contain ":" unless they are quoted with "".\
  368. For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\
  369. Please refer https://github.com/pydot/pydot/issues/258'
  370. )
  371. pydot_node = pydot.Node(str_n).get_name()
  372. node = Q.get_node(pydot_node)
  373. if isinstance(node, list):
  374. node = node[0]
  375. pos = node.get_pos()[1:-1] # strip leading and trailing double quotes
  376. if pos is not None:
  377. xx, yy = pos.split(",")
  378. node_pos[n] = (float(xx), float(yy))
  379. return node_pos