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