123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- """
- *****
- 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
- """
- import warnings
- 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.
- """
- msg = (
- "nx.nx_pydot.write_dot depends on the pydot package, which has"
- "known issues and is not actively maintained. Consider using"
- "nx.nx_agraph.write_dot instead.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- P = to_pydot(G)
- path.write(P.to_string())
- return
- @open_file(0, mode="r")
- 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
- msg = (
- "nx.nx_pydot.read_dot depends on the pydot package, which has"
- "known issues and is not actively maintained. Consider using"
- "nx.nx_agraph.read_dot instead.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- 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])
- 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))
- """
- msg = (
- "nx.nx_pydot.from_pydot depends on the pydot package, which has"
- "known issues and is not actively maintained.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- 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
- msg = (
- "nx.nx_pydot.to_pydot depends on the pydot package, which has"
- "known issues and is not actively maintained.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- # 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.
- """
- msg = (
- "nx.nx_pydot.graphviz_layout depends on the pydot package, which has"
- "known issues and is not actively maintained. Consider using"
- "nx.nx_agraph.graphviz_layout instead.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- 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
- msg = (
- "nx.nx_pydot.pydot_layout depends on the pydot package, which has"
- "known issues and is not actively maintained.\n\n"
- "See https://github.com/networkx/networkx/issues/5723"
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=2)
- 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
|