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