up follow livre
This commit is contained in:
parent
70a5c3465c
commit
cffb31c1ef
12198 changed files with 2562132 additions and 35 deletions
|
|
@ -0,0 +1,7 @@
|
|||
# graph drawing and interface to graphviz
|
||||
|
||||
from .layout import *
|
||||
from .nx_latex import *
|
||||
from .nx_pylab import *
|
||||
from . import nx_agraph
|
||||
from . import nx_pydot
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2028
venv/lib/python3.13/site-packages/networkx/drawing/layout.py
Normal file
2028
venv/lib/python3.13/site-packages/networkx/drawing/layout.py
Normal file
File diff suppressed because it is too large
Load diff
464
venv/lib/python3.13/site-packages/networkx/drawing/nx_agraph.py
Normal file
464
venv/lib/python3.13/site-packages/networkx/drawing/nx_agraph.py
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
"""
|
||||
***************
|
||||
Graphviz AGraph
|
||||
***************
|
||||
|
||||
Interface to pygraphviz AGraph class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.complete_graph(5)
|
||||
>>> A = nx.nx_agraph.to_agraph(G)
|
||||
>>> H = nx.nx_agraph.from_agraph(A)
|
||||
|
||||
See Also
|
||||
--------
|
||||
- Pygraphviz: http://pygraphviz.github.io/
|
||||
- Graphviz: https://www.graphviz.org
|
||||
- DOT Language: http://www.graphviz.org/doc/info/lang.html
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = [
|
||||
"from_agraph",
|
||||
"to_agraph",
|
||||
"write_dot",
|
||||
"read_dot",
|
||||
"graphviz_layout",
|
||||
"pygraphviz_layout",
|
||||
"view_pygraphviz",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def from_agraph(A, create_using=None):
|
||||
"""Returns a NetworkX Graph or DiGraph from a PyGraphviz graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : PyGraphviz AGraph
|
||||
A graph created with PyGraphviz
|
||||
|
||||
create_using : NetworkX graph constructor, optional (default=None)
|
||||
Graph type to create. If graph instance, then cleared before populated.
|
||||
If `None`, then the appropriate Graph type is inferred from `A`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> K5 = nx.complete_graph(5)
|
||||
>>> A = nx.nx_agraph.to_agraph(K5)
|
||||
>>> G = nx.nx_agraph.from_agraph(A)
|
||||
|
||||
Notes
|
||||
-----
|
||||
The Graph G will have a dictionary G.graph_attr containing
|
||||
the default graphviz attributes for graphs, nodes and edges.
|
||||
|
||||
Default node attributes will be in the dictionary G.node_attr
|
||||
which is keyed by node.
|
||||
|
||||
Edge attributes will be returned as edge data in G. With
|
||||
edge_attr=False the edge data will be the Graphviz edge weight
|
||||
attribute or the value 1 if no edge weight attribute is found.
|
||||
|
||||
"""
|
||||
if create_using is None:
|
||||
if A.is_directed():
|
||||
if A.is_strict():
|
||||
create_using = nx.DiGraph
|
||||
else:
|
||||
create_using = nx.MultiDiGraph
|
||||
else:
|
||||
if A.is_strict():
|
||||
create_using = nx.Graph
|
||||
else:
|
||||
create_using = nx.MultiGraph
|
||||
|
||||
# assign defaults
|
||||
N = nx.empty_graph(0, create_using)
|
||||
if A.name is not None:
|
||||
N.name = A.name
|
||||
|
||||
# add graph attributes
|
||||
N.graph.update(A.graph_attr)
|
||||
|
||||
# add nodes, attributes to N.node_attr
|
||||
for n in A.nodes():
|
||||
str_attr = {str(k): v for k, v in n.attr.items()}
|
||||
N.add_node(str(n), **str_attr)
|
||||
|
||||
# add edges, assign edge data as dictionary of attributes
|
||||
for e in A.edges():
|
||||
u, v = str(e[0]), str(e[1])
|
||||
attr = dict(e.attr)
|
||||
str_attr = {str(k): v for k, v in attr.items()}
|
||||
if not N.is_multigraph():
|
||||
if e.name is not None:
|
||||
str_attr["key"] = e.name
|
||||
N.add_edge(u, v, **str_attr)
|
||||
else:
|
||||
N.add_edge(u, v, key=e.name, **str_attr)
|
||||
|
||||
# add default attributes for graph, nodes, and edges
|
||||
# hang them on N.graph_attr
|
||||
N.graph["graph"] = dict(A.graph_attr)
|
||||
N.graph["node"] = dict(A.node_attr)
|
||||
N.graph["edge"] = dict(A.edge_attr)
|
||||
return N
|
||||
|
||||
|
||||
def to_agraph(N):
|
||||
"""Returns a pygraphviz graph from a NetworkX graph N.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
N : NetworkX graph
|
||||
A graph created with NetworkX
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> K5 = nx.complete_graph(5)
|
||||
>>> A = nx.nx_agraph.to_agraph(K5)
|
||||
|
||||
Notes
|
||||
-----
|
||||
If N has an dict N.graph_attr an attempt will be made first
|
||||
to copy properties attached to the graph (see from_agraph)
|
||||
and then updated with the calling arguments if any.
|
||||
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError as err:
|
||||
raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
|
||||
directed = N.is_directed()
|
||||
strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
|
||||
|
||||
A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed)
|
||||
|
||||
# default graph attributes
|
||||
A.graph_attr.update(N.graph.get("graph", {}))
|
||||
A.node_attr.update(N.graph.get("node", {}))
|
||||
A.edge_attr.update(N.graph.get("edge", {}))
|
||||
|
||||
A.graph_attr.update(
|
||||
(k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge")
|
||||
)
|
||||
|
||||
# add nodes
|
||||
for n, nodedata in N.nodes(data=True):
|
||||
A.add_node(n)
|
||||
# Add node data
|
||||
a = A.get_node(n)
|
||||
for key, val in nodedata.items():
|
||||
if key == "pos":
|
||||
a.attr["pos"] = f"{val[0]},{val[1]}!"
|
||||
else:
|
||||
a.attr[key] = str(val)
|
||||
|
||||
# loop over edges
|
||||
if N.is_multigraph():
|
||||
for u, v, key, edgedata in N.edges(data=True, keys=True):
|
||||
str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"}
|
||||
A.add_edge(u, v, key=str(key))
|
||||
# Add edge data
|
||||
a = A.get_edge(u, v)
|
||||
a.attr.update(str_edgedata)
|
||||
|
||||
else:
|
||||
for u, v, edgedata in N.edges(data=True):
|
||||
str_edgedata = {k: str(v) for k, v in edgedata.items()}
|
||||
A.add_edge(u, v)
|
||||
# Add edge data
|
||||
a = A.get_edge(u, v)
|
||||
a.attr.update(str_edgedata)
|
||||
|
||||
return A
|
||||
|
||||
|
||||
def write_dot(G, path):
|
||||
"""Write NetworkX graph G to Graphviz dot format on path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph
|
||||
path : filename
|
||||
Filename or file handle to write
|
||||
|
||||
Notes
|
||||
-----
|
||||
To use a specific graph layout, call ``A.layout`` prior to `write_dot`.
|
||||
Note that some graphviz layouts are not guaranteed to be deterministic,
|
||||
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
|
||||
"""
|
||||
A = to_agraph(G)
|
||||
A.write(path)
|
||||
A.clear()
|
||||
return
|
||||
|
||||
|
||||
@nx._dispatchable(name="agraph_read_dot", graphs=None, returns_graph=True)
|
||||
def read_dot(path):
|
||||
"""Returns a NetworkX graph from a dot file on path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : file or string
|
||||
File name or file handle to read.
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"read_dot() requires pygraphviz http://pygraphviz.github.io/"
|
||||
) from err
|
||||
A = pygraphviz.AGraph(file=path)
|
||||
gr = from_agraph(A)
|
||||
A.clear()
|
||||
return gr
|
||||
|
||||
|
||||
def graphviz_layout(G, prog="neato", root=None, args=""):
|
||||
"""Create node positions for G using Graphviz.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
A graph created with NetworkX
|
||||
prog : string
|
||||
Name of Graphviz layout program
|
||||
root : string, optional
|
||||
Root node for twopi layout
|
||||
args : string, optional
|
||||
Extra arguments to Graphviz layout program
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dictionary of x, y, positions keyed by node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> pos = nx.nx_agraph.graphviz_layout(G)
|
||||
>>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a wrapper for pygraphviz_layout.
|
||||
|
||||
Note that some graphviz layouts are not guaranteed to be deterministic,
|
||||
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
|
||||
"""
|
||||
return pygraphviz_layout(G, prog=prog, root=root, args=args)
|
||||
|
||||
|
||||
def pygraphviz_layout(G, prog="neato", root=None, args=""):
|
||||
"""Create node positions for G using Graphviz.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
A graph created with NetworkX
|
||||
prog : string
|
||||
Name of Graphviz layout program
|
||||
root : string, optional
|
||||
Root node for twopi layout
|
||||
args : string, optional
|
||||
Extra arguments to Graphviz layout program
|
||||
|
||||
Returns
|
||||
-------
|
||||
node_pos : dict
|
||||
Dictionary of x, y, positions keyed by node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> pos = nx.nx_agraph.graphviz_layout(G)
|
||||
>>> pos = nx.nx_agraph.graphviz_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_agraph.pygraphviz_layout(H, prog="dot")
|
||||
>>> G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
|
||||
|
||||
Note that some graphviz layouts are not guaranteed to be deterministic,
|
||||
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError as err:
|
||||
raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
|
||||
if root is not None:
|
||||
args += f"-Groot={root}"
|
||||
A = to_agraph(G)
|
||||
A.layout(prog=prog, args=args)
|
||||
node_pos = {}
|
||||
for n in G:
|
||||
node = pygraphviz.Node(A, n)
|
||||
try:
|
||||
xs = node.attr["pos"].split(",")
|
||||
node_pos[n] = tuple(float(x) for x in xs)
|
||||
except:
|
||||
print("no position for node", n)
|
||||
node_pos[n] = (0.0, 0.0)
|
||||
return node_pos
|
||||
|
||||
|
||||
@nx.utils.open_file(5, "w+b")
|
||||
def view_pygraphviz(
|
||||
G, edgelabel=None, prog="dot", args="", suffix="", path=None, show=True
|
||||
):
|
||||
"""Views the graph G using the specified layout algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
The machine to draw.
|
||||
edgelabel : str, callable, None
|
||||
If a string, then it specifies the edge attribute to be displayed
|
||||
on the edge labels. If a callable, then it is called for each
|
||||
edge and it should return the string to be displayed on the edges.
|
||||
The function signature of `edgelabel` should be edgelabel(data),
|
||||
where `data` is the edge attribute dictionary.
|
||||
prog : string
|
||||
Name of Graphviz layout program.
|
||||
args : str
|
||||
Additional arguments to pass to the Graphviz layout program.
|
||||
suffix : str
|
||||
If `filename` is None, we save to a temporary file. The value of
|
||||
`suffix` will appear at the tail end of the temporary filename.
|
||||
path : str, None
|
||||
The filename used to save the image. If None, save to a temporary
|
||||
file. File formats are the same as those from pygraphviz.agraph.draw.
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
show : bool, default = True
|
||||
Whether to display the graph with :mod:`PIL.Image.show`,
|
||||
default is `True`. If `False`, the rendered graph is still available
|
||||
at `path`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : str
|
||||
The filename of the generated image.
|
||||
A : PyGraphviz graph
|
||||
The PyGraphviz graph instance used to generate the image.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If this function is called in succession too quickly, sometimes the
|
||||
image is not displayed. So you might consider time.sleep(.5) between
|
||||
calls if you experience problems.
|
||||
|
||||
Note that some graphviz layouts are not guaranteed to be deterministic,
|
||||
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
|
||||
|
||||
"""
|
||||
if not len(G):
|
||||
raise nx.NetworkXException("An empty graph cannot be drawn.")
|
||||
|
||||
# If we are providing default values for graphviz, these must be set
|
||||
# before any nodes or edges are added to the PyGraphviz graph object.
|
||||
# The reason for this is that default values only affect incoming objects.
|
||||
# If you change the default values after the objects have been added,
|
||||
# then they inherit no value and are set only if explicitly set.
|
||||
|
||||
# to_agraph() uses these values.
|
||||
attrs = ["edge", "node", "graph"]
|
||||
for attr in attrs:
|
||||
if attr not in G.graph:
|
||||
G.graph[attr] = {}
|
||||
|
||||
# These are the default values.
|
||||
edge_attrs = {"fontsize": "10"}
|
||||
node_attrs = {
|
||||
"style": "filled",
|
||||
"fillcolor": "#0000FF40",
|
||||
"height": "0.75",
|
||||
"width": "0.75",
|
||||
"shape": "circle",
|
||||
}
|
||||
graph_attrs = {}
|
||||
|
||||
def update_attrs(which, attrs):
|
||||
# Update graph attributes. Return list of those which were added.
|
||||
added = []
|
||||
for k, v in attrs.items():
|
||||
if k not in G.graph[which]:
|
||||
G.graph[which][k] = v
|
||||
added.append(k)
|
||||
|
||||
def clean_attrs(which, added):
|
||||
# Remove added attributes
|
||||
for attr in added:
|
||||
del G.graph[which][attr]
|
||||
if not G.graph[which]:
|
||||
del G.graph[which]
|
||||
|
||||
# Update all default values
|
||||
update_attrs("edge", edge_attrs)
|
||||
update_attrs("node", node_attrs)
|
||||
update_attrs("graph", graph_attrs)
|
||||
|
||||
# Convert to agraph, so we inherit default values
|
||||
A = to_agraph(G)
|
||||
|
||||
# Remove the default values we added to the original graph.
|
||||
clean_attrs("edge", edge_attrs)
|
||||
clean_attrs("node", node_attrs)
|
||||
clean_attrs("graph", graph_attrs)
|
||||
|
||||
# If the user passed in an edgelabel, we update the labels for all edges.
|
||||
if edgelabel is not None:
|
||||
if not callable(edgelabel):
|
||||
|
||||
def func(data):
|
||||
return "".join([" ", str(data[edgelabel]), " "])
|
||||
|
||||
else:
|
||||
func = edgelabel
|
||||
|
||||
# update all the edge labels
|
||||
if G.is_multigraph():
|
||||
for u, v, key, data in G.edges(keys=True, data=True):
|
||||
# PyGraphviz doesn't convert the key to a string. See #339
|
||||
edge = A.get_edge(u, v, str(key))
|
||||
edge.attr["label"] = str(func(data))
|
||||
else:
|
||||
for u, v, data in G.edges(data=True):
|
||||
edge = A.get_edge(u, v)
|
||||
edge.attr["label"] = str(func(data))
|
||||
|
||||
if path is None:
|
||||
ext = "png"
|
||||
if suffix:
|
||||
suffix = f"_{suffix}.{ext}"
|
||||
else:
|
||||
suffix = f".{ext}"
|
||||
path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
||||
else:
|
||||
# Assume the decorator worked and it is a file-object.
|
||||
pass
|
||||
|
||||
# Write graph to file
|
||||
A.draw(path=path, format=None, prog=prog, args=args)
|
||||
path.close()
|
||||
|
||||
# Show graph in a new window (depends on platform configuration)
|
||||
if show:
|
||||
from PIL import Image
|
||||
|
||||
Image.open(path.name).show()
|
||||
|
||||
return path.name, A
|
||||
570
venv/lib/python3.13/site-packages/networkx/drawing/nx_latex.py
Normal file
570
venv/lib/python3.13/site-packages/networkx/drawing/nx_latex.py
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
r"""
|
||||
*****
|
||||
LaTeX
|
||||
*****
|
||||
|
||||
Export NetworkX graphs in LaTeX format using the TikZ library within TeX/LaTeX.
|
||||
Usually, you will want the drawing to appear in a figure environment so
|
||||
you use ``to_latex(G, caption="A caption")``. If you want the raw
|
||||
drawing commands without a figure environment use :func:`to_latex_raw`.
|
||||
And if you want to write to a file instead of just returning the latex
|
||||
code as a string, use ``write_latex(G, "filename.tex", caption="A caption")``.
|
||||
|
||||
To construct a figure with subfigures for each graph to be shown, provide
|
||||
``to_latex`` or ``write_latex`` a list of graphs, a list of subcaptions,
|
||||
and a number of rows of subfigures inside the figure.
|
||||
|
||||
To be able to refer to the figures or subfigures in latex using ``\\ref``,
|
||||
the keyword ``latex_label`` is available for figures and `sub_labels` for
|
||||
a list of labels, one for each subfigure.
|
||||
|
||||
We intend to eventually provide an interface to the TikZ Graph
|
||||
features which include e.g. layout algorithms.
|
||||
|
||||
Let us know via github what you'd like to see available, or better yet
|
||||
give us some code to do it, or even better make a github pull request
|
||||
to add the feature.
|
||||
|
||||
The TikZ approach
|
||||
=================
|
||||
Drawing options can be stored on the graph as node/edge attributes, or
|
||||
can be provided as dicts keyed by node/edge to a string of the options
|
||||
for that node/edge. Similarly a label can be shown for each node/edge
|
||||
by specifying the labels as graph node/edge attributes or by providing
|
||||
a dict keyed by node/edge to the text to be written for that node/edge.
|
||||
|
||||
Options for the tikzpicture environment (e.g. "[scale=2]") can be provided
|
||||
via a keyword argument. Similarly default node and edge options can be
|
||||
provided through keywords arguments. The default node options are applied
|
||||
to the single TikZ "path" that draws all nodes (and no edges). The default edge
|
||||
options are applied to a TikZ "scope" which contains a path for each edge.
|
||||
|
||||
Examples
|
||||
========
|
||||
>>> G = nx.path_graph(3)
|
||||
>>> nx.write_latex(G, "just_my_figure.tex", as_document=True)
|
||||
>>> nx.write_latex(G, "my_figure.tex", caption="A path graph", latex_label="fig1")
|
||||
>>> latex_code = nx.to_latex(G) # a string rather than a file
|
||||
|
||||
You can change many features of the nodes and edges.
|
||||
|
||||
>>> G = nx.path_graph(4, create_using=nx.DiGraph)
|
||||
>>> pos = {n: (n, n) for n in G} # nodes set on a line
|
||||
|
||||
>>> G.nodes[0]["style"] = "blue"
|
||||
>>> G.nodes[2]["style"] = "line width=3,draw"
|
||||
>>> G.nodes[3]["label"] = "Stop"
|
||||
>>> G.edges[(0, 1)]["label"] = "1st Step"
|
||||
>>> G.edges[(0, 1)]["label_opts"] = "near start"
|
||||
>>> G.edges[(1, 2)]["style"] = "line width=3"
|
||||
>>> G.edges[(1, 2)]["label"] = "2nd Step"
|
||||
>>> G.edges[(2, 3)]["style"] = "green"
|
||||
>>> G.edges[(2, 3)]["label"] = "3rd Step"
|
||||
>>> G.edges[(2, 3)]["label_opts"] = "near end"
|
||||
|
||||
>>> nx.write_latex(G, "latex_graph.tex", pos=pos, as_document=True)
|
||||
|
||||
Then compile the LaTeX using something like ``pdflatex latex_graph.tex``
|
||||
and view the pdf file created: ``latex_graph.pdf``.
|
||||
|
||||
If you want **subfigures** each containing one graph, you can input a list of graphs.
|
||||
|
||||
>>> H1 = nx.path_graph(4)
|
||||
>>> H2 = nx.complete_graph(4)
|
||||
>>> H3 = nx.path_graph(8)
|
||||
>>> H4 = nx.complete_graph(8)
|
||||
>>> graphs = [H1, H2, H3, H4]
|
||||
>>> caps = ["Path 4", "Complete graph 4", "Path 8", "Complete graph 8"]
|
||||
>>> lbls = ["fig2a", "fig2b", "fig2c", "fig2d"]
|
||||
>>> nx.write_latex(graphs, "subfigs.tex", n_rows=2, sub_captions=caps, sub_labels=lbls)
|
||||
>>> latex_code = nx.to_latex(graphs, n_rows=2, sub_captions=caps, sub_labels=lbls)
|
||||
|
||||
>>> node_color = {0: "red", 1: "orange", 2: "blue", 3: "gray!90"}
|
||||
>>> edge_width = {e: "line width=1.5" for e in H3.edges}
|
||||
>>> pos = nx.circular_layout(H3)
|
||||
>>> latex_code = nx.to_latex(H3, pos, node_options=node_color, edge_options=edge_width)
|
||||
>>> print(latex_code)
|
||||
\documentclass{report}
|
||||
\usepackage{tikz}
|
||||
\usepackage{subcaption}
|
||||
<BLANKLINE>
|
||||
\begin{document}
|
||||
\begin{figure}
|
||||
\begin{tikzpicture}
|
||||
\draw
|
||||
(1.0, 0.0) node[red] (0){0}
|
||||
(0.707, 0.707) node[orange] (1){1}
|
||||
(-0.0, 1.0) node[blue] (2){2}
|
||||
(-0.707, 0.707) node[gray!90] (3){3}
|
||||
(-1.0, -0.0) node (4){4}
|
||||
(-0.707, -0.707) node (5){5}
|
||||
(0.0, -1.0) node (6){6}
|
||||
(0.707, -0.707) node (7){7};
|
||||
\begin{scope}[-]
|
||||
\draw[line width=1.5] (0) to (1);
|
||||
\draw[line width=1.5] (1) to (2);
|
||||
\draw[line width=1.5] (2) to (3);
|
||||
\draw[line width=1.5] (3) to (4);
|
||||
\draw[line width=1.5] (4) to (5);
|
||||
\draw[line width=1.5] (5) to (6);
|
||||
\draw[line width=1.5] (6) to (7);
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{figure}
|
||||
\end{document}
|
||||
|
||||
Notes
|
||||
-----
|
||||
If you want to change the preamble/postamble of the figure/document/subfigure
|
||||
environment, use the keyword arguments: `figure_wrapper`, `document_wrapper`,
|
||||
`subfigure_wrapper`. The default values are stored in private variables
|
||||
e.g. ``nx.nx_layout._DOCUMENT_WRAPPER``
|
||||
|
||||
References
|
||||
----------
|
||||
TikZ: https://tikz.dev/
|
||||
|
||||
TikZ options details: https://tikz.dev/tikz-actions
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = [
|
||||
"to_latex_raw",
|
||||
"to_latex",
|
||||
"write_latex",
|
||||
]
|
||||
|
||||
|
||||
@nx.utils.not_implemented_for("multigraph")
|
||||
def to_latex_raw(
|
||||
G,
|
||||
pos="pos",
|
||||
tikz_options="",
|
||||
default_node_options="",
|
||||
node_options="node_options",
|
||||
node_label="label",
|
||||
default_edge_options="",
|
||||
edge_options="edge_options",
|
||||
edge_label="label",
|
||||
edge_label_options="edge_label_options",
|
||||
):
|
||||
"""Return a string of the LaTeX/TikZ code to draw `G`
|
||||
|
||||
This function produces just the code for the tikzpicture
|
||||
without any enclosing environment.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
G : NetworkX graph
|
||||
The NetworkX graph to be drawn
|
||||
pos : string or dict (default "pos")
|
||||
The name of the node attribute on `G` that holds the position of each node.
|
||||
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
|
||||
They can also be strings to denote positions in TikZ style, such as (x, y)
|
||||
or (angle:radius).
|
||||
If a dict, it should be keyed by node to a position.
|
||||
If an empty dict, a circular layout is computed by TikZ.
|
||||
tikz_options : string
|
||||
The tikzpicture options description defining the options for the picture.
|
||||
Often large scale options like `[scale=2]`.
|
||||
default_node_options : string
|
||||
The draw options for a path of nodes. Individual node options override these.
|
||||
node_options : string or dict
|
||||
The name of the node attribute on `G` that holds the options for each node.
|
||||
Or a dict keyed by node to a string holding the options for that node.
|
||||
node_label : string or dict
|
||||
The name of the node attribute on `G` that holds the node label (text)
|
||||
displayed for each node. If the attribute is "" or not present, the node
|
||||
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
|
||||
Or a dict keyed by node to a string holding the label for that node.
|
||||
default_edge_options : string
|
||||
The options for the scope drawing all edges. The default is "[-]" for
|
||||
undirected graphs and "[->]" for directed graphs.
|
||||
edge_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the options for each edge.
|
||||
If the edge is a self-loop and ``"loop" not in edge_options`` the option
|
||||
"loop," is added to the options for the self-loop edge. Hence you can
|
||||
use "[loop above]" explicitly, but the default is "[loop]".
|
||||
Or a dict keyed by edge to a string holding the options for that edge.
|
||||
edge_label : string or dict
|
||||
The name of the edge attribute on `G` that holds the edge label (text)
|
||||
displayed for each edge. If the attribute is "" or not present, no edge
|
||||
label is drawn.
|
||||
Or a dict keyed by edge to a string holding the label for that edge.
|
||||
edge_label_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the label options for
|
||||
each edge. For example, "[sloped,above,blue]". The default is no options.
|
||||
Or a dict keyed by edge to a string holding the label options for that edge.
|
||||
|
||||
Returns
|
||||
=======
|
||||
latex_code : string
|
||||
The text string which draws the desired graph(s) when compiled by LaTeX.
|
||||
|
||||
See Also
|
||||
========
|
||||
to_latex
|
||||
write_latex
|
||||
"""
|
||||
i4 = "\n "
|
||||
i8 = "\n "
|
||||
|
||||
# set up position dict
|
||||
# TODO allow pos to be None and use a nice TikZ default
|
||||
if not isinstance(pos, dict):
|
||||
pos = nx.get_node_attributes(G, pos)
|
||||
if not pos:
|
||||
# circular layout with radius 2
|
||||
pos = {n: f"({round(360.0 * i / len(G), 3)}:2)" for i, n in enumerate(G)}
|
||||
for node in G:
|
||||
if node not in pos:
|
||||
raise nx.NetworkXError(f"node {node} has no specified pos {pos}")
|
||||
posnode = pos[node]
|
||||
if not isinstance(posnode, str):
|
||||
try:
|
||||
posx, posy = posnode
|
||||
pos[node] = f"({round(posx, 3)}, {round(posy, 3)})"
|
||||
except (TypeError, ValueError):
|
||||
msg = f"position pos[{node}] is not 2-tuple or a string: {posnode}"
|
||||
raise nx.NetworkXError(msg)
|
||||
|
||||
# set up all the dicts
|
||||
if not isinstance(node_options, dict):
|
||||
node_options = nx.get_node_attributes(G, node_options)
|
||||
if not isinstance(node_label, dict):
|
||||
node_label = nx.get_node_attributes(G, node_label)
|
||||
if not isinstance(edge_options, dict):
|
||||
edge_options = nx.get_edge_attributes(G, edge_options)
|
||||
if not isinstance(edge_label, dict):
|
||||
edge_label = nx.get_edge_attributes(G, edge_label)
|
||||
if not isinstance(edge_label_options, dict):
|
||||
edge_label_options = nx.get_edge_attributes(G, edge_label_options)
|
||||
|
||||
# process default options (add brackets or not)
|
||||
topts = "" if tikz_options == "" else f"[{tikz_options.strip('[]')}]"
|
||||
defn = "" if default_node_options == "" else f"[{default_node_options.strip('[]')}]"
|
||||
linestyle = f"{'->' if G.is_directed() else '-'}"
|
||||
if default_edge_options == "":
|
||||
defe = "[" + linestyle + "]"
|
||||
elif "-" in default_edge_options:
|
||||
defe = default_edge_options
|
||||
else:
|
||||
defe = f"[{linestyle},{default_edge_options.strip('[]')}]"
|
||||
|
||||
# Construct the string line by line
|
||||
result = " \\begin{tikzpicture}" + topts
|
||||
result += i4 + " \\draw" + defn
|
||||
# load the nodes
|
||||
for n in G:
|
||||
# node options goes inside square brackets
|
||||
nopts = f"[{node_options[n].strip('[]')}]" if n in node_options else ""
|
||||
# node text goes inside curly brackets {}
|
||||
ntext = f"{{{node_label[n]}}}" if n in node_label else f"{{{n}}}"
|
||||
|
||||
result += i8 + f"{pos[n]} node{nopts} ({n}){ntext}"
|
||||
result += ";\n"
|
||||
|
||||
# load the edges
|
||||
result += " \\begin{scope}" + defe
|
||||
for edge in G.edges:
|
||||
u, v = edge[:2]
|
||||
e_opts = f"{edge_options[edge]}".strip("[]") if edge in edge_options else ""
|
||||
# add loop options for selfloops if not present
|
||||
if u == v and "loop" not in e_opts:
|
||||
e_opts = "loop," + e_opts
|
||||
e_opts = f"[{e_opts}]" if e_opts != "" else ""
|
||||
# TODO -- handle bending of multiedges
|
||||
|
||||
els = edge_label_options[edge] if edge in edge_label_options else ""
|
||||
# edge label options goes inside square brackets []
|
||||
els = f"[{els.strip('[]')}]"
|
||||
# edge text is drawn using the TikZ node command inside curly brackets {}
|
||||
e_label = f" node{els} {{{edge_label[edge]}}}" if edge in edge_label else ""
|
||||
|
||||
result += i8 + f"\\draw{e_opts} ({u}) to{e_label} ({v});"
|
||||
|
||||
result += "\n \\end{scope}\n \\end{tikzpicture}\n"
|
||||
return result
|
||||
|
||||
|
||||
_DOC_WRAPPER_TIKZ = r"""\documentclass{{report}}
|
||||
\usepackage{{tikz}}
|
||||
\usepackage{{subcaption}}
|
||||
|
||||
\begin{{document}}
|
||||
{content}
|
||||
\end{{document}}"""
|
||||
|
||||
|
||||
_FIG_WRAPPER = r"""\begin{{figure}}
|
||||
{content}{caption}{label}
|
||||
\end{{figure}}"""
|
||||
|
||||
|
||||
_SUBFIG_WRAPPER = r""" \begin{{subfigure}}{{{size}\textwidth}}
|
||||
{content}{caption}{label}
|
||||
\end{{subfigure}}"""
|
||||
|
||||
|
||||
def to_latex(
|
||||
Gbunch,
|
||||
pos="pos",
|
||||
tikz_options="",
|
||||
default_node_options="",
|
||||
node_options="node_options",
|
||||
node_label="node_label",
|
||||
default_edge_options="",
|
||||
edge_options="edge_options",
|
||||
edge_label="edge_label",
|
||||
edge_label_options="edge_label_options",
|
||||
caption="",
|
||||
latex_label="",
|
||||
sub_captions=None,
|
||||
sub_labels=None,
|
||||
n_rows=1,
|
||||
as_document=True,
|
||||
document_wrapper=_DOC_WRAPPER_TIKZ,
|
||||
figure_wrapper=_FIG_WRAPPER,
|
||||
subfigure_wrapper=_SUBFIG_WRAPPER,
|
||||
):
|
||||
"""Return latex code to draw the graph(s) in `Gbunch`
|
||||
|
||||
The TikZ drawing utility in LaTeX is used to draw the graph(s).
|
||||
If `Gbunch` is a graph, it is drawn in a figure environment.
|
||||
If `Gbunch` is an iterable of graphs, each is drawn in a subfigure environment
|
||||
within a single figure environment.
|
||||
|
||||
If `as_document` is True, the figure is wrapped inside a document environment
|
||||
so that the resulting string is ready to be compiled by LaTeX. Otherwise,
|
||||
the string is ready for inclusion in a larger tex document using ``\\include``
|
||||
or ``\\input`` statements.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
Gbunch : NetworkX graph or iterable of NetworkX graphs
|
||||
The NetworkX graph to be drawn or an iterable of graphs
|
||||
to be drawn inside subfigures of a single figure.
|
||||
pos : string or list of strings
|
||||
The name of the node attribute on `G` that holds the position of each node.
|
||||
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
|
||||
They can also be strings to denote positions in TikZ style, such as (x, y)
|
||||
or (angle:radius).
|
||||
If a dict, it should be keyed by node to a position.
|
||||
If an empty dict, a circular layout is computed by TikZ.
|
||||
If you are drawing many graphs in subfigures, use a list of position dicts.
|
||||
tikz_options : string
|
||||
The tikzpicture options description defining the options for the picture.
|
||||
Often large scale options like `[scale=2]`.
|
||||
default_node_options : string
|
||||
The draw options for a path of nodes. Individual node options override these.
|
||||
node_options : string or dict
|
||||
The name of the node attribute on `G` that holds the options for each node.
|
||||
Or a dict keyed by node to a string holding the options for that node.
|
||||
node_label : string or dict
|
||||
The name of the node attribute on `G` that holds the node label (text)
|
||||
displayed for each node. If the attribute is "" or not present, the node
|
||||
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
|
||||
Or a dict keyed by node to a string holding the label for that node.
|
||||
default_edge_options : string
|
||||
The options for the scope drawing all edges. The default is "[-]" for
|
||||
undirected graphs and "[->]" for directed graphs.
|
||||
edge_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the options for each edge.
|
||||
If the edge is a self-loop and ``"loop" not in edge_options`` the option
|
||||
"loop," is added to the options for the self-loop edge. Hence you can
|
||||
use "[loop above]" explicitly, but the default is "[loop]".
|
||||
Or a dict keyed by edge to a string holding the options for that edge.
|
||||
edge_label : string or dict
|
||||
The name of the edge attribute on `G` that holds the edge label (text)
|
||||
displayed for each edge. If the attribute is "" or not present, no edge
|
||||
label is drawn.
|
||||
Or a dict keyed by edge to a string holding the label for that edge.
|
||||
edge_label_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the label options for
|
||||
each edge. For example, "[sloped,above,blue]". The default is no options.
|
||||
Or a dict keyed by edge to a string holding the label options for that edge.
|
||||
caption : string
|
||||
The caption string for the figure environment
|
||||
latex_label : string
|
||||
The latex label used for the figure for easy referral from the main text
|
||||
sub_captions : list of strings
|
||||
The sub_caption string for each subfigure in the figure
|
||||
sub_latex_labels : list of strings
|
||||
The latex label for each subfigure in the figure
|
||||
n_rows : int
|
||||
The number of rows of subfigures to arrange for multiple graphs
|
||||
as_document : bool
|
||||
Whether to wrap the latex code in a document environment for compiling
|
||||
document_wrapper : formatted text string with variable ``content``.
|
||||
This text is called to evaluate the content embedded in a document
|
||||
environment with a preamble setting up TikZ.
|
||||
figure_wrapper : formatted text string
|
||||
This text is evaluated with variables ``content``, ``caption`` and ``label``.
|
||||
It wraps the content and if a caption is provided, adds the latex code for
|
||||
that caption, and if a label is provided, adds the latex code for a label.
|
||||
subfigure_wrapper : formatted text string
|
||||
This text evaluate variables ``size``, ``content``, ``caption`` and ``label``.
|
||||
It wraps the content and if a caption is provided, adds the latex code for
|
||||
that caption, and if a label is provided, adds the latex code for a label.
|
||||
The size is the vertical size of each row of subfigures as a fraction.
|
||||
|
||||
Returns
|
||||
=======
|
||||
latex_code : string
|
||||
The text string which draws the desired graph(s) when compiled by LaTeX.
|
||||
|
||||
See Also
|
||||
========
|
||||
write_latex
|
||||
to_latex_raw
|
||||
"""
|
||||
if hasattr(Gbunch, "adj"):
|
||||
raw = to_latex_raw(
|
||||
Gbunch,
|
||||
pos,
|
||||
tikz_options,
|
||||
default_node_options,
|
||||
node_options,
|
||||
node_label,
|
||||
default_edge_options,
|
||||
edge_options,
|
||||
edge_label,
|
||||
edge_label_options,
|
||||
)
|
||||
else: # iterator of graphs
|
||||
sbf = subfigure_wrapper
|
||||
size = 1 / n_rows
|
||||
|
||||
N = len(Gbunch)
|
||||
if isinstance(pos, str | dict):
|
||||
pos = [pos] * N
|
||||
if sub_captions is None:
|
||||
sub_captions = [""] * N
|
||||
if sub_labels is None:
|
||||
sub_labels = [""] * N
|
||||
if not (len(Gbunch) == len(pos) == len(sub_captions) == len(sub_labels)):
|
||||
raise nx.NetworkXError(
|
||||
"length of Gbunch, sub_captions and sub_figures must agree"
|
||||
)
|
||||
|
||||
raw = ""
|
||||
for G, pos, subcap, sublbl in zip(Gbunch, pos, sub_captions, sub_labels):
|
||||
subraw = to_latex_raw(
|
||||
G,
|
||||
pos,
|
||||
tikz_options,
|
||||
default_node_options,
|
||||
node_options,
|
||||
node_label,
|
||||
default_edge_options,
|
||||
edge_options,
|
||||
edge_label,
|
||||
edge_label_options,
|
||||
)
|
||||
cap = f" \\caption{{{subcap}}}" if subcap else ""
|
||||
lbl = f"\\label{{{sublbl}}}" if sublbl else ""
|
||||
raw += sbf.format(size=size, content=subraw, caption=cap, label=lbl)
|
||||
raw += "\n"
|
||||
|
||||
# put raw latex code into a figure environment and optionally into a document
|
||||
raw = raw[:-1]
|
||||
cap = f"\n \\caption{{{caption}}}" if caption else ""
|
||||
lbl = f"\\label{{{latex_label}}}" if latex_label else ""
|
||||
fig = figure_wrapper.format(content=raw, caption=cap, label=lbl)
|
||||
if as_document:
|
||||
return document_wrapper.format(content=fig)
|
||||
return fig
|
||||
|
||||
|
||||
@nx.utils.open_file(1, mode="w")
|
||||
def write_latex(Gbunch, path, **options):
|
||||
"""Write the latex code to draw the graph(s) onto `path`.
|
||||
|
||||
This convenience function creates the latex drawing code as a string
|
||||
and writes that to a file ready to be compiled when `as_document` is True
|
||||
or ready to be ``import`` ed or ``include`` ed into your main LaTeX document.
|
||||
|
||||
The `path` argument can be a string filename or a file handle to write to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Gbunch : NetworkX graph or iterable of NetworkX graphs
|
||||
If Gbunch is a graph, it is drawn in a figure environment.
|
||||
If Gbunch is an iterable of graphs, each is drawn in a subfigure
|
||||
environment within a single figure environment.
|
||||
path : string or file
|
||||
Filename or file handle to write to.
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
options : dict
|
||||
By default, TikZ is used with options: (others are ignored)::
|
||||
|
||||
pos : string or dict or list
|
||||
The name of the node attribute on `G` that holds the position of each node.
|
||||
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
|
||||
They can also be strings to denote positions in TikZ style, such as (x, y)
|
||||
or (angle:radius).
|
||||
If a dict, it should be keyed by node to a position.
|
||||
If an empty dict, a circular layout is computed by TikZ.
|
||||
If you are drawing many graphs in subfigures, use a list of position dicts.
|
||||
tikz_options : string
|
||||
The tikzpicture options description defining the options for the picture.
|
||||
Often large scale options like `[scale=2]`.
|
||||
default_node_options : string
|
||||
The draw options for a path of nodes. Individual node options override these.
|
||||
node_options : string or dict
|
||||
The name of the node attribute on `G` that holds the options for each node.
|
||||
Or a dict keyed by node to a string holding the options for that node.
|
||||
node_label : string or dict
|
||||
The name of the node attribute on `G` that holds the node label (text)
|
||||
displayed for each node. If the attribute is "" or not present, the node
|
||||
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
|
||||
Or a dict keyed by node to a string holding the label for that node.
|
||||
default_edge_options : string
|
||||
The options for the scope drawing all edges. The default is "[-]" for
|
||||
undirected graphs and "[->]" for directed graphs.
|
||||
edge_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the options for each edge.
|
||||
If the edge is a self-loop and ``"loop" not in edge_options`` the option
|
||||
"loop," is added to the options for the self-loop edge. Hence you can
|
||||
use "[loop above]" explicitly, but the default is "[loop]".
|
||||
Or a dict keyed by edge to a string holding the options for that edge.
|
||||
edge_label : string or dict
|
||||
The name of the edge attribute on `G` that holds the edge label (text)
|
||||
displayed for each edge. If the attribute is "" or not present, no edge
|
||||
label is drawn.
|
||||
Or a dict keyed by edge to a string holding the label for that edge.
|
||||
edge_label_options : string or dict
|
||||
The name of the edge attribute on `G` that holds the label options for
|
||||
each edge. For example, "[sloped,above,blue]". The default is no options.
|
||||
Or a dict keyed by edge to a string holding the label options for that edge.
|
||||
caption : string
|
||||
The caption string for the figure environment
|
||||
latex_label : string
|
||||
The latex label used for the figure for easy referral from the main text
|
||||
sub_captions : list of strings
|
||||
The sub_caption string for each subfigure in the figure
|
||||
sub_latex_labels : list of strings
|
||||
The latex label for each subfigure in the figure
|
||||
n_rows : int
|
||||
The number of rows of subfigures to arrange for multiple graphs
|
||||
as_document : bool
|
||||
Whether to wrap the latex code in a document environment for compiling
|
||||
document_wrapper : formatted text string with variable ``content``.
|
||||
This text is called to evaluate the content embedded in a document
|
||||
environment with a preamble setting up the TikZ syntax.
|
||||
figure_wrapper : formatted text string
|
||||
This text is evaluated with variables ``content``, ``caption`` and ``label``.
|
||||
It wraps the content and if a caption is provided, adds the latex code for
|
||||
that caption, and if a label is provided, adds the latex code for a label.
|
||||
subfigure_wrapper : formatted text string
|
||||
This text evaluate variables ``size``, ``content``, ``caption`` and ``label``.
|
||||
It wraps the content and if a caption is provided, adds the latex code for
|
||||
that caption, and if a label is provided, adds the latex code for a label.
|
||||
The size is the vertical size of each row of subfigures as a fraction.
|
||||
|
||||
See Also
|
||||
========
|
||||
to_latex
|
||||
"""
|
||||
path.write(to_latex(Gbunch, **options))
|
||||
361
venv/lib/python3.13/site-packages/networkx/drawing/nx_pydot.py
Normal file
361
venv/lib/python3.13/site-packages/networkx/drawing/nx_pydot.py
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
"""
|
||||
*****
|
||||
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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
path : string or file
|
||||
Filename or file handle for data output.
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
"""
|
||||
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 to read.
|
||||
Filenames ending in .gz or .bz2 will be decompressed.
|
||||
|
||||
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))
|
||||
|
||||
"""
|
||||
# NOTE: Pydot v3 expects a dummy argument whereas Pydot v4 doesn't
|
||||
# Remove the try-except when Pydot v4 becomes the minimum supported version
|
||||
try:
|
||||
strict = P.get_strict()
|
||||
except TypeError:
|
||||
strict = P.get_strict(None) # pydot bug: get_strict() shouldn't take argument
|
||||
multiedges = not strict
|
||||
|
||||
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 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()}
|
||||
n = str(n)
|
||||
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)
|
||||
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)
|
||||
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(H, 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)
|
||||
node = Q.get_node(pydot.quote_id_if_necessary(str_n))
|
||||
|
||||
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
|
||||
3021
venv/lib/python3.13/site-packages/networkx/drawing/nx_pylab.py
Normal file
3021
venv/lib/python3.13/site-packages/networkx/drawing/nx_pylab.py
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -0,0 +1,240 @@
|
|||
"""Unit tests for PyGraphviz interface."""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import edges_equal, graphs_equal, nodes_equal
|
||||
|
||||
pygraphviz = pytest.importorskip("pygraphviz")
|
||||
|
||||
|
||||
class TestAGraph:
|
||||
def build_graph(self, G):
|
||||
edges = [("A", "B"), ("A", "C"), ("A", "C"), ("B", "C"), ("A", "D")]
|
||||
G.add_edges_from(edges)
|
||||
G.add_node("E")
|
||||
G.graph["metal"] = "bronze"
|
||||
return G
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes(), G2.nodes())
|
||||
assert edges_equal(G1.edges(), G2.edges())
|
||||
assert G1.graph["metal"] == G2.graph["metal"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph())
|
||||
)
|
||||
def test_agraph_roundtripping(self, G, tmp_path):
|
||||
G = self.build_graph(G)
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
H = nx.nx_agraph.from_agraph(A)
|
||||
self.assert_equal(G, H)
|
||||
|
||||
fname = tmp_path / "test.dot"
|
||||
nx.drawing.nx_agraph.write_dot(H, fname)
|
||||
Hin = nx.nx_agraph.read_dot(fname)
|
||||
self.assert_equal(H, Hin)
|
||||
|
||||
fname = tmp_path / "fh_test.dot"
|
||||
with open(fname, "w") as fh:
|
||||
nx.drawing.nx_agraph.write_dot(H, fh)
|
||||
|
||||
with open(fname) as fh:
|
||||
Hin = nx.nx_agraph.read_dot(fh)
|
||||
self.assert_equal(H, Hin)
|
||||
|
||||
def test_from_agraph_name(self):
|
||||
G = nx.Graph(name="test")
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
H = nx.nx_agraph.from_agraph(A)
|
||||
assert G.name == "test"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph_class", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
|
||||
)
|
||||
def test_from_agraph_create_using(self, graph_class):
|
||||
G = nx.path_graph(3)
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
H = nx.nx_agraph.from_agraph(A, create_using=graph_class)
|
||||
assert isinstance(H, graph_class)
|
||||
|
||||
def test_from_agraph_named_edges(self):
|
||||
# Create an AGraph from an existing (non-multi) Graph
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([0, 1])
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
# Add edge (+ name, given by key) to the AGraph
|
||||
A.add_edge(0, 1, key="foo")
|
||||
# Verify a.name roundtrips out to 'key' in from_agraph
|
||||
H = nx.nx_agraph.from_agraph(A)
|
||||
assert isinstance(H, nx.Graph)
|
||||
assert ("0", "1", {"key": "foo"}) in H.edges(data=True)
|
||||
|
||||
def test_to_agraph_with_nodedata(self):
|
||||
G = nx.Graph()
|
||||
G.add_node(1, color="red")
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
assert dict(A.nodes()[0].attr) == {"color": "red"}
|
||||
|
||||
@pytest.mark.parametrize("graph_class", (nx.Graph, nx.MultiGraph))
|
||||
def test_to_agraph_with_edgedata(self, graph_class):
|
||||
G = graph_class()
|
||||
G.add_nodes_from([0, 1])
|
||||
G.add_edge(0, 1, color="yellow")
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
assert dict(A.edges()[0].attr) == {"color": "yellow"}
|
||||
|
||||
def test_view_pygraphviz_path(self, tmp_path):
|
||||
G = nx.complete_graph(3)
|
||||
input_path = str(tmp_path / "graph.png")
|
||||
out_path, A = nx.nx_agraph.view_pygraphviz(G, path=input_path, show=False)
|
||||
assert out_path == input_path
|
||||
# Ensure file is not empty
|
||||
with open(input_path, "rb") as fh:
|
||||
data = fh.read()
|
||||
assert len(data) > 0
|
||||
|
||||
def test_view_pygraphviz_file_suffix(self, tmp_path):
|
||||
G = nx.complete_graph(3)
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, suffix=1, show=False)
|
||||
assert path[-6:] == "_1.png"
|
||||
|
||||
def test_view_pygraphviz(self):
|
||||
G = nx.Graph() # "An empty graph cannot be drawn."
|
||||
pytest.raises(nx.NetworkXException, nx.nx_agraph.view_pygraphviz, G)
|
||||
G = nx.barbell_graph(4, 6)
|
||||
nx.nx_agraph.view_pygraphviz(G, show=False)
|
||||
|
||||
def test_view_pygraphviz_edgelabel(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=7)
|
||||
G.add_edge(2, 3, weight=8)
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="weight", show=False)
|
||||
for edge in A.edges():
|
||||
assert edge.attr["weight"] in ("7", "8")
|
||||
|
||||
def test_view_pygraphviz_callable_edgelabel(self):
|
||||
G = nx.complete_graph(3)
|
||||
|
||||
def foo_label(data):
|
||||
return "foo"
|
||||
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel=foo_label, show=False)
|
||||
for edge in A.edges():
|
||||
assert edge.attr["label"] == "foo"
|
||||
|
||||
def test_view_pygraphviz_multigraph_edgelabels(self):
|
||||
G = nx.MultiGraph()
|
||||
G.add_edge(0, 1, key=0, name="left_fork")
|
||||
G.add_edge(0, 1, key=1, name="right_fork")
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="name", show=False)
|
||||
edges = A.edges()
|
||||
assert len(edges) == 2
|
||||
for edge in edges:
|
||||
assert edge.attr["label"].strip() in ("left_fork", "right_fork")
|
||||
|
||||
def test_graph_with_reserved_keywords(self):
|
||||
# test attribute/keyword clash case for #1582
|
||||
# node: n
|
||||
# edges: u,v
|
||||
G = nx.Graph()
|
||||
G = self.build_graph(G)
|
||||
G.nodes["E"]["n"] = "keyword"
|
||||
G.edges[("A", "B")]["u"] = "keyword"
|
||||
G.edges[("A", "B")]["v"] = "keyword"
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
|
||||
def test_view_pygraphviz_no_added_attrs_to_input(self):
|
||||
G = nx.complete_graph(2)
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
|
||||
assert G.graph == {}
|
||||
|
||||
@pytest.mark.xfail(reason="known bug in clean_attrs")
|
||||
def test_view_pygraphviz_leaves_input_graph_unmodified(self):
|
||||
G = nx.complete_graph(2)
|
||||
# Add entries to graph dict that to_agraph handles specially
|
||||
G.graph["node"] = {"width": "0.80"}
|
||||
G.graph["edge"] = {"fontsize": "14"}
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
|
||||
assert G.graph == {"node": {"width": "0.80"}, "edge": {"fontsize": "14"}}
|
||||
|
||||
def test_graph_with_AGraph_attrs(self):
|
||||
G = nx.complete_graph(2)
|
||||
# Add entries to graph dict that to_agraph handles specially
|
||||
G.graph["node"] = {"width": "0.80"}
|
||||
G.graph["edge"] = {"fontsize": "14"}
|
||||
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
|
||||
# Ensure user-specified values are not lost
|
||||
assert dict(A.node_attr)["width"] == "0.80"
|
||||
assert dict(A.edge_attr)["fontsize"] == "14"
|
||||
|
||||
def test_round_trip_empty_graph(self):
|
||||
G = nx.Graph()
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
H = nx.nx_agraph.from_agraph(A)
|
||||
# assert graphs_equal(G, H)
|
||||
AA = nx.nx_agraph.to_agraph(H)
|
||||
HH = nx.nx_agraph.from_agraph(AA)
|
||||
assert graphs_equal(H, HH)
|
||||
G.graph["graph"] = {}
|
||||
G.graph["node"] = {}
|
||||
G.graph["edge"] = {}
|
||||
assert graphs_equal(G, HH)
|
||||
|
||||
@pytest.mark.xfail(reason="integer->string node conversion in round trip")
|
||||
def test_round_trip_integer_nodes(self):
|
||||
G = nx.complete_graph(3)
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
H = nx.nx_agraph.from_agraph(A)
|
||||
assert graphs_equal(G, H)
|
||||
|
||||
def test_graphviz_alias(self):
|
||||
G = self.build_graph(nx.Graph())
|
||||
pos_graphviz = nx.nx_agraph.graphviz_layout(G)
|
||||
pos_pygraphviz = nx.nx_agraph.pygraphviz_layout(G)
|
||||
assert pos_graphviz == pos_pygraphviz
|
||||
|
||||
@pytest.mark.parametrize("root", range(5))
|
||||
def test_pygraphviz_layout_root(self, root):
|
||||
# NOTE: test depends on layout prog being deterministic
|
||||
G = nx.complete_graph(5)
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
# Get layout with root arg is not None
|
||||
pygv_layout = nx.nx_agraph.pygraphviz_layout(G, prog="circo", root=root)
|
||||
# Equivalent layout directly on AGraph
|
||||
A.layout(args=f"-Groot={root}", prog="circo")
|
||||
# Parse AGraph layout
|
||||
a1_pos = tuple(float(v) for v in dict(A.get_node("1").attr)["pos"].split(","))
|
||||
assert pygv_layout[1] == a1_pos
|
||||
|
||||
def test_2d_layout(self):
|
||||
G = nx.Graph()
|
||||
G = self.build_graph(G)
|
||||
G.graph["dimen"] = 2
|
||||
pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
|
||||
pos = list(pos.values())
|
||||
assert len(pos) == 5
|
||||
assert len(pos[0]) == 2
|
||||
|
||||
def test_3d_layout(self):
|
||||
G = nx.Graph()
|
||||
G = self.build_graph(G)
|
||||
G.graph["dimen"] = 3
|
||||
pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
|
||||
pos = list(pos.values())
|
||||
assert len(pos) == 5
|
||||
assert len(pos[0]) == 3
|
||||
|
||||
def test_no_warnings_raised(self):
|
||||
# Test that no warnings are raised when Networkx graph
|
||||
# is converted to Pygraphviz graph and 'pos'
|
||||
# attribute is given
|
||||
G = nx.Graph()
|
||||
G.add_node(0, pos=(0, 0))
|
||||
G.add_node(1, pos=(1, 1))
|
||||
A = nx.nx_agraph.to_agraph(G)
|
||||
with warnings.catch_warnings(record=True) as record:
|
||||
A.layout()
|
||||
assert len(record) == 0
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def test_tikz_attributes():
|
||||
G = nx.path_graph(4, create_using=nx.DiGraph)
|
||||
pos = {n: (n, n) for n in G}
|
||||
|
||||
G.add_edge(0, 0)
|
||||
G.edges[(0, 0)]["label"] = "Loop"
|
||||
G.edges[(0, 0)]["label_options"] = "midway"
|
||||
|
||||
G.nodes[0]["style"] = "blue"
|
||||
G.nodes[1]["style"] = "line width=3,draw"
|
||||
G.nodes[2]["style"] = "circle,draw,blue!50"
|
||||
G.nodes[3]["label"] = "Stop"
|
||||
G.edges[(0, 1)]["label"] = "1st Step"
|
||||
G.edges[(0, 1)]["label_options"] = "near end"
|
||||
G.edges[(2, 3)]["label"] = "3rd Step"
|
||||
G.edges[(2, 3)]["label_options"] = "near start"
|
||||
G.edges[(2, 3)]["style"] = "bend left,green"
|
||||
G.edges[(1, 2)]["label"] = "2nd"
|
||||
G.edges[(1, 2)]["label_options"] = "pos=0.5"
|
||||
G.edges[(1, 2)]["style"] = ">->,bend right,line width=3,green!90"
|
||||
|
||||
output_tex = nx.to_latex(
|
||||
G,
|
||||
pos=pos,
|
||||
as_document=False,
|
||||
tikz_options="[scale=3]",
|
||||
node_options="style",
|
||||
edge_options="style",
|
||||
node_label="label",
|
||||
edge_label="label",
|
||||
edge_label_options="label_options",
|
||||
)
|
||||
expected_tex = r"""\begin{figure}
|
||||
\begin{tikzpicture}[scale=3]
|
||||
\draw
|
||||
(0, 0) node[blue] (0){0}
|
||||
(1, 1) node[line width=3,draw] (1){1}
|
||||
(2, 2) node[circle,draw,blue!50] (2){2}
|
||||
(3, 3) node (3){Stop};
|
||||
\begin{scope}[->]
|
||||
\draw (0) to node[near end] {1st Step} (1);
|
||||
\draw[loop,] (0) to node[midway] {Loop} (0);
|
||||
\draw[>->,bend right,line width=3,green!90] (1) to node[pos=0.5] {2nd} (2);
|
||||
\draw[bend left,green] (2) to node[near start] {3rd Step} (3);
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{figure}"""
|
||||
|
||||
# First, check for consistency line-by-line - if this fails, the mismatched
|
||||
# line will be shown explicitly in the failure summary
|
||||
for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")):
|
||||
assert expected == actual
|
||||
|
||||
assert output_tex == expected_tex
|
||||
|
||||
|
||||
def test_basic_multiple_graphs():
|
||||
H1 = nx.path_graph(4)
|
||||
H2 = nx.complete_graph(4)
|
||||
H3 = nx.path_graph(8)
|
||||
H4 = nx.complete_graph(8)
|
||||
captions = [
|
||||
"Path on 4 nodes",
|
||||
"Complete graph on 4 nodes",
|
||||
"Path on 8 nodes",
|
||||
"Complete graph on 8 nodes",
|
||||
]
|
||||
labels = ["fig2a", "fig2b", "fig2c", "fig2d"]
|
||||
latex_code = nx.to_latex(
|
||||
[H1, H2, H3, H4],
|
||||
n_rows=2,
|
||||
sub_captions=captions,
|
||||
sub_labels=labels,
|
||||
)
|
||||
assert "begin{document}" in latex_code
|
||||
assert "begin{figure}" in latex_code
|
||||
assert latex_code.count("begin{subfigure}") == 4
|
||||
assert latex_code.count("tikzpicture") == 8
|
||||
assert latex_code.count("[-]") == 4
|
||||
|
||||
|
||||
def test_basic_tikz():
|
||||
expected_tex = r"""\documentclass{report}
|
||||
\usepackage{tikz}
|
||||
\usepackage{subcaption}
|
||||
|
||||
\begin{document}
|
||||
\begin{figure}
|
||||
\begin{subfigure}{0.5\textwidth}
|
||||
\begin{tikzpicture}[scale=2]
|
||||
\draw[gray!90]
|
||||
(0.749, 0.702) node[red!90] (0){0}
|
||||
(1.0, -0.014) node[red!90] (1){1}
|
||||
(-0.777, -0.705) node (2){2}
|
||||
(-0.984, 0.042) node (3){3}
|
||||
(-0.028, 0.375) node[cyan!90] (4){4}
|
||||
(-0.412, 0.888) node (5){5}
|
||||
(0.448, -0.856) node (6){6}
|
||||
(0.003, -0.431) node[cyan!90] (7){7};
|
||||
\begin{scope}[->,gray!90]
|
||||
\draw (0) to (4);
|
||||
\draw (0) to (5);
|
||||
\draw (0) to (6);
|
||||
\draw (0) to (7);
|
||||
\draw (1) to (4);
|
||||
\draw (1) to (5);
|
||||
\draw (1) to (6);
|
||||
\draw (1) to (7);
|
||||
\draw (2) to (4);
|
||||
\draw (2) to (5);
|
||||
\draw (2) to (6);
|
||||
\draw (2) to (7);
|
||||
\draw (3) to (4);
|
||||
\draw (3) to (5);
|
||||
\draw (3) to (6);
|
||||
\draw (3) to (7);
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\caption{My tikz number 1 of 2}\label{tikz_1_2}
|
||||
\end{subfigure}
|
||||
\begin{subfigure}{0.5\textwidth}
|
||||
\begin{tikzpicture}[scale=2]
|
||||
\draw[gray!90]
|
||||
(0.749, 0.702) node[green!90] (0){0}
|
||||
(1.0, -0.014) node[green!90] (1){1}
|
||||
(-0.777, -0.705) node (2){2}
|
||||
(-0.984, 0.042) node (3){3}
|
||||
(-0.028, 0.375) node[purple!90] (4){4}
|
||||
(-0.412, 0.888) node (5){5}
|
||||
(0.448, -0.856) node (6){6}
|
||||
(0.003, -0.431) node[purple!90] (7){7};
|
||||
\begin{scope}[->,gray!90]
|
||||
\draw (0) to (4);
|
||||
\draw (0) to (5);
|
||||
\draw (0) to (6);
|
||||
\draw (0) to (7);
|
||||
\draw (1) to (4);
|
||||
\draw (1) to (5);
|
||||
\draw (1) to (6);
|
||||
\draw (1) to (7);
|
||||
\draw (2) to (4);
|
||||
\draw (2) to (5);
|
||||
\draw (2) to (6);
|
||||
\draw (2) to (7);
|
||||
\draw (3) to (4);
|
||||
\draw (3) to (5);
|
||||
\draw (3) to (6);
|
||||
\draw (3) to (7);
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\caption{My tikz number 2 of 2}\label{tikz_2_2}
|
||||
\end{subfigure}
|
||||
\caption{A graph generated with python and latex.}
|
||||
\end{figure}
|
||||
\end{document}"""
|
||||
|
||||
edges = [
|
||||
(0, 4),
|
||||
(0, 5),
|
||||
(0, 6),
|
||||
(0, 7),
|
||||
(1, 4),
|
||||
(1, 5),
|
||||
(1, 6),
|
||||
(1, 7),
|
||||
(2, 4),
|
||||
(2, 5),
|
||||
(2, 6),
|
||||
(2, 7),
|
||||
(3, 4),
|
||||
(3, 5),
|
||||
(3, 6),
|
||||
(3, 7),
|
||||
]
|
||||
G = nx.DiGraph()
|
||||
G.add_nodes_from(range(8))
|
||||
G.add_edges_from(edges)
|
||||
pos = {
|
||||
0: (0.7490296171687696, 0.702353520257394),
|
||||
1: (1.0, -0.014221357723796535),
|
||||
2: (-0.7765783344161441, -0.7054170966808919),
|
||||
3: (-0.9842690223417624, 0.04177547602465483),
|
||||
4: (-0.02768523817180917, 0.3745724439551441),
|
||||
5: (-0.41154855146767433, 0.8880106515525136),
|
||||
6: (0.44780153389148264, -0.8561492709269164),
|
||||
7: (0.0032499953371383505, -0.43092436645809945),
|
||||
}
|
||||
|
||||
rc_node_color = {0: "red!90", 1: "red!90", 4: "cyan!90", 7: "cyan!90"}
|
||||
gp_node_color = {0: "green!90", 1: "green!90", 4: "purple!90", 7: "purple!90"}
|
||||
|
||||
H = G.copy()
|
||||
nx.set_node_attributes(G, rc_node_color, "color")
|
||||
nx.set_node_attributes(H, gp_node_color, "color")
|
||||
|
||||
sub_captions = ["My tikz number 1 of 2", "My tikz number 2 of 2"]
|
||||
sub_labels = ["tikz_1_2", "tikz_2_2"]
|
||||
|
||||
output_tex = nx.to_latex(
|
||||
[G, H],
|
||||
[pos, pos],
|
||||
tikz_options="[scale=2]",
|
||||
default_node_options="gray!90",
|
||||
default_edge_options="gray!90",
|
||||
node_options="color",
|
||||
sub_captions=sub_captions,
|
||||
sub_labels=sub_labels,
|
||||
caption="A graph generated with python and latex.",
|
||||
n_rows=2,
|
||||
as_document=True,
|
||||
)
|
||||
|
||||
# First, check for consistency line-by-line - if this fails, the mismatched
|
||||
# line will be shown explicitly in the failure summary
|
||||
for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")):
|
||||
assert expected == actual
|
||||
# Double-check for document-level consistency
|
||||
assert output_tex == expected_tex
|
||||
|
||||
|
||||
def test_exception_pos_single_graph(to_latex=nx.to_latex):
|
||||
# smoke test that pos can be a string
|
||||
G = nx.path_graph(4)
|
||||
to_latex(G, pos="pos")
|
||||
|
||||
# must include all nodes
|
||||
pos = {0: (1, 2), 1: (0, 1), 2: (2, 1)}
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(G, pos)
|
||||
|
||||
# must have 2 values
|
||||
pos[3] = (1, 2, 3)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(G, pos)
|
||||
pos[3] = 2
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(G, pos)
|
||||
|
||||
# check that passes with 2 values
|
||||
pos[3] = (3, 2)
|
||||
to_latex(G, pos)
|
||||
|
||||
|
||||
def test_exception_multiple_graphs(to_latex=nx.to_latex):
|
||||
G = nx.path_graph(3)
|
||||
pos_bad = {0: (1, 2), 1: (0, 1)}
|
||||
pos_OK = {0: (1, 2), 1: (0, 1), 2: (2, 1)}
|
||||
fourG = [G, G, G, G]
|
||||
fourpos = [pos_OK, pos_OK, pos_OK, pos_OK]
|
||||
|
||||
# input single dict to use for all graphs
|
||||
to_latex(fourG, pos_OK)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(fourG, pos_bad)
|
||||
|
||||
# input list of dicts to use for all graphs
|
||||
to_latex(fourG, fourpos)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(fourG, [pos_bad, pos_bad, pos_bad, pos_bad])
|
||||
|
||||
# every pos dict must include all nodes
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(fourG, [pos_OK, pos_OK, pos_bad, pos_OK])
|
||||
|
||||
# test sub_captions and sub_labels (len must match Gbunch)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(fourG, fourpos, sub_captions=["hi", "hi"])
|
||||
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
to_latex(fourG, fourpos, sub_labels=["hi", "hi"])
|
||||
|
||||
# all pass
|
||||
to_latex(fourG, fourpos, sub_captions=["hi"] * 4, sub_labels=["lbl"] * 4)
|
||||
|
||||
|
||||
def test_exception_multigraph():
|
||||
G = nx.path_graph(4, create_using=nx.MultiGraph)
|
||||
G.add_edge(1, 2)
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
nx.to_latex(G)
|
||||
|
|
@ -0,0 +1,631 @@
|
|||
"""Unit tests for layout functions."""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
|
||||
class TestLayout:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.Gi = nx.grid_2d_graph(5, 5)
|
||||
cls.Gs = nx.Graph()
|
||||
nx.add_path(cls.Gs, "abcdef")
|
||||
cls.bigG = nx.grid_2d_graph(25, 25) # > 500 nodes for sparse
|
||||
|
||||
def test_spring_fixed_without_pos(self):
|
||||
G = nx.path_graph(4)
|
||||
# No pos dict at all
|
||||
with pytest.raises(ValueError, match="nodes are fixed without positions"):
|
||||
nx.spring_layout(G, fixed=[0])
|
||||
|
||||
pos = {0: (1, 1), 2: (0, 0)}
|
||||
# Node 1 not in pos dict
|
||||
with pytest.raises(ValueError, match="nodes are fixed without positions"):
|
||||
nx.spring_layout(G, fixed=[0, 1], pos=pos)
|
||||
|
||||
# All fixed nodes in pos dict
|
||||
out = nx.spring_layout(G, fixed=[0, 2], pos=pos) # No ValueError
|
||||
assert all(np.array_equal(out[n], pos[n]) for n in (0, 2))
|
||||
|
||||
def test_spring_init_pos(self):
|
||||
# Tests GH #2448
|
||||
import math
|
||||
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 0), (2, 3)])
|
||||
|
||||
init_pos = {0: (0.0, 0.0)}
|
||||
fixed_pos = [0]
|
||||
pos = nx.fruchterman_reingold_layout(G, pos=init_pos, fixed=fixed_pos)
|
||||
has_nan = any(math.isnan(c) for coords in pos.values() for c in coords)
|
||||
assert not has_nan, "values should not be nan"
|
||||
|
||||
def test_smoke_empty_graph(self):
|
||||
G = []
|
||||
nx.random_layout(G)
|
||||
nx.circular_layout(G)
|
||||
nx.planar_layout(G)
|
||||
nx.spring_layout(G)
|
||||
nx.fruchterman_reingold_layout(G)
|
||||
nx.spectral_layout(G)
|
||||
nx.shell_layout(G)
|
||||
nx.bipartite_layout(G, G)
|
||||
nx.spiral_layout(G)
|
||||
nx.multipartite_layout(G)
|
||||
nx.kamada_kawai_layout(G)
|
||||
|
||||
def test_smoke_int(self):
|
||||
G = self.Gi
|
||||
nx.random_layout(G)
|
||||
nx.circular_layout(G)
|
||||
nx.planar_layout(G)
|
||||
nx.spring_layout(G)
|
||||
nx.forceatlas2_layout(G)
|
||||
nx.fruchterman_reingold_layout(G)
|
||||
nx.fruchterman_reingold_layout(self.bigG)
|
||||
nx.spectral_layout(G)
|
||||
nx.spectral_layout(G.to_directed())
|
||||
nx.spectral_layout(self.bigG)
|
||||
nx.spectral_layout(self.bigG.to_directed())
|
||||
nx.shell_layout(G)
|
||||
nx.spiral_layout(G)
|
||||
nx.kamada_kawai_layout(G)
|
||||
nx.kamada_kawai_layout(G, dim=1)
|
||||
nx.kamada_kawai_layout(G, dim=3)
|
||||
nx.arf_layout(G)
|
||||
|
||||
def test_smoke_string(self):
|
||||
G = self.Gs
|
||||
nx.random_layout(G)
|
||||
nx.circular_layout(G)
|
||||
nx.planar_layout(G)
|
||||
nx.spring_layout(G)
|
||||
nx.forceatlas2_layout(G)
|
||||
nx.fruchterman_reingold_layout(G)
|
||||
nx.spectral_layout(G)
|
||||
nx.shell_layout(G)
|
||||
nx.spiral_layout(G)
|
||||
nx.kamada_kawai_layout(G)
|
||||
nx.kamada_kawai_layout(G, dim=1)
|
||||
nx.kamada_kawai_layout(G, dim=3)
|
||||
nx.arf_layout(G)
|
||||
|
||||
def check_scale_and_center(self, pos, scale, center):
|
||||
center = np.array(center)
|
||||
low = center - scale
|
||||
hi = center + scale
|
||||
vpos = np.array(list(pos.values()))
|
||||
length = vpos.max(0) - vpos.min(0)
|
||||
assert (length <= 2 * scale).all()
|
||||
assert (vpos >= low).all()
|
||||
assert (vpos <= hi).all()
|
||||
|
||||
def test_scale_and_center_arg(self):
|
||||
sc = self.check_scale_and_center
|
||||
c = (4, 5)
|
||||
G = nx.complete_graph(9)
|
||||
G.add_node(9)
|
||||
sc(nx.random_layout(G, center=c), scale=0.5, center=(4.5, 5.5))
|
||||
# rest can have 2*scale length: [-scale, scale]
|
||||
sc(nx.spring_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
sc(nx.spectral_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
sc(nx.circular_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
sc(nx.shell_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
sc(nx.spiral_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
sc(nx.kamada_kawai_layout(G, scale=2, center=c), scale=2, center=c)
|
||||
|
||||
c = (2, 3, 5)
|
||||
sc(nx.kamada_kawai_layout(G, dim=3, scale=2, center=c), scale=2, center=c)
|
||||
|
||||
def test_planar_layout_non_planar_input(self):
|
||||
G = nx.complete_graph(9)
|
||||
pytest.raises(nx.NetworkXException, nx.planar_layout, G)
|
||||
|
||||
def test_smoke_planar_layout_embedding_input(self):
|
||||
embedding = nx.PlanarEmbedding()
|
||||
embedding.set_data({0: [1, 2], 1: [0, 2], 2: [0, 1]})
|
||||
nx.planar_layout(embedding)
|
||||
|
||||
def test_default_scale_and_center(self):
|
||||
sc = self.check_scale_and_center
|
||||
c = (0, 0)
|
||||
G = nx.complete_graph(9)
|
||||
G.add_node(9)
|
||||
sc(nx.random_layout(G), scale=0.5, center=(0.5, 0.5))
|
||||
sc(nx.spring_layout(G), scale=1, center=c)
|
||||
sc(nx.spectral_layout(G), scale=1, center=c)
|
||||
sc(nx.circular_layout(G), scale=1, center=c)
|
||||
sc(nx.shell_layout(G), scale=1, center=c)
|
||||
sc(nx.spiral_layout(G), scale=1, center=c)
|
||||
sc(nx.kamada_kawai_layout(G), scale=1, center=c)
|
||||
|
||||
c = (0, 0, 0)
|
||||
sc(nx.kamada_kawai_layout(G, dim=3), scale=1, center=c)
|
||||
|
||||
def test_circular_planar_and_shell_dim_error(self):
|
||||
G = nx.path_graph(4)
|
||||
pytest.raises(ValueError, nx.circular_layout, G, dim=1)
|
||||
pytest.raises(ValueError, nx.shell_layout, G, dim=1)
|
||||
pytest.raises(ValueError, nx.shell_layout, G, dim=3)
|
||||
pytest.raises(ValueError, nx.planar_layout, G, dim=1)
|
||||
pytest.raises(ValueError, nx.planar_layout, G, dim=3)
|
||||
|
||||
def test_adjacency_interface_numpy(self):
|
||||
A = nx.to_numpy_array(self.Gs)
|
||||
pos = nx.drawing.layout._fruchterman_reingold(A)
|
||||
assert pos.shape == (6, 2)
|
||||
pos = nx.drawing.layout._fruchterman_reingold(A, dim=3)
|
||||
assert pos.shape == (6, 3)
|
||||
pos = nx.drawing.layout._sparse_fruchterman_reingold(A)
|
||||
assert pos.shape == (6, 2)
|
||||
|
||||
def test_adjacency_interface_scipy(self):
|
||||
A = nx.to_scipy_sparse_array(self.Gs, dtype="d")
|
||||
pos = nx.drawing.layout._sparse_fruchterman_reingold(A)
|
||||
assert pos.shape == (6, 2)
|
||||
pos = nx.drawing.layout._sparse_spectral(A)
|
||||
assert pos.shape == (6, 2)
|
||||
pos = nx.drawing.layout._sparse_fruchterman_reingold(A, dim=3)
|
||||
assert pos.shape == (6, 3)
|
||||
|
||||
def test_single_nodes(self):
|
||||
G = nx.path_graph(1)
|
||||
vpos = nx.shell_layout(G)
|
||||
assert not vpos[0].any()
|
||||
G = nx.path_graph(4)
|
||||
vpos = nx.shell_layout(G, [[0], [1, 2], [3]])
|
||||
assert not vpos[0].any()
|
||||
assert vpos[3].any() # ensure node 3 not at origin (#3188)
|
||||
assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753)
|
||||
vpos = nx.shell_layout(G, [[0], [1, 2], [3]], rotate=0)
|
||||
assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753)
|
||||
|
||||
def test_smoke_initial_pos_forceatlas2(self):
|
||||
pos = nx.circular_layout(self.Gi)
|
||||
npos = nx.forceatlas2_layout(self.Gi, pos=pos)
|
||||
|
||||
def test_smoke_initial_pos_fruchterman_reingold(self):
|
||||
pos = nx.circular_layout(self.Gi)
|
||||
npos = nx.fruchterman_reingold_layout(self.Gi, pos=pos)
|
||||
|
||||
def test_smoke_initial_pos_arf(self):
|
||||
pos = nx.circular_layout(self.Gi)
|
||||
npos = nx.arf_layout(self.Gi, pos=pos)
|
||||
|
||||
def test_fixed_node_fruchterman_reingold(self):
|
||||
# Dense version (numpy based)
|
||||
pos = nx.circular_layout(self.Gi)
|
||||
npos = nx.spring_layout(self.Gi, pos=pos, fixed=[(0, 0)])
|
||||
assert tuple(pos[(0, 0)]) == tuple(npos[(0, 0)])
|
||||
# Sparse version (scipy based)
|
||||
pos = nx.circular_layout(self.bigG)
|
||||
npos = nx.spring_layout(self.bigG, pos=pos, fixed=[(0, 0)])
|
||||
for axis in range(2):
|
||||
assert pos[(0, 0)][axis] == pytest.approx(npos[(0, 0)][axis], abs=1e-7)
|
||||
|
||||
def test_center_parameter(self):
|
||||
G = nx.path_graph(1)
|
||||
nx.random_layout(G, center=(1, 1))
|
||||
vpos = nx.circular_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.planar_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.spring_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.fruchterman_reingold_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.spectral_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.shell_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
vpos = nx.spiral_layout(G, center=(1, 1))
|
||||
assert tuple(vpos[0]) == (1, 1)
|
||||
|
||||
def test_center_wrong_dimensions(self):
|
||||
G = nx.path_graph(1)
|
||||
assert id(nx.spring_layout) == id(nx.fruchterman_reingold_layout)
|
||||
pytest.raises(ValueError, nx.random_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.circular_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.planar_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.spring_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.spring_layout, G, dim=3, center=(1, 1))
|
||||
pytest.raises(ValueError, nx.spectral_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.spectral_layout, G, dim=3, center=(1, 1))
|
||||
pytest.raises(ValueError, nx.shell_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.spiral_layout, G, center=(1, 1, 1))
|
||||
pytest.raises(ValueError, nx.kamada_kawai_layout, G, center=(1, 1, 1))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = nx.empty_graph()
|
||||
vpos = nx.random_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.circular_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.planar_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.bipartite_layout(G, G)
|
||||
assert vpos == {}
|
||||
vpos = nx.spring_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.fruchterman_reingold_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.spectral_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.shell_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.spiral_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.multipartite_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.kamada_kawai_layout(G, center=(1, 1))
|
||||
assert vpos == {}
|
||||
vpos = nx.forceatlas2_layout(G)
|
||||
assert vpos == {}
|
||||
vpos = nx.arf_layout(G)
|
||||
assert vpos == {}
|
||||
|
||||
def test_bipartite_layout(self):
|
||||
G = nx.complete_bipartite_graph(3, 5)
|
||||
top, bottom = nx.bipartite.sets(G)
|
||||
|
||||
vpos = nx.bipartite_layout(G, top)
|
||||
assert len(vpos) == len(G)
|
||||
|
||||
top_x = vpos[list(top)[0]][0]
|
||||
bottom_x = vpos[list(bottom)[0]][0]
|
||||
for node in top:
|
||||
assert vpos[node][0] == top_x
|
||||
for node in bottom:
|
||||
assert vpos[node][0] == bottom_x
|
||||
|
||||
vpos = nx.bipartite_layout(
|
||||
G, top, align="horizontal", center=(2, 2), scale=2, aspect_ratio=1
|
||||
)
|
||||
assert len(vpos) == len(G)
|
||||
|
||||
top_y = vpos[list(top)[0]][1]
|
||||
bottom_y = vpos[list(bottom)[0]][1]
|
||||
for node in top:
|
||||
assert vpos[node][1] == top_y
|
||||
for node in bottom:
|
||||
assert vpos[node][1] == bottom_y
|
||||
|
||||
pytest.raises(ValueError, nx.bipartite_layout, G, top, align="foo")
|
||||
|
||||
def test_multipartite_layout(self):
|
||||
sizes = (0, 5, 7, 2, 8)
|
||||
G = nx.complete_multipartite_graph(*sizes)
|
||||
|
||||
vpos = nx.multipartite_layout(G)
|
||||
assert len(vpos) == len(G)
|
||||
|
||||
start = 0
|
||||
for n in sizes:
|
||||
end = start + n
|
||||
assert all(vpos[start][0] == vpos[i][0] for i in range(start + 1, end))
|
||||
start += n
|
||||
|
||||
vpos = nx.multipartite_layout(G, align="horizontal", scale=2, center=(2, 2))
|
||||
assert len(vpos) == len(G)
|
||||
|
||||
start = 0
|
||||
for n in sizes:
|
||||
end = start + n
|
||||
assert all(vpos[start][1] == vpos[i][1] for i in range(start + 1, end))
|
||||
start += n
|
||||
|
||||
pytest.raises(ValueError, nx.multipartite_layout, G, align="foo")
|
||||
|
||||
def test_kamada_kawai_costfn_1d(self):
|
||||
costfn = nx.drawing.layout._kamada_kawai_costfn
|
||||
|
||||
pos = np.array([4.0, 7.0])
|
||||
invdist = 1 / np.array([[0.1, 2.0], [2.0, 0.3]])
|
||||
|
||||
cost, grad = costfn(pos, np, invdist, meanweight=0, dim=1)
|
||||
|
||||
assert cost == pytest.approx(((3 / 2.0 - 1) ** 2), abs=1e-7)
|
||||
assert grad[0] == pytest.approx((-0.5), abs=1e-7)
|
||||
assert grad[1] == pytest.approx(0.5, abs=1e-7)
|
||||
|
||||
def check_kamada_kawai_costfn(self, pos, invdist, meanwt, dim):
|
||||
costfn = nx.drawing.layout._kamada_kawai_costfn
|
||||
|
||||
cost, grad = costfn(pos.ravel(), np, invdist, meanweight=meanwt, dim=dim)
|
||||
|
||||
expected_cost = 0.5 * meanwt * np.sum(np.sum(pos, axis=0) ** 2)
|
||||
for i in range(pos.shape[0]):
|
||||
for j in range(i + 1, pos.shape[0]):
|
||||
diff = np.linalg.norm(pos[i] - pos[j])
|
||||
expected_cost += (diff * invdist[i][j] - 1.0) ** 2
|
||||
|
||||
assert cost == pytest.approx(expected_cost, abs=1e-7)
|
||||
|
||||
dx = 1e-4
|
||||
for nd in range(pos.shape[0]):
|
||||
for dm in range(pos.shape[1]):
|
||||
idx = nd * pos.shape[1] + dm
|
||||
ps = pos.flatten()
|
||||
|
||||
ps[idx] += dx
|
||||
cplus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0]
|
||||
|
||||
ps[idx] -= 2 * dx
|
||||
cminus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0]
|
||||
|
||||
assert grad[idx] == pytest.approx((cplus - cminus) / (2 * dx), abs=1e-5)
|
||||
|
||||
def test_kamada_kawai_costfn(self):
|
||||
invdist = 1 / np.array([[0.1, 2.1, 1.7], [2.1, 0.2, 0.6], [1.7, 0.6, 0.3]])
|
||||
meanwt = 0.3
|
||||
|
||||
# 2d
|
||||
pos = np.array([[1.3, -3.2], [2.7, -0.3], [5.1, 2.5]])
|
||||
|
||||
self.check_kamada_kawai_costfn(pos, invdist, meanwt, 2)
|
||||
|
||||
# 3d
|
||||
pos = np.array([[0.9, 8.6, -8.7], [-10, -0.5, -7.1], [9.1, -8.1, 1.6]])
|
||||
|
||||
self.check_kamada_kawai_costfn(pos, invdist, meanwt, 3)
|
||||
|
||||
def test_spiral_layout(self):
|
||||
G = self.Gs
|
||||
|
||||
# a lower value of resolution should result in a more compact layout
|
||||
# intuitively, the total distance from the start and end nodes
|
||||
# via each node in between (transiting through each) will be less,
|
||||
# assuming rescaling does not occur on the computed node positions
|
||||
pos_standard = np.array(list(nx.spiral_layout(G, resolution=0.35).values()))
|
||||
pos_tighter = np.array(list(nx.spiral_layout(G, resolution=0.34).values()))
|
||||
distances = np.linalg.norm(pos_standard[:-1] - pos_standard[1:], axis=1)
|
||||
distances_tighter = np.linalg.norm(pos_tighter[:-1] - pos_tighter[1:], axis=1)
|
||||
assert sum(distances) > sum(distances_tighter)
|
||||
|
||||
# return near-equidistant points after the first value if set to true
|
||||
pos_equidistant = np.array(list(nx.spiral_layout(G, equidistant=True).values()))
|
||||
distances_equidistant = np.linalg.norm(
|
||||
pos_equidistant[:-1] - pos_equidistant[1:], axis=1
|
||||
)
|
||||
assert np.allclose(
|
||||
distances_equidistant[1:], distances_equidistant[-1], atol=0.01
|
||||
)
|
||||
|
||||
def test_spiral_layout_equidistant(self):
|
||||
G = nx.path_graph(10)
|
||||
nx.spiral_layout(G, equidistant=True, store_pos_as="pos")
|
||||
pos = nx.get_node_attributes(G, "pos")
|
||||
# Extract individual node positions as an array
|
||||
p = np.array(list(pos.values()))
|
||||
# Elementwise-distance between node positions
|
||||
dist = np.linalg.norm(p[1:] - p[:-1], axis=1)
|
||||
assert np.allclose(np.diff(dist), 0, atol=1e-3)
|
||||
|
||||
def test_forceatlas2_layout_partial_input_test(self):
|
||||
# check whether partial pos input still returns a full proper position
|
||||
G = self.Gs
|
||||
node = nx.utils.arbitrary_element(G)
|
||||
pos = nx.circular_layout(G)
|
||||
del pos[node]
|
||||
pos = nx.forceatlas2_layout(G, pos=pos)
|
||||
assert len(pos) == len(G)
|
||||
|
||||
def test_rescale_layout_dict(self):
|
||||
G = nx.empty_graph()
|
||||
vpos = nx.random_layout(G, center=(1, 1))
|
||||
assert nx.rescale_layout_dict(vpos) == {}
|
||||
|
||||
G = nx.empty_graph(2)
|
||||
vpos = {0: (0.0, 0.0), 1: (1.0, 1.0)}
|
||||
s_vpos = nx.rescale_layout_dict(vpos)
|
||||
assert np.linalg.norm([sum(x) for x in zip(*s_vpos.values())]) < 1e-6
|
||||
|
||||
G = nx.empty_graph(3)
|
||||
vpos = {0: (0, 0), 1: (1, 1), 2: (0.5, 0.5)}
|
||||
s_vpos = nx.rescale_layout_dict(vpos)
|
||||
|
||||
expectation = {
|
||||
0: np.array((-1, -1)),
|
||||
1: np.array((1, 1)),
|
||||
2: np.array((0, 0)),
|
||||
}
|
||||
for k, v in expectation.items():
|
||||
assert (s_vpos[k] == v).all()
|
||||
s_vpos = nx.rescale_layout_dict(vpos, scale=2)
|
||||
expectation = {
|
||||
0: np.array((-2, -2)),
|
||||
1: np.array((2, 2)),
|
||||
2: np.array((0, 0)),
|
||||
}
|
||||
for k, v in expectation.items():
|
||||
assert (s_vpos[k] == v).all()
|
||||
|
||||
def test_arf_layout_partial_input_test(self):
|
||||
# Checks whether partial pos input still returns a proper position.
|
||||
G = self.Gs
|
||||
node = nx.utils.arbitrary_element(G)
|
||||
pos = nx.circular_layout(G)
|
||||
del pos[node]
|
||||
pos = nx.arf_layout(G, pos=pos)
|
||||
assert len(pos) == len(G)
|
||||
|
||||
def test_arf_layout_negative_a_check(self):
|
||||
"""
|
||||
Checks input parameters correctly raises errors. For example, `a` should be larger than 1
|
||||
"""
|
||||
G = self.Gs
|
||||
pytest.raises(ValueError, nx.arf_layout, G=G, a=-1)
|
||||
|
||||
def test_smoke_seed_input(self):
|
||||
G = self.Gs
|
||||
nx.random_layout(G, seed=42)
|
||||
nx.spring_layout(G, seed=42)
|
||||
nx.arf_layout(G, seed=42)
|
||||
nx.forceatlas2_layout(G, seed=42)
|
||||
|
||||
def test_node_at_center(self):
|
||||
# see gh-7791 avoid divide by zero
|
||||
G = nx.path_graph(3)
|
||||
orig_pos = {i: [i - 1, 0.0] for i in range(3)}
|
||||
new_pos = nx.forceatlas2_layout(G, pos=orig_pos)
|
||||
|
||||
def test_initial_only_some_pos(self):
|
||||
G = nx.path_graph(3)
|
||||
orig_pos = {i: [i - 1, 0.0] for i in range(2)}
|
||||
new_pos = nx.forceatlas2_layout(G, pos=orig_pos, seed=42)
|
||||
|
||||
|
||||
def test_multipartite_layout_nonnumeric_partition_labels():
|
||||
"""See gh-5123."""
|
||||
G = nx.Graph()
|
||||
G.add_node(0, subset="s0")
|
||||
G.add_node(1, subset="s0")
|
||||
G.add_node(2, subset="s1")
|
||||
G.add_node(3, subset="s1")
|
||||
G.add_edges_from([(0, 2), (0, 3), (1, 2)])
|
||||
pos = nx.multipartite_layout(G)
|
||||
assert len(pos) == len(G)
|
||||
|
||||
|
||||
def test_multipartite_layout_layer_order():
|
||||
"""Return the layers in sorted order if the layers of the multipartite
|
||||
graph are sortable. See gh-5691"""
|
||||
G = nx.Graph()
|
||||
node_group = dict(zip(("a", "b", "c", "d", "e"), (2, 3, 1, 2, 4)))
|
||||
for node, layer in node_group.items():
|
||||
G.add_node(node, subset=layer)
|
||||
|
||||
# Horizontal alignment, therefore y-coord determines layers
|
||||
pos = nx.multipartite_layout(G, align="horizontal")
|
||||
|
||||
layers = nx.utils.groups(node_group)
|
||||
pos_from_layers = nx.multipartite_layout(G, align="horizontal", subset_key=layers)
|
||||
for (n1, p1), (n2, p2) in zip(pos.items(), pos_from_layers.items()):
|
||||
assert n1 == n2 and (p1 == p2).all()
|
||||
|
||||
# Nodes "a" and "d" are in the same layer
|
||||
assert pos["a"][-1] == pos["d"][-1]
|
||||
# positions should be sorted according to layer
|
||||
assert pos["c"][-1] < pos["a"][-1] < pos["b"][-1] < pos["e"][-1]
|
||||
|
||||
# Make sure that multipartite_layout still works when layers are not sortable
|
||||
G.nodes["a"]["subset"] = "layer_0" # Can't sort mixed strs/ints
|
||||
pos_nosort = nx.multipartite_layout(G) # smoke test: this should not raise
|
||||
assert pos_nosort.keys() == pos.keys()
|
||||
|
||||
|
||||
def _num_nodes_per_bfs_layer(pos):
|
||||
"""Helper function to extract the number of nodes in each layer of bfs_layout"""
|
||||
x = np.array(list(pos.values()))[:, 0] # node positions in layered dimension
|
||||
_, layer_count = np.unique(x, return_counts=True)
|
||||
return layer_count
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", range(2, 7))
|
||||
def test_bfs_layout_complete_graph(n):
|
||||
"""The complete graph should result in two layers: the starting node and
|
||||
a second layer containing all neighbors."""
|
||||
G = nx.complete_graph(n)
|
||||
nx.bfs_layout(G, start=0, store_pos_as="pos")
|
||||
pos = nx.get_node_attributes(G, "pos")
|
||||
assert np.array_equal(_num_nodes_per_bfs_layer(pos), [1, n - 1])
|
||||
|
||||
|
||||
def test_bfs_layout_barbell():
|
||||
G = nx.barbell_graph(5, 3)
|
||||
# Start in one of the "bells"
|
||||
pos = nx.bfs_layout(G, start=0)
|
||||
# start, bell-1, [1] * len(bar)+1, bell-1
|
||||
expected_nodes_per_layer = [1, 4, 1, 1, 1, 1, 4]
|
||||
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
|
||||
# Start in the other "bell" - expect same layer pattern
|
||||
pos = nx.bfs_layout(G, start=12)
|
||||
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
|
||||
# Starting in the center of the bar, expect layers to be symmetric
|
||||
pos = nx.bfs_layout(G, start=6)
|
||||
# Expected layers: {6 (start)}, {5, 7}, {4, 8}, {8 nodes from remainder of bells}
|
||||
expected_nodes_per_layer = [1, 2, 2, 8]
|
||||
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
|
||||
|
||||
|
||||
def test_bfs_layout_disconnected():
|
||||
G = nx.complete_graph(5)
|
||||
G.add_edges_from([(10, 11), (11, 12)])
|
||||
with pytest.raises(nx.NetworkXError, match="bfs_layout didn't include all nodes"):
|
||||
nx.bfs_layout(G, start=0)
|
||||
|
||||
|
||||
def test_bipartite_layout_default_nodes_raises_non_bipartite_input():
|
||||
G = nx.complete_graph(5)
|
||||
with pytest.raises(nx.NetworkXError, match="Graph is not bipartite"):
|
||||
nx.bipartite_layout(G)
|
||||
# No exception if nodes are explicitly specified
|
||||
pos = nx.bipartite_layout(G, nodes=[2, 3])
|
||||
|
||||
|
||||
def test_bipartite_layout_default_nodes():
|
||||
G = nx.complete_bipartite_graph(3, 3)
|
||||
pos = nx.bipartite_layout(G) # no nodes specified
|
||||
# X coords of nodes should be the same within the bipartite sets
|
||||
for nodeset in nx.bipartite.sets(G):
|
||||
xs = [pos[k][0] for k in nodeset]
|
||||
assert all(x == pytest.approx(xs[0]) for x in xs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"layout",
|
||||
[
|
||||
nx.random_layout,
|
||||
nx.circular_layout,
|
||||
nx.shell_layout,
|
||||
nx.spring_layout,
|
||||
nx.kamada_kawai_layout,
|
||||
nx.spectral_layout,
|
||||
nx.planar_layout,
|
||||
nx.spiral_layout,
|
||||
nx.forceatlas2_layout,
|
||||
],
|
||||
)
|
||||
def test_layouts_negative_dim(layout):
|
||||
"""Test all layouts that support dim kwarg handle invalid inputs."""
|
||||
G = nx.path_graph(4)
|
||||
valid_err_msgs = "|".join(
|
||||
[
|
||||
"negative dimensions.*not allowed",
|
||||
"can only handle 2",
|
||||
"cannot handle.*2",
|
||||
]
|
||||
)
|
||||
with pytest.raises(ValueError, match=valid_err_msgs):
|
||||
layout(G, dim=-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num_nodes", "expected_method"), [(100, "force"), (501, "energy")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"extra_layout_kwargs",
|
||||
[
|
||||
{}, # No extra kwargs
|
||||
{"pos": {0: (0, 0)}, "fixed": [0]}, # Fixed node position
|
||||
{"dim": 3}, # 3D layout
|
||||
],
|
||||
)
|
||||
def test_spring_layout_graph_size_heuristic(
|
||||
num_nodes, expected_method, extra_layout_kwargs
|
||||
):
|
||||
"""Expect 'force' layout for n < 500 and 'energy' for n >= 500"""
|
||||
G = nx.cycle_graph(num_nodes)
|
||||
# Seeded layout to compare explicit method to one determined by "auto"
|
||||
seed = 163674319
|
||||
|
||||
# Compare explicit method to auto method
|
||||
expected = nx.spring_layout(
|
||||
G, method=expected_method, seed=seed, **extra_layout_kwargs
|
||||
)
|
||||
actual = nx.spring_layout(G, method="auto", seed=seed, **extra_layout_kwargs)
|
||||
assert np.allclose(list(expected.values()), list(actual.values()), atol=1e-5)
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
"""Unit tests for pydot drawing functions."""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import graphs_equal
|
||||
|
||||
pydot = pytest.importorskip("pydot")
|
||||
|
||||
|
||||
class TestPydot:
|
||||
@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
|
||||
@pytest.mark.parametrize("prog", ("neato", "dot"))
|
||||
def test_pydot(self, G, prog, tmp_path):
|
||||
"""
|
||||
Validate :mod:`pydot`-based usage of the passed NetworkX graph with the
|
||||
passed basename of an external GraphViz command (e.g., `dot`, `neato`).
|
||||
"""
|
||||
|
||||
# Set the name of this graph to... "G". Failing to do so will
|
||||
# subsequently trip an assertion expecting this name.
|
||||
G.graph["name"] = "G"
|
||||
|
||||
# Add arbitrary nodes and edges to the passed empty graph.
|
||||
G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("A", "D")])
|
||||
G.add_node("E")
|
||||
|
||||
# Validate layout of this graph with the passed GraphViz command.
|
||||
graph_layout = nx.nx_pydot.pydot_layout(G, prog=prog)
|
||||
assert isinstance(graph_layout, dict)
|
||||
|
||||
# Convert this graph into a "pydot.Dot" instance.
|
||||
P = nx.nx_pydot.to_pydot(G)
|
||||
|
||||
# Convert this "pydot.Dot" instance back into a graph of the same type.
|
||||
G2 = G.__class__(nx.nx_pydot.from_pydot(P))
|
||||
|
||||
# Validate the original and resulting graphs to be the same.
|
||||
assert graphs_equal(G, G2)
|
||||
|
||||
fname = tmp_path / "out.dot"
|
||||
|
||||
# Serialize this "pydot.Dot" instance to a temporary file in dot format
|
||||
P.write_raw(fname)
|
||||
|
||||
# Deserialize a list of new "pydot.Dot" instances back from this file.
|
||||
Pin_list = pydot.graph_from_dot_file(path=fname, encoding="utf-8")
|
||||
|
||||
# Validate this file to contain only one graph.
|
||||
assert len(Pin_list) == 1
|
||||
|
||||
# The single "pydot.Dot" instance deserialized from this file.
|
||||
Pin = Pin_list[0]
|
||||
|
||||
# Sorted list of all nodes in the original "pydot.Dot" instance.
|
||||
n1 = sorted(p.get_name() for p in P.get_node_list())
|
||||
|
||||
# Sorted list of all nodes in the deserialized "pydot.Dot" instance.
|
||||
n2 = sorted(p.get_name() for p in Pin.get_node_list())
|
||||
|
||||
# Validate these instances to contain the same nodes.
|
||||
assert n1 == n2
|
||||
|
||||
# Sorted list of all edges in the original "pydot.Dot" instance.
|
||||
e1 = sorted((e.get_source(), e.get_destination()) for e in P.get_edge_list())
|
||||
|
||||
# Sorted list of all edges in the original "pydot.Dot" instance.
|
||||
e2 = sorted((e.get_source(), e.get_destination()) for e in Pin.get_edge_list())
|
||||
|
||||
# Validate these instances to contain the same edges.
|
||||
assert e1 == e2
|
||||
|
||||
# Deserialize a new graph of the same type back from this file.
|
||||
Hin = nx.nx_pydot.read_dot(fname)
|
||||
Hin = G.__class__(Hin)
|
||||
|
||||
# Validate the original and resulting graphs to be the same.
|
||||
assert graphs_equal(G, Hin)
|
||||
|
||||
def test_read_write(self):
|
||||
G = nx.MultiGraph()
|
||||
G.graph["name"] = "G"
|
||||
G.add_edge("1", "2", key="0") # read assumes strings
|
||||
fh = StringIO()
|
||||
nx.nx_pydot.write_dot(G, fh)
|
||||
fh.seek(0)
|
||||
H = nx.nx_pydot.read_dot(fh)
|
||||
assert graphs_equal(G, H)
|
||||
|
||||
|
||||
def test_pydot_issue_7581(tmp_path):
|
||||
"""Validate that `nx_pydot.pydot_layout` handles nodes
|
||||
with characters like "\n", " ".
|
||||
|
||||
Those characters cause `pydot` to escape and quote them on output,
|
||||
which caused #7581.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([("A\nbig test", "B"), ("A\nbig test", "C"), ("B", "C")])
|
||||
|
||||
graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot")
|
||||
assert isinstance(graph_layout, dict)
|
||||
|
||||
# Convert the graph to pydot and back into a graph. There should be no difference.
|
||||
P = nx.nx_pydot.to_pydot(G)
|
||||
G2 = nx.Graph(nx.nx_pydot.from_pydot(P))
|
||||
assert graphs_equal(G, G2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
|
||||
)
|
||||
def test_hashable_pydot(graph_type):
|
||||
# gh-5790
|
||||
G = graph_type()
|
||||
G.add_edge("5", frozenset([1]), t='"Example:A"', l=False)
|
||||
G.add_edge("1", 2, w=True, t=("node1",), l=frozenset(["node1"]))
|
||||
G.add_edge("node", (3, 3), w="string")
|
||||
|
||||
assert [
|
||||
{"t": '"Example:A"', "l": "False"},
|
||||
{"w": "True", "t": "('node1',)", "l": "frozenset({'node1'})"},
|
||||
{"w": "string"},
|
||||
] == [
|
||||
attr
|
||||
for _, _, attr in nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).edges.data()
|
||||
]
|
||||
|
||||
assert {str(i) for i in G.nodes()} == set(
|
||||
nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).nodes
|
||||
)
|
||||
|
||||
|
||||
def test_pydot_numerical_name():
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([("A", "B"), (0, 1)])
|
||||
graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot")
|
||||
assert isinstance(graph_layout, dict)
|
||||
assert "0" not in graph_layout
|
||||
assert 0 in graph_layout
|
||||
assert "1" not in graph_layout
|
||||
assert 1 in graph_layout
|
||||
assert "A" in graph_layout
|
||||
assert "B" in graph_layout
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue