""" ***** Pydot ***** Import and export NetworkX graphs in Graphviz dot format using pydot. Either this module or nx_agraph can be used to interface with graphviz. Examples -------- >>> G = nx.complete_graph(5) >>> PG = nx.nx_pydot.to_pydot(G) >>> H = nx.nx_pydot.from_pydot(PG) See Also -------- - pydot: https://github.com/erocarrera/pydot - Graphviz: https://www.graphviz.org - DOT Language: http://www.graphviz.org/doc/info/lang.html """ from locale import getpreferredencoding import networkx as nx from networkx.utils import open_file __all__ = [ "write_dot", "read_dot", "graphviz_layout", "pydot_layout", "to_pydot", "from_pydot", ] @open_file(1, mode="w") def write_dot(G, path): """Write NetworkX graph G to Graphviz dot format on path. Path can be a string or a file handle. """ P = to_pydot(G) path.write(P.to_string()) return @open_file(0, mode="r") @nx._dispatchable(name="pydot_read_dot", graphs=None, returns_graph=True) def read_dot(path): """Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the dot file with the passed path. If this file contains multiple graphs, only the first such graph is returned. All graphs _except_ the first are silently ignored. Parameters ---------- path : str or file Filename or file handle. Returns ------- G : MultiGraph or MultiDiGraph A :class:`MultiGraph` or :class:`MultiDiGraph`. Notes ----- Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a :class:`MultiGraph`. """ import pydot data = path.read() # List of one or more "pydot.Dot" instances deserialized from this file. P_list = pydot.graph_from_dot_data(data) # Convert only the first such instance into a NetworkX graph. return from_pydot(P_list[0]) @nx._dispatchable(graphs=None, returns_graph=True) def from_pydot(P): """Returns a NetworkX graph from a Pydot graph. Parameters ---------- P : Pydot graph A graph created with Pydot Returns ------- G : NetworkX multigraph A MultiGraph or MultiDiGraph. Examples -------- >>> K5 = nx.complete_graph(5) >>> A = nx.nx_pydot.to_pydot(K5) >>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph # make a Graph instead of MultiGraph >>> G = nx.Graph(nx.nx_pydot.from_pydot(A)) """ if P.get_strict(None): # pydot bug: get_strict() shouldn't take argument multiedges = False else: multiedges = True if P.get_type() == "graph": # undirected if multiedges: N = nx.MultiGraph() else: N = nx.Graph() else: if multiedges: N = nx.MultiDiGraph() else: N = nx.DiGraph() # assign defaults name = P.get_name().strip('"') if name != "": N.name = name # add nodes, attributes to N.node_attr for p in P.get_node_list(): n = p.get_name().strip('"') if n in ("node", "graph", "edge"): continue N.add_node(n, **p.get_attributes()) # add edges for e in P.get_edge_list(): u = e.get_source() v = e.get_destination() attr = e.get_attributes() s = [] d = [] if isinstance(u, str): s.append(u.strip('"')) else: for unodes in u["nodes"]: s.append(unodes.strip('"')) if isinstance(v, str): d.append(v.strip('"')) else: for vnodes in v["nodes"]: d.append(vnodes.strip('"')) for source_node in s: for destination_node in d: N.add_edge(source_node, destination_node, **attr) # add default attributes for graph, nodes, edges pattr = P.get_attributes() if pattr: N.graph["graph"] = pattr try: N.graph["node"] = P.get_node_defaults()[0] except (IndexError, TypeError): pass # N.graph['node']={} try: N.graph["edge"] = P.get_edge_defaults()[0] except (IndexError, TypeError): pass # N.graph['edge']={} return N def _check_colon_quotes(s): # A quick helper function to check if a string has a colon in it # and if it is quoted properly with double quotes. # refer https://github.com/pydot/pydot/issues/258 return ":" in s and (s[0] != '"' or s[-1] != '"') def to_pydot(N): """Returns a pydot graph from a NetworkX graph N. Parameters ---------- N : NetworkX graph A graph created with NetworkX Examples -------- >>> K5 = nx.complete_graph(5) >>> P = nx.nx_pydot.to_pydot(K5) Notes ----- """ import pydot # set Graphviz graph type if N.is_directed(): graph_type = "digraph" else: graph_type = "graph" strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph() name = N.name graph_defaults = N.graph.get("graph", {}) if name == "": P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults) else: P = pydot.Dot( f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults ) try: P.set_node_defaults(**N.graph["node"]) except KeyError: pass try: P.set_edge_defaults(**N.graph["edge"]) except KeyError: pass for n, nodedata in N.nodes(data=True): str_nodedata = {str(k): str(v) for k, v in nodedata.items()} # Explicitly catch nodes with ":" in node names or nodedata. n = str(n) raise_error = _check_colon_quotes(n) or ( any( (_check_colon_quotes(k) or _check_colon_quotes(v)) for k, v in str_nodedata.items() ) ) if raise_error: raise ValueError( f'Node names and attributes should not contain ":" unless they are quoted with "".\ For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ Please refer https://github.com/pydot/pydot/issues/258' ) p = pydot.Node(n, **str_nodedata) P.add_node(p) if N.is_multigraph(): for u, v, key, edgedata in N.edges(data=True, keys=True): str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"} u, v = str(u), str(v) raise_error = ( _check_colon_quotes(u) or _check_colon_quotes(v) or ( any( (_check_colon_quotes(k) or _check_colon_quotes(val)) for k, val in str_edgedata.items() ) ) ) if raise_error: raise ValueError( f'Node names and attributes should not contain ":" unless they are quoted with "".\ For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ Please refer https://github.com/pydot/pydot/issues/258' ) edge = pydot.Edge(u, v, key=str(key), **str_edgedata) P.add_edge(edge) else: for u, v, edgedata in N.edges(data=True): str_edgedata = {str(k): str(v) for k, v in edgedata.items()} u, v = str(u), str(v) raise_error = ( _check_colon_quotes(u) or _check_colon_quotes(v) or ( any( (_check_colon_quotes(k) or _check_colon_quotes(val)) for k, val in str_edgedata.items() ) ) ) if raise_error: raise ValueError( f'Node names and attributes should not contain ":" unless they are quoted with "".\ For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ Please refer https://github.com/pydot/pydot/issues/258' ) edge = pydot.Edge(u, v, **str_edgedata) P.add_edge(edge) return P def graphviz_layout(G, prog="neato", root=None): """Create node positions using Pydot and Graphviz. Returns a dictionary of positions keyed by node. Parameters ---------- G : NetworkX Graph The graph for which the layout is computed. prog : string (default: 'neato') The name of the GraphViz program to use for layout. Options depend on GraphViz version but may include: 'dot', 'twopi', 'fdp', 'sfdp', 'circo' root : Node from G or None (default: None) The node of G from which to start some layout algorithms. Returns ------- Dictionary of (x, y) positions keyed by node. Examples -------- >>> G = nx.complete_graph(4) >>> pos = nx.nx_pydot.graphviz_layout(G) >>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot") Notes ----- This is a wrapper for pydot_layout. """ return pydot_layout(G=G, prog=prog, root=root) def pydot_layout(G, prog="neato", root=None): """Create node positions using :mod:`pydot` and Graphviz. Parameters ---------- G : Graph NetworkX graph to be laid out. prog : string (default: 'neato') Name of the GraphViz command to use for layout. Options depend on GraphViz version but may include: 'dot', 'twopi', 'fdp', 'sfdp', 'circo' root : Node from G or None (default: None) The node of G from which to start some layout algorithms. Returns ------- dict Dictionary of positions keyed by node. Examples -------- >>> G = nx.complete_graph(4) >>> pos = nx.nx_pydot.pydot_layout(G) >>> pos = nx.nx_pydot.pydot_layout(G, prog="dot") Notes ----- If you use complex node objects, they may have the same string representation and GraphViz could treat them as the same node. The layout may assign both nodes a single location. See Issue #1568 If this occurs in your case, consider relabeling the nodes just for the layout computation using something similar to:: H = nx.convert_node_labels_to_integers(G, label_attribute="node_label") H_layout = nx.nx_pydot.pydot_layout(G, prog="dot") G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()} """ import pydot P = to_pydot(G) if root is not None: P.set("root", str(root)) # List of low-level bytes comprising a string in the dot language converted # from the passed graph with the passed external GraphViz command. D_bytes = P.create_dot(prog=prog) # Unique string decoded from these bytes with the preferred locale encoding D = str(D_bytes, encoding=getpreferredencoding()) if D == "": # no data returned print(f"Graphviz layout with {prog} failed") print() print("To debug what happened try:") print("P = nx.nx_pydot.to_pydot(G)") print('P.write_dot("file.dot")') print(f"And then run {prog} on file.dot") return # List of one or more "pydot.Dot" instances deserialized from this string. Q_list = pydot.graph_from_dot_data(D) assert len(Q_list) == 1 # The first and only such instance, as guaranteed by the above assertion. Q = Q_list[0] node_pos = {} for n in G.nodes(): str_n = str(n) # Explicitly catch nodes with ":" in node names or nodedata. if _check_colon_quotes(str_n): raise ValueError( f'Node names and node attributes should not contain ":" unless they are quoted with "".\ For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ Please refer https://github.com/pydot/pydot/issues/258' ) pydot_node = pydot.Node(str_n).get_name() node = Q.get_node(pydot_node) if isinstance(node, list): node = node[0] pos = node.get_pos()[1:-1] # strip leading and trailing double quotes if pos is not None: xx, yy = pos.split(",") node_pos[n] = (float(xx), float(yy)) return node_pos