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 if isinstance(val, str):
165 a.attr["pos"] = val
166 else:
167 a.attr["pos"] = f"{val[0]},{val[1]}!"
168 else:
169 a.attr[key] = str(val)
170
171 # loop over edges
172 if N.is_multigraph():
173 for u, v, key, edgedata in N.edges(data=True, keys=True):
174 str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"}
175 A.add_edge(u, v, key=str(key))
176 # Add edge data
177 a = A.get_edge(u, v)
178 a.attr.update(str_edgedata)
179
180 else:
181 for u, v, edgedata in N.edges(data=True):
182 str_edgedata = {k: str(v) for k, v in edgedata.items()}
183 A.add_edge(u, v)
184 # Add edge data
185 a = A.get_edge(u, v)
186 a.attr.update(str_edgedata)
187
188 return A
189
190
191def write_dot(G, path):
192 """Write NetworkX graph G to Graphviz dot format on path.
193
194 Parameters
195 ----------
196 G : graph
197 A networkx graph
198 path : filename
199 Filename or file handle to write
200
201 Notes
202 -----
203 To use a specific graph layout, call ``A.layout`` prior to `write_dot`.
204 Note that some graphviz layouts are not guaranteed to be deterministic,
205 see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
206 """
207 A = to_agraph(G)
208 A.write(path)
209 A.clear()
210 return
211
212
213@nx._dispatchable(name="agraph_read_dot", graphs=None, returns_graph=True)
214def read_dot(path):
215 """Returns a NetworkX graph from a dot file on path.
216
217 Parameters
218 ----------
219 path : file or string
220 File name or file handle to read.
221 """
222 try:
223 import pygraphviz
224 except ImportError as err:
225 raise ImportError(
226 "read_dot() requires pygraphviz http://pygraphviz.github.io/"
227 ) from err
228 A = pygraphviz.AGraph(file=path)
229 gr = from_agraph(A)
230 A.clear()
231 return gr
232
233
234def graphviz_layout(G, prog="neato", root=None, args=""):
235 """Create node positions for G using Graphviz.
236
237 Parameters
238 ----------
239 G : NetworkX graph
240 A graph created with NetworkX
241 prog : string
242 Name of Graphviz layout program
243 root : string, optional
244 Root node for twopi layout
245 args : string, optional
246 Extra arguments to Graphviz layout program
247
248 Returns
249 -------
250 Dictionary of x, y, positions keyed by node.
251
252 Examples
253 --------
254 >>> G = nx.petersen_graph()
255 >>> pos = nx.nx_agraph.graphviz_layout(G)
256 >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
257
258 Notes
259 -----
260 This is a wrapper for pygraphviz_layout.
261
262 Note that some graphviz layouts are not guaranteed to be deterministic,
263 see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
264 """
265 return pygraphviz_layout(G, prog=prog, root=root, args=args)
266
267
268def pygraphviz_layout(G, prog="neato", root=None, args=""):
269 """Create node positions for G using Graphviz.
270
271 Parameters
272 ----------
273 G : NetworkX graph
274 A graph created with NetworkX
275 prog : string
276 Name of Graphviz layout program
277 root : string, optional
278 Root node for twopi layout
279 args : string, optional
280 Extra arguments to Graphviz layout program
281
282 Returns
283 -------
284 node_pos : dict
285 Dictionary of x, y, positions keyed by node.
286
287 Examples
288 --------
289 >>> G = nx.petersen_graph()
290 >>> pos = nx.nx_agraph.graphviz_layout(G)
291 >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
292
293 Notes
294 -----
295 If you use complex node objects, they may have the same string
296 representation and GraphViz could treat them as the same node.
297 The layout may assign both nodes a single location. See Issue #1568
298 If this occurs in your case, consider relabeling the nodes just
299 for the layout computation using something similar to::
300
301 >>> H = nx.convert_node_labels_to_integers(G, label_attribute="node_label")
302 >>> H_layout = nx.nx_agraph.pygraphviz_layout(H, prog="dot")
303 >>> G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
304
305 Note that some graphviz layouts are not guaranteed to be deterministic,
306 see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
307 """
308 try:
309 import pygraphviz
310 except ImportError as err:
311 raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
312 if root is not None:
313 args += f"-Groot={root}"
314 A = to_agraph(G)
315 A.layout(prog=prog, args=args)
316 node_pos = {}
317 for n in G:
318 node = pygraphviz.Node(A, n)
319 try:
320 xs = node.attr["pos"].split(",")
321 node_pos[n] = tuple(float(x) for x in xs)
322 except:
323 print("no position for node", n)
324 node_pos[n] = (0.0, 0.0)
325 return node_pos
326
327
328@nx.utils.open_file(5, "w+b")
329def view_pygraphviz(
330 G, edgelabel=None, prog="dot", args="", suffix="", path=None, show=True
331):
332 """Views the graph G using the specified layout algorithm.
333
334 Parameters
335 ----------
336 G : NetworkX graph
337 The machine to draw.
338 edgelabel : str, callable, None
339 If a string, then it specifies the edge attribute to be displayed
340 on the edge labels. If a callable, then it is called for each
341 edge and it should return the string to be displayed on the edges.
342 The function signature of `edgelabel` should be edgelabel(data),
343 where `data` is the edge attribute dictionary.
344 prog : string
345 Name of Graphviz layout program.
346 args : str
347 Additional arguments to pass to the Graphviz layout program.
348 suffix : str
349 If `filename` is None, we save to a temporary file. The value of
350 `suffix` will appear at the tail end of the temporary filename.
351 path : str, None
352 The filename used to save the image. If None, save to a temporary
353 file. File formats are the same as those from pygraphviz.agraph.draw.
354 Filenames ending in .gz or .bz2 will be compressed.
355 show : bool, default = True
356 Whether to display the graph with :mod:`PIL.Image.show`,
357 default is `True`. If `False`, the rendered graph is still available
358 at `path`.
359
360 Returns
361 -------
362 path : str
363 The filename of the generated image.
364 A : PyGraphviz graph
365 The PyGraphviz graph instance used to generate the image.
366
367 Notes
368 -----
369 If this function is called in succession too quickly, sometimes the
370 image is not displayed. So you might consider time.sleep(.5) between
371 calls if you experience problems.
372
373 Note that some graphviz layouts are not guaranteed to be deterministic,
374 see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
375
376 """
377 if not len(G):
378 raise nx.NetworkXException("An empty graph cannot be drawn.")
379
380 # If we are providing default values for graphviz, these must be set
381 # before any nodes or edges are added to the PyGraphviz graph object.
382 # The reason for this is that default values only affect incoming objects.
383 # If you change the default values after the objects have been added,
384 # then they inherit no value and are set only if explicitly set.
385
386 # to_agraph() uses these values.
387 attrs = ["edge", "node", "graph"]
388 for attr in attrs:
389 if attr not in G.graph:
390 G.graph[attr] = {}
391
392 # These are the default values.
393 edge_attrs = {"fontsize": "10"}
394 node_attrs = {
395 "style": "filled",
396 "fillcolor": "#0000FF40",
397 "height": "0.75",
398 "width": "0.75",
399 "shape": "circle",
400 }
401 graph_attrs = {}
402
403 def update_attrs(which, attrs):
404 # Update graph attributes. Return list of those which were added.
405 added = []
406 for k, v in attrs.items():
407 if k not in G.graph[which]:
408 G.graph[which][k] = v
409 added.append(k)
410
411 def clean_attrs(which, added):
412 # Remove added attributes
413 for attr in added:
414 del G.graph[which][attr]
415 if not G.graph[which]:
416 del G.graph[which]
417
418 # Update all default values
419 update_attrs("edge", edge_attrs)
420 update_attrs("node", node_attrs)
421 update_attrs("graph", graph_attrs)
422
423 # Convert to agraph, so we inherit default values
424 A = to_agraph(G)
425
426 # Remove the default values we added to the original graph.
427 clean_attrs("edge", edge_attrs)
428 clean_attrs("node", node_attrs)
429 clean_attrs("graph", graph_attrs)
430
431 # If the user passed in an edgelabel, we update the labels for all edges.
432 if edgelabel is not None:
433 if not callable(edgelabel):
434
435 def func(data):
436 return "".join([" ", str(data[edgelabel]), " "])
437
438 else:
439 func = edgelabel
440
441 # update all the edge labels
442 if G.is_multigraph():
443 for u, v, key, data in G.edges(keys=True, data=True):
444 # PyGraphviz doesn't convert the key to a string. See #339
445 edge = A.get_edge(u, v, str(key))
446 edge.attr["label"] = str(func(data))
447 else:
448 for u, v, data in G.edges(data=True):
449 edge = A.get_edge(u, v)
450 edge.attr["label"] = str(func(data))
451
452 if path is None:
453 ext = "png"
454 if suffix:
455 suffix = f"_{suffix}.{ext}"
456 else:
457 suffix = f".{ext}"
458 path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
459 else:
460 # Assume the decorator worked and it is a file-object.
461 pass
462
463 # Write graph to file
464 A.draw(path=path, format=None, prog=prog, args=args)
465 path.close()
466
467 # Show graph in a new window (depends on platform configuration)
468 if show:
469 from PIL import Image
470
471 Image.open(path.name).show()
472
473 return path.name, A