rm CondaPkg environment
This commit is contained in:
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
.. warning::
|
||||
This parser uses the standard xml library present in Python, which is
|
||||
insecure - see :doc:`library/xml` for additional information.
|
||||
insecure - see :external+python:mod:`xml` for additional information.
|
||||
Only parse GEFX files you trust.
|
||||
|
||||
GEXF (Graph Exchange XML Format) is a language for describing complex
|
||||
@@ -177,27 +177,32 @@ def read_gexf(path, node_type=None, relabel=False, version="1.2draft"):
|
||||
|
||||
|
||||
class GEXF:
|
||||
versions = {}
|
||||
d = {
|
||||
"NS_GEXF": "http://www.gexf.net/1.1draft",
|
||||
"NS_VIZ": "http://www.gexf.net/1.1draft/viz",
|
||||
"NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"SCHEMALOCATION": " ".join(
|
||||
["http://www.gexf.net/1.1draft", "http://www.gexf.net/1.1draft/gexf.xsd"]
|
||||
),
|
||||
"VERSION": "1.1",
|
||||
versions = {
|
||||
"1.1draft": {
|
||||
"NS_GEXF": "http://www.gexf.net/1.1draft",
|
||||
"NS_VIZ": "http://www.gexf.net/1.1draft/viz",
|
||||
"NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"SCHEMALOCATION": " ".join(
|
||||
[
|
||||
"http://www.gexf.net/1.1draft",
|
||||
"http://www.gexf.net/1.1draft/gexf.xsd",
|
||||
]
|
||||
),
|
||||
"VERSION": "1.1",
|
||||
},
|
||||
"1.2draft": {
|
||||
"NS_GEXF": "http://www.gexf.net/1.2draft",
|
||||
"NS_VIZ": "http://www.gexf.net/1.2draft/viz",
|
||||
"NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"SCHEMALOCATION": " ".join(
|
||||
[
|
||||
"http://www.gexf.net/1.2draft",
|
||||
"http://www.gexf.net/1.2draft/gexf.xsd",
|
||||
]
|
||||
),
|
||||
"VERSION": "1.2",
|
||||
},
|
||||
}
|
||||
versions["1.1draft"] = d
|
||||
d = {
|
||||
"NS_GEXF": "http://www.gexf.net/1.2draft",
|
||||
"NS_VIZ": "http://www.gexf.net/1.2draft/viz",
|
||||
"NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"SCHEMALOCATION": " ".join(
|
||||
["http://www.gexf.net/1.2draft", "http://www.gexf.net/1.2draft/gexf.xsd"]
|
||||
),
|
||||
"VERSION": "1.2",
|
||||
}
|
||||
versions["1.2draft"] = d
|
||||
|
||||
def construct_types(self):
|
||||
types = [
|
||||
@@ -564,7 +569,7 @@ class GEXFWriter(GEXF):
|
||||
r=str(color.get("r")),
|
||||
g=str(color.get("g")),
|
||||
b=str(color.get("b")),
|
||||
a=str(color.get("a")),
|
||||
a=str(color.get("a", 1.0)),
|
||||
)
|
||||
element.append(e)
|
||||
|
||||
|
||||
@@ -365,9 +365,9 @@ def parse_gml_lines(lines, label, destringizer):
|
||||
pass
|
||||
# Special handling for empty lists and tuples
|
||||
if value == "()":
|
||||
value = tuple()
|
||||
value = ()
|
||||
if value == "[]":
|
||||
value = list()
|
||||
value = []
|
||||
curr_token = next(tokens)
|
||||
elif category == Pattern.DICT_START:
|
||||
curr_token, value = parse_dict(curr_token)
|
||||
|
||||
@@ -121,7 +121,7 @@ def from_graph6_bytes(bytes_in):
|
||||
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(range(n))
|
||||
for (i, j), b in zip([(i, j) for j in range(1, n) for i in range(j)], bits()):
|
||||
for (i, j), b in zip(((i, j) for j in range(1, n) for i in range(j)), bits()):
|
||||
if b:
|
||||
G.add_edge(i, j)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Read and write graphs in GraphML format.
|
||||
.. warning::
|
||||
|
||||
This parser uses the standard xml library present in Python, which is
|
||||
insecure - see :doc:`library/xml` for additional information.
|
||||
insecure - see :external+python:mod:`xml` for additional information.
|
||||
Only parse GraphML files you trust.
|
||||
|
||||
This implementation does not support mixed graphs (directed and unidirected
|
||||
@@ -643,8 +643,8 @@ class GraphMLWriter(GraphML):
|
||||
# data that needs to be added to them.
|
||||
# We postpone processing in order to do type inference/generalization.
|
||||
# See self.attr_type
|
||||
for (xml_obj, data) in self.attributes.items():
|
||||
for (k, v, scope, default) in data:
|
||||
for xml_obj, data in self.attributes.items():
|
||||
for k, v, scope, default in data:
|
||||
xml_obj.append(
|
||||
self.add_data(
|
||||
str(k), self.attr_type(k, scope, v), str(v), scope, default
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@ import networkx as nx
|
||||
|
||||
__all__ = ["adjacency_data", "adjacency_graph"]
|
||||
|
||||
_attrs = dict(id="id", key="key")
|
||||
_attrs = {"id": "id", "key": "key"}
|
||||
|
||||
|
||||
def adjacency_data(G, attrs=_attrs):
|
||||
|
||||
@@ -5,7 +5,13 @@ import networkx as nx
|
||||
__all__ = ["node_link_data", "node_link_graph"]
|
||||
|
||||
|
||||
_attrs = dict(source="source", target="target", name="id", key="key", link="links")
|
||||
_attrs = {
|
||||
"source": "source",
|
||||
"target": "target",
|
||||
"name": "id",
|
||||
"key": "key",
|
||||
"link": "links",
|
||||
}
|
||||
|
||||
|
||||
def _to_tuple(x):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -56,5 +56,5 @@ class TestAdjacency:
|
||||
def test_exception(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.MultiDiGraph()
|
||||
attrs = dict(id="node", key="node")
|
||||
attrs = {"id": "node", "key": "node"}
|
||||
adjacency_data(G, attrs)
|
||||
|
||||
@@ -16,7 +16,13 @@ def test_attrs_deprecation(recwarn):
|
||||
assert len(recwarn) == 0
|
||||
|
||||
# Future warning raised with `attrs` kwarg
|
||||
attrs = dict(source="source", target="target", name="id", key="key", link="links")
|
||||
attrs = {
|
||||
"source": "source",
|
||||
"target": "target",
|
||||
"name": "id",
|
||||
"key": "key",
|
||||
"link": "links",
|
||||
}
|
||||
data = node_link_data(G, attrs=attrs)
|
||||
assert len(recwarn) == 1
|
||||
|
||||
@@ -26,7 +32,6 @@ def test_attrs_deprecation(recwarn):
|
||||
|
||||
|
||||
class TestNodeLink:
|
||||
|
||||
# TODO: To be removed when signature change complete
|
||||
def test_custom_attrs_dep(self):
|
||||
G = nx.path_graph(4)
|
||||
@@ -35,13 +40,13 @@ class TestNodeLink:
|
||||
G.graph[1] = "one"
|
||||
G.graph["foo"] = "bar"
|
||||
|
||||
attrs = dict(
|
||||
source="c_source",
|
||||
target="c_target",
|
||||
name="c_id",
|
||||
key="c_key",
|
||||
link="c_links",
|
||||
)
|
||||
attrs = {
|
||||
"source": "c_source",
|
||||
"target": "c_target",
|
||||
"name": "c_id",
|
||||
"key": "c_key",
|
||||
"link": "c_links",
|
||||
}
|
||||
|
||||
H = node_link_graph(
|
||||
node_link_data(G, attrs=attrs), multigraph=False, attrs=attrs
|
||||
@@ -53,11 +58,11 @@ class TestNodeLink:
|
||||
|
||||
# provide only a partial dictionary of keywords.
|
||||
# This is similar to an example in the doc string
|
||||
attrs = dict(
|
||||
link="c_links",
|
||||
source="c_source",
|
||||
target="c_target",
|
||||
)
|
||||
attrs = {
|
||||
"link": "c_links",
|
||||
"source": "c_source",
|
||||
"target": "c_target",
|
||||
}
|
||||
H = node_link_graph(
|
||||
node_link_data(G, attrs=attrs), multigraph=False, attrs=attrs
|
||||
)
|
||||
@@ -70,7 +75,7 @@ class TestNodeLink:
|
||||
def test_exception_dep(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.MultiDiGraph()
|
||||
attrs = dict(name="node", source="node", target="node", key="node")
|
||||
attrs = {"name": "node", "source": "node", "target": "node", "key": "node"}
|
||||
node_link_data(G, attrs)
|
||||
|
||||
def test_graph(self):
|
||||
@@ -133,7 +138,7 @@ class TestNodeLink:
|
||||
def test_exception(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.MultiDiGraph()
|
||||
attrs = dict(name="node", source="node", target="node", key="node")
|
||||
attrs = {"name": "node", "source": "node", "target": "node", "key": "node"}
|
||||
node_link_data(G, **attrs)
|
||||
|
||||
def test_string_ids(self):
|
||||
@@ -155,13 +160,13 @@ class TestNodeLink:
|
||||
G.graph[1] = "one"
|
||||
G.graph["foo"] = "bar"
|
||||
|
||||
attrs = dict(
|
||||
source="c_source",
|
||||
target="c_target",
|
||||
name="c_id",
|
||||
key="c_key",
|
||||
link="c_links",
|
||||
)
|
||||
attrs = {
|
||||
"source": "c_source",
|
||||
"target": "c_target",
|
||||
"name": "c_id",
|
||||
"key": "c_key",
|
||||
"link": "c_links",
|
||||
}
|
||||
|
||||
H = node_link_graph(node_link_data(G, **attrs), multigraph=False, **attrs)
|
||||
assert nx.is_isomorphic(G, H)
|
||||
|
||||
@@ -71,7 +71,7 @@ def parse_leda(lines):
|
||||
[
|
||||
line.rstrip("\n")
|
||||
for line in lines
|
||||
if not (line.startswith("#") or line.startswith("\n") or line == "")
|
||||
if not (line.startswith(("#", "\n")) or line == "")
|
||||
]
|
||||
)
|
||||
for i in range(3):
|
||||
|
||||
@@ -62,7 +62,7 @@ def _generate_sparse6_bytes(G, nodes, header):
|
||||
edges = sorted((max(u, v), min(u, v)) for u, v in G.edges())
|
||||
bits = []
|
||||
curv = 0
|
||||
for (v, u) in edges:
|
||||
for v, u in edges:
|
||||
if v == curv: # current vertex edge
|
||||
bits.append(0)
|
||||
bits.extend(enc(u))
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -462,7 +462,7 @@ gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd" version="1.2">
|
||||
G = nx.MultiGraph()
|
||||
G.add_node(0, label="1", color="green")
|
||||
G.add_node(1, label="2", color="green")
|
||||
G.add_edge(0, 1, id="0", wight=3, type="undirected", start=0, end=1)
|
||||
G.add_edge(0, 1, id="0", weight=3, type="undirected", start=0, end=1)
|
||||
G.add_edge(0, 1, id="1", label="foo", start=0, end=1)
|
||||
G.add_edge(0, 1)
|
||||
fh = io.BytesIO()
|
||||
@@ -491,6 +491,16 @@ gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd" version="1.2">
|
||||
sorted(e) for e in H.edges()
|
||||
)
|
||||
|
||||
# Test missing alpha value for version >draft1.1 - set default alpha value
|
||||
# to 1.0 instead of `None` when writing for better general compatibility
|
||||
fh = io.BytesIO()
|
||||
# G.nodes[0]["viz"]["color"] does not have an alpha value explicitly defined
|
||||
# so the default is used instead
|
||||
nx.write_gexf(G, fh, version="1.2draft")
|
||||
fh.seek(0)
|
||||
H = nx.read_gexf(fh, node_type=int)
|
||||
assert H.nodes[0]["viz"]["color"]["a"] == 1.0
|
||||
|
||||
# Second graph for the other branch
|
||||
G = nx.Graph()
|
||||
G.add_node(0, label="1", color="green")
|
||||
|
||||
@@ -146,13 +146,13 @@ graph [
|
||||
def test_parse_gml(self):
|
||||
G = nx.parse_gml(self.simple_data, label="label")
|
||||
assert sorted(G.nodes()) == ["Node 1", "Node 2", "Node 3"]
|
||||
assert [e for e in sorted(G.edges())] == [
|
||||
assert sorted(G.edges()) == [
|
||||
("Node 1", "Node 2"),
|
||||
("Node 2", "Node 3"),
|
||||
("Node 3", "Node 1"),
|
||||
]
|
||||
|
||||
assert [e for e in sorted(G.edges(data=True))] == [
|
||||
assert sorted(G.edges(data=True)) == [
|
||||
(
|
||||
"Node 1",
|
||||
"Node 2",
|
||||
@@ -446,14 +446,14 @@ graph
|
||||
G = nx.Graph()
|
||||
G.name = data
|
||||
G.graph["data"] = data
|
||||
G.add_node(0, int=-1, data=dict(data=data))
|
||||
G.add_node(0, int=-1, data={"data": data})
|
||||
G.add_edge(0, 0, float=-2.5, data=data)
|
||||
gml = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
|
||||
G = nx.parse_gml(gml, destringizer=literal_destringizer)
|
||||
assert data == G.name
|
||||
assert {"name": data, "data": data} == G.graph
|
||||
assert list(G.nodes(data=True)) == [(0, dict(int=-1, data=dict(data=data)))]
|
||||
assert list(G.edges(data=True)) == [(0, 0, dict(float=-2.5, data=data))]
|
||||
assert list(G.nodes(data=True)) == [(0, {"int": -1, "data": {"data": data}})]
|
||||
assert list(G.edges(data=True)) == [(0, 0, {"float": -2.5, "data": data})]
|
||||
G = nx.Graph()
|
||||
G.graph["data"] = "frozenset([1, 2, 3])"
|
||||
G = nx.parse_gml(nx.generate_gml(G), destringizer=literal_eval)
|
||||
@@ -544,7 +544,7 @@ graph
|
||||
"directed 1 multigraph 1 ]"
|
||||
)
|
||||
|
||||
# Tests for string convertable alphanumeric id and label values
|
||||
# Tests for string convertible alphanumeric id and label values
|
||||
nx.parse_gml("graph [edge [ source a target a ] node [ id a label b ] ]")
|
||||
nx.parse_gml(
|
||||
"graph [ node [ id n42 label 0 ] node [ id x43 label 1 ]"
|
||||
@@ -710,7 +710,7 @@ class TestPropertyLists:
|
||||
assert graph.nodes(data=True)["n1"] == {"properties": ["element"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("coll", (list(), tuple()))
|
||||
@pytest.mark.parametrize("coll", ([], ()))
|
||||
def test_stringize_empty_list_tuple(coll):
|
||||
G = nx.path_graph(2)
|
||||
G.nodes[0]["test"] = coll # test serializing an empty collection
|
||||
|
||||
@@ -522,13 +522,13 @@ class TestReadGraphML(BaseGraphML):
|
||||
# edges with no data, no keys:
|
||||
(1, 2),
|
||||
# edges with only data:
|
||||
(1, 2, dict(key="data_key1")),
|
||||
(1, 2, dict(id="data_id2")),
|
||||
(1, 2, dict(key="data_key3", id="data_id3")),
|
||||
(1, 2, {"key": "data_key1"}),
|
||||
(1, 2, {"id": "data_id2"}),
|
||||
(1, 2, {"key": "data_key3", "id": "data_id3"}),
|
||||
# edges with both data and keys:
|
||||
(1, 2, 103, dict(key="data_key4")),
|
||||
(1, 2, 104, dict(id="data_id5")),
|
||||
(1, 2, 105, dict(key="data_key6", id="data_id7")),
|
||||
(1, 2, 103, {"key": "data_key4"}),
|
||||
(1, 2, 104, {"id": "data_id5"}),
|
||||
(1, 2, 105, {"key": "data_key6", "id": "data_id7"}),
|
||||
]
|
||||
)
|
||||
fh = io.BytesIO()
|
||||
@@ -1485,7 +1485,7 @@ class TestWriteGraphML(BaseGraphML):
|
||||
# test for handling json escaped strings in python 2 Issue #1880
|
||||
import json
|
||||
|
||||
a = dict(a='{"a": "123"}') # an object with many chars to escape
|
||||
a = {"a": '{"a": "123"}'} # an object with many chars to escape
|
||||
sa = json.dumps(a)
|
||||
G = nx.Graph()
|
||||
G.graph["test"] = sa
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,474 @@
|
||||
"""
|
||||
Text-based visual representations of graphs
|
||||
"""
|
||||
import sys
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = ["forest_str"]
|
||||
import networkx as nx
|
||||
from networkx.utils import open_file
|
||||
|
||||
__all__ = ["forest_str", "generate_network_text", "write_network_text"]
|
||||
|
||||
|
||||
class _AsciiBaseGlyphs:
|
||||
empty = "+"
|
||||
newtree_last = "+-- "
|
||||
newtree_mid = "+-- "
|
||||
endof_forest = " "
|
||||
within_forest = ": "
|
||||
within_tree = "| "
|
||||
|
||||
|
||||
class AsciiDirectedGlyphs(_AsciiBaseGlyphs):
|
||||
last = "L-> "
|
||||
mid = "|-> "
|
||||
backedge = "<-"
|
||||
|
||||
|
||||
class AsciiUndirectedGlyphs(_AsciiBaseGlyphs):
|
||||
last = "L-- "
|
||||
mid = "|-- "
|
||||
backedge = "-"
|
||||
|
||||
|
||||
class _UtfBaseGlyphs:
|
||||
# Notes on available box and arrow characters
|
||||
# https://en.wikipedia.org/wiki/Box-drawing_character
|
||||
# https://stackoverflow.com/questions/2701192/triangle-arrow
|
||||
empty = "╙"
|
||||
newtree_last = "╙── "
|
||||
newtree_mid = "╟── "
|
||||
endof_forest = " "
|
||||
within_forest = "╎ "
|
||||
within_tree = "│ "
|
||||
|
||||
|
||||
class UtfDirectedGlyphs(_UtfBaseGlyphs):
|
||||
last = "└─╼ "
|
||||
mid = "├─╼ "
|
||||
backedge = "╾"
|
||||
|
||||
|
||||
class UtfUndirectedGlyphs(_UtfBaseGlyphs):
|
||||
last = "└── "
|
||||
mid = "├── "
|
||||
backedge = "─"
|
||||
|
||||
|
||||
def generate_network_text(
|
||||
graph, with_labels=True, sources=None, max_depth=None, ascii_only=False
|
||||
):
|
||||
"""Generate lines in the "network text" format
|
||||
|
||||
This works via a depth-first traversal of the graph and writing a line for
|
||||
each unique node encountered. Non-tree edges are written to the right of
|
||||
each node, and connection to a non-tree edge is indicated with an ellipsis.
|
||||
This representation works best when the input graph is a forest, but any
|
||||
graph can be represented.
|
||||
|
||||
This notation is original to networkx, although it is simple enough that it
|
||||
may be known in existing literature. See #5602 for details. The procedure
|
||||
is summarized as follows:
|
||||
|
||||
1. Given a set of source nodes (which can be specified, or automatically
|
||||
discovered via finding the (strongly) connected components and choosing one
|
||||
node with minimum degree from each), we traverse the graph in depth first
|
||||
order.
|
||||
|
||||
2. Each reachable node will be printed exactly once on it's own line.
|
||||
|
||||
3. Edges are indicated in one of three ways:
|
||||
|
||||
a. a parent "L-style" connection on the upper left. This corresponds to
|
||||
a traversal in the directed DFS tree.
|
||||
|
||||
b. a backref "<-style" connection shown directly on the right. For
|
||||
directed graphs, these are drawn for any incoming edges to a node that
|
||||
is not a parent edge. For undirected graphs, these are drawn for only
|
||||
the non-parent edges that have already been represented (The edges that
|
||||
have not been represented will be handled in the recursive case).
|
||||
|
||||
c. a child "L-style" connection on the lower right. Drawing of the
|
||||
children are handled recursively.
|
||||
|
||||
4. The children of each node (wrt the directed DFS tree) are drawn
|
||||
underneath and to the right of it. In the case that a child node has already
|
||||
been drawn the connection is replaced with an ellipsis ("...") to indicate
|
||||
that there is one or more connections represented elsewhere.
|
||||
|
||||
5. If a maximum depth is specified, an edge to nodes past this maximum
|
||||
depth will be represented by an ellipsis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
graph : nx.DiGraph | nx.Graph
|
||||
Graph to represent
|
||||
|
||||
with_labels : bool | str
|
||||
If True will use the "label" attribute of a node to display if it
|
||||
exists otherwise it will use the node value itself. If given as a
|
||||
string, then that attribute name will be used instead of "label".
|
||||
Defaults to True.
|
||||
|
||||
sources : List
|
||||
Specifies which nodes to start traversal from. Note: nodes that are not
|
||||
reachable from one of these sources may not be shown. If unspecified,
|
||||
the minimal set of nodes needed to reach all others will be used.
|
||||
|
||||
max_depth : int | None
|
||||
The maximum depth to traverse before stopping. Defaults to None.
|
||||
|
||||
ascii_only : Boolean
|
||||
If True only ASCII characters are used to construct the visualization
|
||||
|
||||
Yields
|
||||
------
|
||||
str : a line of generated text
|
||||
"""
|
||||
is_directed = graph.is_directed()
|
||||
|
||||
if is_directed:
|
||||
glyphs = AsciiDirectedGlyphs if ascii_only else UtfDirectedGlyphs
|
||||
succ = graph.succ
|
||||
pred = graph.pred
|
||||
else:
|
||||
glyphs = AsciiUndirectedGlyphs if ascii_only else UtfUndirectedGlyphs
|
||||
succ = graph.adj
|
||||
pred = graph.adj
|
||||
|
||||
if isinstance(with_labels, str):
|
||||
label_attr = with_labels
|
||||
elif with_labels:
|
||||
label_attr = "label"
|
||||
else:
|
||||
label_attr = None
|
||||
|
||||
if max_depth == 0:
|
||||
yield glyphs.empty + " ..."
|
||||
elif len(graph.nodes) == 0:
|
||||
yield glyphs.empty
|
||||
else:
|
||||
# If the nodes to traverse are unspecified, find the minimal set of
|
||||
# nodes that will reach the entire graph
|
||||
if sources is None:
|
||||
sources = _find_sources(graph)
|
||||
|
||||
# Populate the stack with each:
|
||||
# 1. parent node in the DFS tree (or None for root nodes),
|
||||
# 2. the current node in the DFS tree
|
||||
# 2. a list of indentations indicating depth
|
||||
# 3. a flag indicating if the node is the final one to be written.
|
||||
# Reverse the stack so sources are popped in the correct order.
|
||||
last_idx = len(sources) - 1
|
||||
stack = [
|
||||
(None, node, [], (idx == last_idx)) for idx, node in enumerate(sources)
|
||||
][::-1]
|
||||
|
||||
num_skipped_children = defaultdict(lambda: 0)
|
||||
seen_nodes = set()
|
||||
while stack:
|
||||
parent, node, indents, this_islast = stack.pop()
|
||||
|
||||
if node is not Ellipsis:
|
||||
skip = node in seen_nodes
|
||||
if skip:
|
||||
# Mark that we skipped a parent's child
|
||||
num_skipped_children[parent] += 1
|
||||
|
||||
if this_islast:
|
||||
# If we reached the last child of a parent, and we skipped
|
||||
# any of that parents children, then we should emit an
|
||||
# ellipsis at the end after this.
|
||||
if num_skipped_children[parent] and parent is not None:
|
||||
# Append the ellipsis to be emitted last
|
||||
next_islast = True
|
||||
try_frame = (node, Ellipsis, indents, next_islast)
|
||||
stack.append(try_frame)
|
||||
|
||||
# Redo this frame, but not as a last object
|
||||
next_islast = False
|
||||
try_frame = (parent, node, indents, next_islast)
|
||||
stack.append(try_frame)
|
||||
continue
|
||||
|
||||
if skip:
|
||||
continue
|
||||
seen_nodes.add(node)
|
||||
|
||||
if not indents:
|
||||
# Top level items (i.e. trees in the forest) get different
|
||||
# glyphs to indicate they are not actually connected
|
||||
if this_islast:
|
||||
this_prefix = indents + [glyphs.newtree_last]
|
||||
next_prefix = indents + [glyphs.endof_forest]
|
||||
else:
|
||||
this_prefix = indents + [glyphs.newtree_mid]
|
||||
next_prefix = indents + [glyphs.within_forest]
|
||||
|
||||
else:
|
||||
# For individual tree edges distinguish between directed and
|
||||
# undirected cases
|
||||
if this_islast:
|
||||
this_prefix = indents + [glyphs.last]
|
||||
next_prefix = indents + [glyphs.endof_forest]
|
||||
else:
|
||||
this_prefix = indents + [glyphs.mid]
|
||||
next_prefix = indents + [glyphs.within_tree]
|
||||
|
||||
if node is Ellipsis:
|
||||
label = " ..."
|
||||
suffix = ""
|
||||
children = []
|
||||
else:
|
||||
if label_attr is not None:
|
||||
label = str(graph.nodes[node].get(label_attr, node))
|
||||
else:
|
||||
label = str(node)
|
||||
|
||||
# Determine:
|
||||
# (1) children to traverse into after showing this node.
|
||||
# (2) parents to immediately show to the right of this node.
|
||||
if is_directed:
|
||||
# In the directed case we must show every successor node
|
||||
# note: it may be skipped later, but we don't have that
|
||||
# information here.
|
||||
children = list(succ[node])
|
||||
# In the directed case we must show every predecessor
|
||||
# except for parent we directly traversed from.
|
||||
handled_parents = {parent}
|
||||
else:
|
||||
# Showing only the unseen children results in a more
|
||||
# concise representation for the undirected case.
|
||||
children = [
|
||||
child for child in succ[node] if child not in seen_nodes
|
||||
]
|
||||
|
||||
# In the undirected case, parents are also children, so we
|
||||
# only need to immediately show the ones we can no longer
|
||||
# traverse
|
||||
handled_parents = {*children, parent}
|
||||
|
||||
if max_depth is not None and len(indents) == max_depth - 1:
|
||||
# Use ellipsis to indicate we have reached maximum depth
|
||||
if children:
|
||||
children = [Ellipsis]
|
||||
handled_parents = {parent}
|
||||
|
||||
# The other parents are other predecessors of this node that
|
||||
# are not handled elsewhere.
|
||||
other_parents = [p for p in pred[node] if p not in handled_parents]
|
||||
if other_parents:
|
||||
if label_attr is not None:
|
||||
other_parents_labels = ", ".join(
|
||||
[
|
||||
str(graph.nodes[p].get(label_attr, p))
|
||||
for p in other_parents
|
||||
]
|
||||
)
|
||||
else:
|
||||
other_parents_labels = ", ".join(
|
||||
[str(p) for p in other_parents]
|
||||
)
|
||||
suffix = " ".join(["", glyphs.backedge, other_parents_labels])
|
||||
else:
|
||||
suffix = ""
|
||||
|
||||
# Emit the line for this node, this will be called for each node
|
||||
# exactly once.
|
||||
yield "".join(this_prefix + [label, suffix])
|
||||
|
||||
# Push children on the stack in reverse order so they are popped in
|
||||
# the original order.
|
||||
for idx, child in enumerate(children[::-1]):
|
||||
next_islast = idx == 0
|
||||
try_frame = (node, child, next_prefix, next_islast)
|
||||
stack.append(try_frame)
|
||||
|
||||
|
||||
@open_file(1, "w")
|
||||
def write_network_text(
|
||||
graph,
|
||||
path=None,
|
||||
with_labels=True,
|
||||
sources=None,
|
||||
max_depth=None,
|
||||
ascii_only=False,
|
||||
end="\n",
|
||||
):
|
||||
"""Creates a nice text representation of a graph
|
||||
|
||||
This works via a depth-first traversal of the graph and writing a line for
|
||||
each unique node encountered. Non-tree edges are written to the right of
|
||||
each node, and connection to a non-tree edge is indicated with an ellipsis.
|
||||
This representation works best when the input graph is a forest, but any
|
||||
graph can be represented.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
graph : nx.DiGraph | nx.Graph
|
||||
Graph to represent
|
||||
|
||||
path : string or file or callable or None
|
||||
Filename or file handle for data output.
|
||||
if a function, then it will be called for each generated line.
|
||||
if None, this will default to "sys.stdout.write"
|
||||
|
||||
with_labels : bool | str
|
||||
If True will use the "label" attribute of a node to display if it
|
||||
exists otherwise it will use the node value itself. If given as a
|
||||
string, then that attribute name will be used instead of "label".
|
||||
Defaults to True.
|
||||
|
||||
sources : List
|
||||
Specifies which nodes to start traversal from. Note: nodes that are not
|
||||
reachable from one of these sources may not be shown. If unspecified,
|
||||
the minimal set of nodes needed to reach all others will be used.
|
||||
|
||||
max_depth : int | None
|
||||
The maximum depth to traverse before stopping. Defaults to None.
|
||||
|
||||
ascii_only : Boolean
|
||||
If True only ASCII characters are used to construct the visualization
|
||||
|
||||
end : string
|
||||
The line ending character
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> graph = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 0
|
||||
├─╼ 1
|
||||
│ ├─╼ 3
|
||||
│ └─╼ 4
|
||||
└─╼ 2
|
||||
├─╼ 5
|
||||
└─╼ 6
|
||||
|
||||
>>> # A near tree with one non-tree edge
|
||||
>>> graph.add_edge(5, 1)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 0
|
||||
├─╼ 1 ╾ 5
|
||||
│ ├─╼ 3
|
||||
│ └─╼ 4
|
||||
└─╼ 2
|
||||
├─╼ 5
|
||||
│ └─╼ ...
|
||||
└─╼ 6
|
||||
|
||||
>>> graph = nx.cycle_graph(5)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 0
|
||||
├── 1
|
||||
│ └── 2
|
||||
│ └── 3
|
||||
│ └── 4 ─ 0
|
||||
└── ...
|
||||
|
||||
>>> graph = nx.generators.barbell_graph(4, 2)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 4
|
||||
├── 5
|
||||
│ └── 6
|
||||
│ ├── 7
|
||||
│ │ ├── 8 ─ 6
|
||||
│ │ │ └── 9 ─ 6, 7
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
└── 3
|
||||
├── 0
|
||||
│ ├── 1 ─ 3
|
||||
│ │ └── 2 ─ 0, 3
|
||||
│ └── ...
|
||||
└── ...
|
||||
|
||||
>>> graph = nx.complete_graph(5, create_using=nx.Graph)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 0
|
||||
├── 1
|
||||
│ ├── 2 ─ 0
|
||||
│ │ ├── 3 ─ 0, 1
|
||||
│ │ │ └── 4 ─ 0, 1, 2
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
└── ...
|
||||
|
||||
>>> graph = nx.complete_graph(3, create_using=nx.DiGraph)
|
||||
>>> nx.write_network_text(graph)
|
||||
╙── 0 ╾ 1, 2
|
||||
├─╼ 1 ╾ 2
|
||||
│ ├─╼ 2 ╾ 0
|
||||
│ │ └─╼ ...
|
||||
│ └─╼ ...
|
||||
└─╼ ...
|
||||
"""
|
||||
if path is None:
|
||||
# The path is unspecified, write to stdout
|
||||
_write = sys.stdout.write
|
||||
elif hasattr(path, "write"):
|
||||
# The path is already an open file
|
||||
_write = path.write
|
||||
elif callable(path):
|
||||
# The path is a custom callable
|
||||
_write = path
|
||||
else:
|
||||
raise TypeError(type(path))
|
||||
|
||||
for line in generate_network_text(
|
||||
graph,
|
||||
with_labels=with_labels,
|
||||
sources=sources,
|
||||
max_depth=max_depth,
|
||||
ascii_only=ascii_only,
|
||||
):
|
||||
_write(line + end)
|
||||
|
||||
|
||||
def _find_sources(graph):
|
||||
"""
|
||||
Determine a minimal set of nodes such that the entire graph is reachable
|
||||
"""
|
||||
# For each connected part of the graph, choose at least
|
||||
# one node as a starting point, preferably without a parent
|
||||
if graph.is_directed():
|
||||
# Choose one node from each SCC with minimum in_degree
|
||||
sccs = list(nx.strongly_connected_components(graph))
|
||||
# condensing the SCCs forms a dag, the nodes in this graph with
|
||||
# 0 in-degree correspond to the SCCs from which the minimum set
|
||||
# of nodes from which all other nodes can be reached.
|
||||
scc_graph = nx.condensation(graph, sccs)
|
||||
supernode_to_nodes = {sn: [] for sn in scc_graph.nodes()}
|
||||
# Note: the order of mapping differs between pypy and cpython
|
||||
# so we have to loop over graph nodes for consistency
|
||||
mapping = scc_graph.graph["mapping"]
|
||||
for n in graph.nodes:
|
||||
sn = mapping[n]
|
||||
supernode_to_nodes[sn].append(n)
|
||||
sources = []
|
||||
for sn in scc_graph.nodes():
|
||||
if scc_graph.in_degree[sn] == 0:
|
||||
scc = supernode_to_nodes[sn]
|
||||
node = min(scc, key=lambda n: graph.in_degree[n])
|
||||
sources.append(node)
|
||||
else:
|
||||
# For undirected graph, the entire graph will be reachable as
|
||||
# long as we consider one node from every connected component
|
||||
sources = [
|
||||
min(cc, key=lambda n: graph.degree[n])
|
||||
for cc in nx.connected_components(graph)
|
||||
]
|
||||
sources = sorted(sources, key=lambda n: graph.degree[n])
|
||||
return sources
|
||||
|
||||
|
||||
def forest_str(graph, with_labels=True, sources=None, write=None, ascii_only=False):
|
||||
"""
|
||||
Creates a nice utf8 representation of a directed forest
|
||||
"""Creates a nice utf8 representation of a forest
|
||||
|
||||
This function has been superseded by
|
||||
:func:`nx.readwrite.text.generate_network_text`, which should be used
|
||||
instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -38,22 +499,22 @@ def forest_str(graph, with_labels=True, sources=None, write=None, ascii_only=Fal
|
||||
str | None :
|
||||
utf8 representation of the tree / forest
|
||||
|
||||
Example
|
||||
-------
|
||||
Examples
|
||||
--------
|
||||
>>> graph = nx.balanced_tree(r=2, h=3, create_using=nx.DiGraph)
|
||||
>>> print(nx.forest_str(graph))
|
||||
╙── 0
|
||||
├─╼ 1
|
||||
│ ├─╼ 3
|
||||
│ │ ├─╼ 7
|
||||
│ │ └─╼ 8
|
||||
│ └─╼ 4
|
||||
│ ├─╼ 9
|
||||
│ └─╼ 10
|
||||
│ ├─╼ 3
|
||||
│ │ ├─╼ 7
|
||||
│ │ └─╼ 8
|
||||
│ └─╼ 4
|
||||
│ ├─╼ 9
|
||||
│ └─╼ 10
|
||||
└─╼ 2
|
||||
├─╼ 5
|
||||
│ ├─╼ 11
|
||||
│ └─╼ 12
|
||||
│ ├─╼ 11
|
||||
│ └─╼ 12
|
||||
└─╼ 6
|
||||
├─╼ 13
|
||||
└─╼ 14
|
||||
@@ -70,7 +531,16 @@ def forest_str(graph, with_labels=True, sources=None, write=None, ascii_only=Fal
|
||||
L-- 1
|
||||
L-- 2
|
||||
"""
|
||||
import networkx as nx
|
||||
msg = (
|
||||
"\nforest_str is deprecated as of version 3.1 and will be removed "
|
||||
"in version 3.3. Use generate_network_text or write_network_text "
|
||||
"instead.\n"
|
||||
)
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
|
||||
if len(graph.nodes) > 0:
|
||||
if not nx.is_forest(graph):
|
||||
raise nx.NetworkXNotImplemented("input must be a forest or the empty graph")
|
||||
|
||||
printbuf = []
|
||||
if write is None:
|
||||
@@ -78,114 +548,14 @@ def forest_str(graph, with_labels=True, sources=None, write=None, ascii_only=Fal
|
||||
else:
|
||||
_write = write
|
||||
|
||||
# Define glphys
|
||||
# Notes on available box and arrow characters
|
||||
# https://en.wikipedia.org/wiki/Box-drawing_character
|
||||
# https://stackoverflow.com/questions/2701192/triangle-arrow
|
||||
if ascii_only:
|
||||
glyph_empty = "+"
|
||||
glyph_newtree_last = "+-- "
|
||||
glyph_newtree_mid = "+-- "
|
||||
glyph_endof_forest = " "
|
||||
glyph_within_forest = ": "
|
||||
glyph_within_tree = "| "
|
||||
|
||||
glyph_directed_last = "L-> "
|
||||
glyph_directed_mid = "|-> "
|
||||
|
||||
glyph_undirected_last = "L-- "
|
||||
glyph_undirected_mid = "|-- "
|
||||
else:
|
||||
glyph_empty = "╙"
|
||||
glyph_newtree_last = "╙── "
|
||||
glyph_newtree_mid = "╟── "
|
||||
glyph_endof_forest = " "
|
||||
glyph_within_forest = "╎ "
|
||||
glyph_within_tree = "│ "
|
||||
|
||||
glyph_directed_last = "└─╼ "
|
||||
glyph_directed_mid = "├─╼ "
|
||||
|
||||
glyph_undirected_last = "└── "
|
||||
glyph_undirected_mid = "├── "
|
||||
|
||||
if len(graph.nodes) == 0:
|
||||
_write(glyph_empty)
|
||||
else:
|
||||
if not nx.is_forest(graph):
|
||||
raise nx.NetworkXNotImplemented("input must be a forest or the empty graph")
|
||||
|
||||
is_directed = graph.is_directed()
|
||||
succ = graph.succ if is_directed else graph.adj
|
||||
|
||||
if sources is None:
|
||||
if is_directed:
|
||||
# use real source nodes for directed trees
|
||||
sources = [n for n in graph.nodes if graph.in_degree[n] == 0]
|
||||
else:
|
||||
# use arbitrary sources for undirected trees
|
||||
sources = [
|
||||
min(cc, key=lambda n: graph.degree[n])
|
||||
for cc in nx.connected_components(graph)
|
||||
]
|
||||
|
||||
# Populate the stack with each source node, empty indentation, and mark
|
||||
# the final node. Reverse the stack so sources are popped in the
|
||||
# correct order.
|
||||
last_idx = len(sources) - 1
|
||||
stack = [(node, "", (idx == last_idx)) for idx, node in enumerate(sources)][
|
||||
::-1
|
||||
]
|
||||
|
||||
seen = set()
|
||||
while stack:
|
||||
node, indent, islast = stack.pop()
|
||||
if node in seen:
|
||||
continue
|
||||
seen.add(node)
|
||||
|
||||
if not indent:
|
||||
# Top level items (i.e. trees in the forest) get different
|
||||
# glyphs to indicate they are not actually connected
|
||||
if islast:
|
||||
this_prefix = indent + glyph_newtree_last
|
||||
next_prefix = indent + glyph_endof_forest
|
||||
else:
|
||||
this_prefix = indent + glyph_newtree_mid
|
||||
next_prefix = indent + glyph_within_forest
|
||||
|
||||
else:
|
||||
# For individual tree edges distinguish between directed and
|
||||
# undirected cases
|
||||
if is_directed:
|
||||
if islast:
|
||||
this_prefix = indent + glyph_directed_last
|
||||
next_prefix = indent + glyph_endof_forest
|
||||
else:
|
||||
this_prefix = indent + glyph_directed_mid
|
||||
next_prefix = indent + glyph_within_tree
|
||||
else:
|
||||
if islast:
|
||||
this_prefix = indent + glyph_undirected_last
|
||||
next_prefix = indent + glyph_endof_forest
|
||||
else:
|
||||
this_prefix = indent + glyph_undirected_mid
|
||||
next_prefix = indent + glyph_within_tree
|
||||
|
||||
if with_labels:
|
||||
label = graph.nodes[node].get("label", node)
|
||||
else:
|
||||
label = node
|
||||
|
||||
_write(this_prefix + str(label))
|
||||
|
||||
# Push children on the stack in reverse order so they are popped in
|
||||
# the original order.
|
||||
children = [child for child in succ[node] if child not in seen]
|
||||
for idx, child in enumerate(children[::-1], start=1):
|
||||
islast_next = idx <= 1
|
||||
try_frame = (child, next_prefix, islast_next)
|
||||
stack.append(try_frame)
|
||||
write_network_text(
|
||||
graph,
|
||||
_write,
|
||||
with_labels=with_labels,
|
||||
sources=sources,
|
||||
ascii_only=ascii_only,
|
||||
end="",
|
||||
)
|
||||
|
||||
if write is None:
|
||||
# Only return a string if the custom write function was not specified
|
||||
|
||||
Reference in New Issue
Block a user