~tfardet/nngt-developers

NNGT: Doc+Plot: updated doc and gallery plots v1 APPLIED

~tfardet: 1
 Doc+Plot: updated doc and gallery plots

 18 files changed, 195 insertions(+), 168 deletions(-)
#608173 .build.yml failed
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~tfardet/nngt-developers/patches/25710/mbox | git am -3
Learn more about email & git

[PATCH NNGT] Doc+Plot: updated doc and gallery plots Export this patch

From: Tanguy Fardet <tanguyfardet@protonmail.com>

* Improve documentation plots and colors
* Improve marker size and shape support
* Correct igraph documentation links
* Correct geospatial map initialization
* Switch NaturalEarth download to new location
* Include all doc examples into tests
---
 .build.yml                                    |  8 +--
 doc/conf.py                                   |  8 +--
 .../graph_properties/plot_attributes.py       |  2 +-
 .../graph_properties/plot_betweenness.py      | 13 ++--
 doc/examples/graph_properties/plot_degrees.py |  3 +-
 .../graph_structure/plot_hive_panel.py        | 18 +++---
 doc/examples/graph_structure/plot_layouts.py  | 30 ++++-----
 doc/examples/graph_structure/plot_map.py      |  7 ++-
 doc/examples/groups_and_metagroups.py         | 33 +++++-----
 doc/nngt_theme/static/nngt_theme.css          | 61 +++++++++++--------
 nngt/analysis/clustering.py                   | 26 +++++---
 nngt/geospatial/_cartopy_ne.py                | 33 +++-------
 nngt/geospatial/countries.py                  |  6 +-
 nngt/plot/plt_networks.py                     | 12 ++--
 nngt/plot/plt_properties.py                   | 37 +++++------
 setup.py                                      |  9 ++-
 testing/__init__.py                           |  9 +--
 testing/test_examples.py                      | 48 +++++++++++----
 18 files changed, 195 insertions(+), 168 deletions(-)

diff --git a/.build.yml b/.build.yml
index f557b27..f631aaf 100644
--- a/.build.yml
+++ b/.build.yml
@@ -15,12 +15,12 @@ tasks:
        sudo apt-key adv --keyserver keys.openpgp.org --recv-key 612DEFB798507F25
        sudo add-apt-repository -y ppa:nest-simulator/nest
        sudo apt-get update -qq
        sudo apt install -y gcc libcairo2-dev pkg-config build-essential autoconf automake python3-dev libblas-dev
        sudo apt install -y gcc libcairo2-dev pkg-config build-essential autoconf automake python3-dev libblas-dev libgeos-dev proj-bin libproj-dev
        sudo apt install -y nest liblapack-dev libatlas-base-dev gfortran libxml2-dev openmpi-bin libopenmpi-dev libgmp-dev
        sudo apt install -y python3-pip python3-tk libigraph0v5 libigraph0-dev python3-graph-tool
        sudo apt install -y python3-pip python3-tk libigraph0v5 libigraph0-dev python3-graph-tool python3-cairo python3-cairocffi
        pip3 install numpy scipy cython mpi4py
        pip3 install networkx python-igraph
        pip3 install pycairo matplotlib seaborn shapely svg.path dxfgrabber
        pip3 install matplotlib seaborn shapely svg.path dxfgrabber 'cartopy<0.20' geopandas descartes
        pip3 install pytest pytest-mpi cov-core coverage coveralls[yaml]
        cd NNGT
        python3 setup.py install --user
@@ -32,7 +32,7 @@ tasks:
        GL=nx coverage run -p -m pytest testing
        GL=ig coverage run -p -m pytest testing
        GL=nngt coverage run -p -m pytest testing
        GL=gt OMP=2 coverage run -p -m pytest testing
        GL=gt OMP=2 coverage run -p -m pytest -s testing
        GL=gt OMP=0 MPI=1 mpirun -n 2 coverage run -p -m pytest --with-mpi testing
        coverage combine
        GIT_BRANCH=$(git show -s --pretty=%D HEAD | tr -s ', /' '\n' | grep -v HEAD | sed -n 2p)
diff --git a/doc/conf.py b/doc/conf.py
index d7f6b38..a2e1edf 100755
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -558,8 +558,8 @@ intersphinx_mapping = {
    'gt': ('https://graph-tool.skewed.de/static/doc/', None),
    'ipython': ('https://ipython.org/ipython-doc/stable/', None),
    'matplotlib': ('https://matplotlib.org/', None),
    'networkx': ('https://networkx.github.io/documentation/stable/', None),
    'numpy': ('https://docs.scipy.org/doc/numpy', None),
    'networkx': ('https://networkx.org/documentation/stable/', None),
    'numpy': ('https://numpy.org/doc/stable/', None),
    'python': ('https://docs.python.org/3/', None),
    'scipy': ('https://docs.scipy.org/doc/scipy/reference', None),
    'shapely': ('https://shapely.readthedocs.io/en/latest/', None),
@@ -569,8 +569,8 @@ extlinks_fancy = {
    'doi': (['https://dx.doi.org/{0}'], ['DOI: {0}']),
    'arxiv': (['https://arxiv.org/abs/{0}'], ['arXiv: {0}']),
    'gtdoc': (['https://graph-tool.skewed.de/static/doc/{0}.html#graph_tool.{1}'], ['graph-tool - {0}']),
    'igdoc': (['https://igraph.org/python/doc/igraph.GraphBase-class.html#{0}'], ['igraph - {0}']),
    'nxdoc': (['https://networkx.github.io/documentation/stable/reference/{0}generated/networkx.{1}.html'], ['networkx - {0}'])
    'igdoc': (['https://igraph.org/python/api/latest/igraph._igraph.GraphBase.html#{0}'], ['igraph - {0}']),
    'nxdoc': (['https://networkx.org/documentation/stable/reference/{0}generated/networkx.{1}.html'], ['networkx - {0}'])
}

# sphinx gallery parameters
diff --git a/doc/examples/graph_properties/plot_attributes.py b/doc/examples/graph_properties/plot_attributes.py
index d20c69b..f0851bb 100644
--- a/doc/examples/graph_properties/plot_attributes.py
+++ b/doc/examples/graph_properties/plot_attributes.py
@@ -31,7 +31,7 @@ import matplotlib.pyplot as plt
plt.rcParams.update({
    'axes.edgecolor': 'grey', 'xtick.color': 'grey', 'ytick.color': 'grey',
    "figure.facecolor": (0, 0, 0, 0), "axes.facecolor": (0, 0, 0, 0),
    "axes.labelcolor": "grey", "axes.titlecolor": "grey", "text.color": "grey"
    "axes.labelcolor": "grey", "text.color": "grey"
})


diff --git a/doc/examples/graph_properties/plot_betweenness.py b/doc/examples/graph_properties/plot_betweenness.py
index 42822fa..a4a4c70 100644
--- a/doc/examples/graph_properties/plot_betweenness.py
+++ b/doc/examples/graph_properties/plot_betweenness.py
@@ -31,8 +31,7 @@ import matplotlib.pyplot as plt
plt.rcParams.update({
    'axes.edgecolor': 'grey', 'xtick.color': 'grey', 'ytick.color': 'grey',
    "figure.facecolor": (0, 0, 0, 0), "axes.facecolor": (0, 0, 0, 0),
    "axes.labelcolor": "grey", "axes.titlecolor": "grey", "text.color": "grey",
    "legend.facecolor": "none"
    "axes.labelcolor": "grey", "text.color": "grey", "legend.facecolor": "none"
})


@@ -50,18 +49,18 @@ g = nngt.generation.distance_rule(5, shape=shape, nodes=1000, avg_deg=3)
# %%
# then we can plot the betweenness

nplt.betweenness_distribution(g, logx=True, show=True,
                                   legend_location='left')
nplt.betweenness_distribution(g, logx=True, show=True, legend_location='left')

# %%
# we can of course change various parameters and plot only the nodes

nplt.betweenness_distribution(g, logx=False, show=True)

nplt.betweenness_distribution(g, btype="node", num_nbins="auto",
                                   alpha=0.5, show=True)
nplt.betweenness_distribution(g, btype="node", num_nbins="auto", alpha=0.5,
                              show=True)

# %%
# By the way, this is the graph we're looking at

nplt.draw_network(g, max_nsize=1, show_environment=False, show=True)
nplt.draw_network(g, max_nsize=5, max_esize=4, ecolor="grey", eborder_color="w",
                  curved_edges=True, show_environment=False, show=True)
diff --git a/doc/examples/graph_properties/plot_degrees.py b/doc/examples/graph_properties/plot_degrees.py
index c2ee531..7f48e8b 100644
--- a/doc/examples/graph_properties/plot_degrees.py
+++ b/doc/examples/graph_properties/plot_degrees.py
@@ -30,8 +30,7 @@ import matplotlib.pyplot as plt
plt.rcParams.update({
    'axes.edgecolor': 'grey', 'xtick.color': 'grey', 'ytick.color': 'grey',
    "figure.facecolor": (0, 0, 0, 0), "axes.facecolor": (0, 0, 0, 0),
    "axes.labelcolor": "grey", "axes.titlecolor": "grey", "text.color": "grey",
    "legend.facecolor": "none"
    "axes.labelcolor": "grey", "text.color": "grey", "legend.facecolor": "none"
})


diff --git a/doc/examples/graph_structure/plot_hive_panel.py b/doc/examples/graph_structure/plot_hive_panel.py
index d62a11c..25a94b9 100644
--- a/doc/examples/graph_structure/plot_hive_panel.py
+++ b/doc/examples/graph_structure/plot_hive_panel.py
@@ -22,7 +22,8 @@ Hive plot panel
===============
"""

import os
import inspect
from os.path import abspath, dirname

import matplotlib.pyplot as plt

@@ -30,13 +31,12 @@ import nngt


plt.rcParams.update({
    "figure.facecolor": (0, 0, 0, 0),
    "axes.titlecolor": "grey", "text.color": "grey"
    "figure.facecolor": (0, 0, 0, 0), "text.color": "grey"
})


dirpath  = os.path.abspath(os.getcwd())
rootpath = os.path.abspath(dirpath + "/../../..")
dirpath  = dirname(inspect.getframeinfo(inspect.currentframe()).filename)
rootpath = abspath(dirpath + "/../../..")


# load graph
@@ -82,10 +82,7 @@ for i in range(len(todo)):
        ax = axes[i, j]

        if i == 0:
            ax.set_title(ax_name)

        if j == 0:
            ax.set_ylabel(radial)
            ax.set_title(ax_name + " (groups)")

        size = todo[list(set([0, 1, 2]).difference([i, j]))[0]]

@@ -94,7 +91,8 @@ for i in range(len(todo)):
            axes_bins=ax_bins, axes_units="native", axis=ax, show_names=False)

for i in range(len(todo)):
    fig.text(0.03, 0.8 - i*0.33, todo[i], rotation=90, fontsize="large")
    fig.text(0.03, 0.83 - i*0.33, todo[i] + " (radius)", rotation=90,
             fontsize="large", va="center")

plt.tight_layout()

diff --git a/doc/examples/graph_structure/plot_layouts.py b/doc/examples/graph_structure/plot_layouts.py
index 0d84d02..07cbb45 100644
--- a/doc/examples/graph_structure/plot_layouts.py
+++ b/doc/examples/graph_structure/plot_layouts.py
@@ -32,7 +32,7 @@ import nngt

plt.rcParams.update({
    "figure.facecolor": (0, 0, 0, 0),
    "axes.labelcolor": "grey", "axes.titlecolor": "grey", "text.color": "grey"
    "axes.labelcolor": "grey", "text.color": "grey"
})


@@ -43,7 +43,6 @@ nngt.seed(0)
mpl_backend = mpl.get_backend()

if nngt.get_config("backend") in ("graph-tool", "igraph"):

    if mpl_backend.startswith("Qt4"):
        if mpl_backend != "Qt4Cairo":
            plt.switch_backend("Qt4Cairo")
@@ -53,13 +52,18 @@ if nngt.get_config("backend") in ("graph-tool", "igraph"):
    elif mpl_backend.startswith("GTK"):
        if mpl_backend != "GTK3Cairo":
            plt.switch_backend("GTK3Cairo")
    elif mpl_backend != "cairo":
    else:
        plt.switch_backend("cairo")


# prepare figure and parameters

_, axes = plt.subplots(2, 2, figsize=(10, 8))
fig = plt.figure(figsize=(10, 8), constrained_layout=False)

gs = fig.add_gridspec(nrows=2, ncols=2, left=0, right=1, bottom=0, top=0.97,
                      wspace=0, hspace=0.05)

axes = [fig.add_subplot(gs[i, j]) for i in (0, 1) for j in (0, 1)]

num_nodes = 50

@@ -84,10 +88,10 @@ nngt.generation.connect_groups(g, (room1, room2), struct, "erdos_renyi",

nngt.generation.connect_groups(g, room3, room1, "erdos_renyi", avg_deg=5)

nngt.plot.library_draw(g, tight=False, axis=axes[0, 0], ecolor="grey",
nngt.plot.library_draw(g, tight=False, axis=axes[0], ecolor="grey",
                       show=False)

axes[0, 0].set_title("Spring-block layout")
axes[0].set_title("Spring-block layout")


# random layout
@@ -96,19 +100,19 @@ sw = nngt.generation.watts_strogatz(4, 0.3, nodes=num_nodes)

betw = nngt.analysis.betweenness(sw, "node")

nngt.plot.draw_network(sw, nsize=betw, ncolor="out-degree", axis=axes[0, 1],
nngt.plot.draw_network(sw, nsize=betw, ncolor="out-degree", axis=axes[1],
                       ecolor="lightgrey", tight=False, show=False)

axes[0, 1].set_title("Random layout")
axes[1].set_title("Random layout")


# circular layout for small-world networks

nngt.plot.draw_network(sw, nsize=betw, ncolor="out-degree", layout="circular",
                       ecolor="lightgrey", axis=axes[1, 0],
                       ecolor="lightgrey", axis=axes[2],
                       show=False, tight=False)

axes[1, 0].set_title("Circular layout")
axes[2].set_title("Circular layout")


# spatial layout
@@ -126,13 +130,11 @@ g = nngt.generation.distance_rule(10, shape=shape, nodes=num_nodes, avg_deg=5,

cc = nngt.analysis.local_clustering(g)

nngt.plot.draw_network(g, ncolor=cc, axis=axes[1, 1], ecolor="grey", show=False,
nngt.plot.draw_network(g, ncolor=cc, axis=axes[3], ecolor="grey", show=False,
                       eborder_width=0.5, eborder_color="w", esize=10,
                       max_nsize=max_nsize, tight=False)

axes[1, 1].set_title("Spatial layout")

plt.tight_layout()
axes[3].set_title("Spatial layout")

# save figure

diff --git a/doc/examples/graph_structure/plot_map.py b/doc/examples/graph_structure/plot_map.py
index f5a2ac2..6b4da61 100644
--- a/doc/examples/graph_structure/plot_map.py
+++ b/doc/examples/graph_structure/plot_map.py
@@ -35,7 +35,7 @@ import nngt.geospatial as ng
plt.rcParams.update({
    'axes.edgecolor': 'grey', 'xtick.color': 'grey', 'ytick.color': 'grey',
    "figure.facecolor": (0, 0, 0, 0), "axes.facecolor": (0, 0, 0, 0),
    "axes.labelcolor": "grey", "axes.titlecolor": "grey", "text.color": "grey"
    "axes.labelcolor": "grey", "text.color": "grey"
})


@@ -62,5 +62,6 @@ g.set_weights(nngt._rng.exponential(2, g.edge_nb()))
ng.draw_map(g, "code", ncolor="in-degree", esize="weight", threshold=0,
            ecolor="grey", proj=ccrs.EqualEarth(), max_nsize=20, show=False)

plt.tight_layout()
plt.show()
if nngt.get_config("with_plot"):
    plt.tight_layout()
    plt.show()
diff --git a/doc/examples/groups_and_metagroups.py b/doc/examples/groups_and_metagroups.py
index cc6e251..59ad0e4 100644
--- a/doc/examples/groups_and_metagroups.py
+++ b/doc/examples/groups_and_metagroups.py
@@ -90,30 +90,33 @@ print(pop["left"])
Plot the graph
'''

if nngt.get_config("with_plot"):
    import matplotlib.pyplot as plt
plt = None

    # we plot the graph, setting the node shape from the left and right groups
    # and the color from the neuronal type (exc. and inhib.)
# we plot the graph, setting the node shape from the left and right groups
# and the color from the neuronal type (exc. and inhib.)

    nngt.plot.draw_network(net, nshape=[left, right], show_environment=False)
nngt.plot.draw_network(net, nshape=[left, right], nsize=20,
                       show_environment=False)

if nngt.get_config("with_plot"):
    import matplotlib.pyplot as plt
    plt.show()

    # further tests to make sure every configuration works
# further tests to make sure every configuration works

    nngt.plot.draw_network(net, nshape=[left, right], show_environment=False,
                           simple_nodes=True)
nngt.plot.draw_network(net, nshape=[left, right], show_environment=False,
                       max_nsize=20, simple_nodes=True)

    nngt.plot.draw_network(net, nshape=["o" for _ in range(net.node_nb())],
                           show_environment=False, simple_nodes=True)
nngt.plot.draw_network(net, nshape=["o" for _ in range(net.node_nb())],
                       show_environment=False, simple_nodes=True)

    nngt.plot.draw_network(net, nshape=["o" for _ in range(net.node_nb())],
                           show_environment=False)
nngt.plot.draw_network(net, nshape=["o" for _ in range(net.node_nb())],
                       show_environment=False)

    nngt.plot.draw_network(net, nshape="s", show_environment=False,
                           simple_nodes=True)
nngt.plot.draw_network(net, nshape="s", show_environment=False,
                       simple_nodes=True)

    nngt.plot.draw_network(net, nshape="s", show_environment=False)
nngt.plot.draw_network(net, nshape="s", show_environment=False)

if nngt.get_config("with_plot"):
    plt.show()
diff --git a/doc/nngt_theme/static/nngt_theme.css b/doc/nngt_theme/static/nngt_theme.css
index 771cc8b..60ce9f4 100755
--- a/doc/nngt_theme/static/nngt_theme.css
+++ b/doc/nngt_theme/static/nngt_theme.css
@@ -7,10 +7,10 @@
    --hv-color: #eeeeee;
    --sb-color: #f7f5fa;
    --dt-color: #f3f3fb;
    --img-invert: 0;
    --alert-info: #5bc0de;
    --admonition-a: #555555;
    --a-color: #008cba;
    --highlighted: #fbe54e;
}

@media (prefers-color-scheme: dark) {
@@ -21,10 +21,37 @@
        --hv-color: #333333;
        --sb-color: #3f3f3f;
        --dt-color: #111111;
        --img-invert: 1;
        --alert-info: #1e7bb3;
        --admonition-a: #cccccc;
        --admonition-a: #bbbbbb;
        --a-color: #2ec5f6;
        --highlighted: #80731e;
    }

    /* math */

    img.math {
        -webkit-filter: invert(100%);
        filter: invert(100%);
    }

    div.math img {
        -webkit-filter: invert(100%);
        filter: invert(100%);
    }

    div.highlight {
        -webkit-filter: invert(100%) hue-rotate(180deg);
        filter: invert(100%) hue-rotate(180deg);
    }

    code {
        -webkit-filter: invert(100%) hue-rotate(180deg);
        filter: invert(100%) hue-rotate(180deg);
    }

    code span.pre {
        -webkit-filter: hue-rotate(180deg);
        filter: hue-rotate(180deg);
    }
}

@@ -36,7 +63,6 @@
        --hv-color: #eeeeee;
        --sb-color: #f7f5fa;
        --dt-color: #f3f3fb;
        --img-invert: 0;
        --alert-info: #5bc0de;
        --admonition-a: #555555;
        --a-color: #008cba;
@@ -227,18 +253,12 @@ a.fn-backref {
    color: var(--fg-color);
}

.alert-info {
    background-color: var(--alert-info) !important;
}

div.highlight {
   -webkit-filter: invert(var(--img-invert));
   filter: invert(var(--img-invert));
dt:target, span.highlighted {
    background-color: var(--highlighted);
}

code {
   -webkit-filter: invert(var(--img-invert));
   filter: invert(var(--img-invert));
.alert-info {
    background-color: var(--alert-info) !important;
}

.form-control {
@@ -280,16 +300,3 @@ div.sphx-glr-download a {
div.sphx-glr-download a:hover {
    background-image: linear-gradient(to top, var(--sb-color), var(--sb-color)) !important;
}


/* math */

img.math {
   -webkit-filter: invert(var(--img-invert));
   filter: invert(var(--img-invert));
}

div.math img {
   -webkit-filter: invert(var(--img-invert));
   filter: invert(var(--img-invert));
}
diff --git a/nngt/analysis/clustering.py b/nngt/analysis/clustering.py
index 65e6925..72d242d 100644
--- a/nngt/analysis/clustering.py
+++ b/nngt/analysis/clustering.py
@@ -82,8 +82,8 @@ def global_clustering(g, directed=True, weights=None, method="continuous",
        otherwise uses any valid edge attribute required.
    method : str, optional (default: 'continuous')
        Method used to compute the weighted clustering, either 'barrat'
        [Barrat2004]_, 'continuous', 'onnela' [Onnela2005]_, or 'zhang'
        [Zhang2005]_.
        [Barrat2004]_, 'continuous' [Fardet2021]_, 'onnela' [Onnela2005]_, or
        'zhang' [Zhang2005]_.
    mode : str, optional (default: "total")
        Type of clustering to use for directed graphs, among "total", "fan-in",
        "fan-out", "middleman", and "cycle" [Fagiolo2007]_.
@@ -119,6 +119,9 @@ def global_clustering(g, directed=True, weights=None, method="continuous",
        and Molecular Biology 2005, 4 (1). :doi:`10.2202/1544-6115.1128`,
        `PDF <https://dibernardo.tigem.it/files/papers/2008/
        zhangbin-statappsgeneticsmolbio.pdf>`_.
    .. [Fardet2021] Fardet, Levina. Weighted directed clustering:
        interpretations and requirements for heterogeneous, inferred, and
        measured networks. 2021. :arxiv:`2105.06318`.

    See also
    --------
@@ -183,6 +186,8 @@ def local_closure(g, directed=True, weights=None, method=None,
    * "fan-in" is given by the pattern [(k, j), (j, i), (k, i)],
    * "fan-out" is given by the pattern [(i, j), (j, k), (i, k)].

    See [Fardet2021]_ for more details.

    Parameters
    ----------
    g : :class:`~nngt.Graph`
@@ -219,6 +224,9 @@ def local_closure(g, directed=True, weights=None, method=None,
        International Conference on Web Search and Data Mining 2019, 303-311.
        :doi:`10.1145/3289600.3290991`, `PDF <https://www.cs.cornell.edu/~arb/
        papers/closure-coefficients-WSDM-2019.pdf>`_.
    .. [Fardet2021] Fardet, Levina. Weighted directed clustering:
        interpretations and requirements for heterogeneous, inferred, and
        measured networks. 2021. :arxiv:`2105.06318`.
    '''
    directed *= g.is_directed()
    weighted  = weights not in (False, None)
@@ -345,8 +353,8 @@ def local_clustering(g, nodes=None, directed=True, weights=None,
    normalized to dimensionless values between 0 and 1 through a division by
    the highest weight.

    The default `method` for weighted networks is based on a modification of
    the proposal in [Zhang2005]_ with:
    The default `method` for weighted networks is the continuous definition
    [Fardet2021]_ and is defined as:

    .. math::

@@ -384,7 +392,8 @@ def local_clustering(g, nodes=None, directed=True, weights=None,
    * equivalence between no-edge and zero-weight edge cases,
    * normalized (always between zero and 1).

    Using either 'continuous' or 'zhang' is recommended for weighted graphs.
    Using either 'continuous' or 'zhang' is usually recommended for weighted
    graphs, see the discussion in [Fardet2021]_ for details.

    Parameters
    ----------
@@ -400,8 +409,8 @@ def local_clustering(g, nodes=None, directed=True, weights=None,
        otherwise uses any valid edge attribute required.
    method : str, optional (default: 'continuous')
        Method used to compute the weighted clustering, either 'barrat'
        [Barrat2004]_/[Clemente2018]_, 'continuous', 'onnela' [Onnela2005]_/
        [Fagiolo2007]_, or 'zhang' [Zhang2005]_.
        [Barrat2004]_/[Clemente2018]_, 'continuous' [Fardet2021]_, 'onnela'
        [Onnela2005]_/[Fagiolo2007]_, or 'zhang' [Zhang2005]_.
    mode : str, optional (default: "total")
        Type of clustering to use for directed graphs, among "total", "fan-in",
        "fan-out", "middleman", and "cycle" [Fagiolo2007]_.
@@ -445,6 +454,9 @@ def local_clustering(g, nodes=None, directed=True, weights=None,
        and Molecular Biology 2005, 4 (1). :doi:`10.2202/1544-6115.1128`,
        `PDF <https://dibernardo.tigem.it/files/papers/2008/
        zhangbin-statappsgeneticsmolbio.pdf>`_.
    .. [Fardet2021] Fardet, Levina. Weighted directed clustering:
        interpretations and requirements for heterogeneous, inferred, and
        measured networks. 2021. :arxiv:`2105.06318`.

    See also
    --------
diff --git a/nngt/geospatial/_cartopy_ne.py b/nngt/geospatial/_cartopy_ne.py
index 983db03..228f483 100644
--- a/nngt/geospatial/_cartopy_ne.py
+++ b/nngt/geospatial/_cartopy_ne.py
@@ -50,10 +50,7 @@ class NEShpDownloader(Downloader):
    # Define the NaturalEarth URL template. The natural earth website
    # returns a 302 status if accessing directly, so we use the naciscdn
    # URL directly.
    _NE_URL_TEMPLATE = ('https://naciscdn.org/naturalearth/{resolution}'
                        '/{category}/ne_{resolution}_{name}.zip')

    _NE_URL_BACKUP = ('https://naturalearth.s3.amazonaws.com/{resolution}'
    _NE_URL_TEMPLATE = ('https://naturalearth.s3.amazonaws.com/{resolution}'
                      '_{category}/ne_{resolution}_{name}.zip')

    def __init__(self, url_template=_NE_URL_TEMPLATE,
@@ -102,8 +99,8 @@ class NEShpDownloader(Downloader):

        return target_path

    @classmethod
    def default_downloader(cls, backup=False):
    @staticmethod
    def default_downloader():
        '''
        Return a generic, standard, NEShpDownloader instance.

@@ -118,20 +115,14 @@ ne_{resolution}_{name}.shp
        '''
        default_spec = ('shapefiles', 'natural_earth', '{category}',
                        'ne_{resolution}_{name}.shp')

        ne_path_template = os.path.join('{config[data_dir]}', *default_spec)

        pre_path_template = os.path.join('{config[pre_existing_data_dir]}',
                                         *default_spec)

        url_template = cls._NE_URL_BACKUP if backup else cls._NE_URL_TEMPLATE

        return NEShpDownloader(url_template=url_template,
                               target_path_template=ne_path_template,
        return NEShpDownloader(target_path_template=ne_path_template,
                               pre_downloaded_path_template=pre_path_template)

    
# add a generic Natural Earth shapefile downloader to the cartopy config 

# add a generic Natural Earth shapefile downloader to the cartopy config
# dictionary's 'downloaders' section.
_ne_key = ('shapefiles', 'natural_earth')
cartopy.config['downloaders'].setdefault(_ne_key,
@@ -161,15 +152,7 @@ def natural_earth(resolution='110m', category='physical', name='coastline'):
    # get hold of the Downloader (typically a NEShpDownloader instance)
    # which we can then simply call its path method to get the appropriate
    # shapefile (it will download if necessary)
    ne_downloader = NEShpDownloader.default_downloader()
    format_dict = {'config': cartopy.config, 'category': category,
                    'name': name, 'resolution': resolution}

    try:
        ne_downloader = NEShpDownloader.default_downloader()
        return ne_downloader.path(format_dict)
    except:
        ne_downloader = NEShpDownloader.default_downloader(backup=True)

                   'name': name, 'resolution': resolution}
    return ne_downloader.path(format_dict)

                                         
diff --git a/nngt/geospatial/countries.py b/nngt/geospatial/countries.py
index 6d85ddb..13ff5f4 100644
--- a/nngt/geospatial/countries.py
+++ b/nngt/geospatial/countries.py
@@ -286,8 +286,7 @@ for _, v in world.iterrows():
            continue
        elif len(idx) > 1:
            pop = cities.iloc[idx].pop_max
            idx = idx[np.argmax(pop)]
            points.append(cities.iloc[idx].geometry)
            points.append(cities.iloc[pop.idxmax()].geometry)
            continue
        else:
            idx = np.where((cities.sov_a3 == v.SU_A3)*cities.adm0cap)[0]
@@ -297,8 +296,7 @@ for _, v in world.iterrows():
                continue
            elif len(idx) > 1:
                pop = cities.iloc[idx].pop_max
                idx = idx[np.argmax(pop)]
                points.append(cities.iloc[idx].geometry)
                points.append(cities.iloc[pop.idxmax()].geometry)
                continue

    points.append(v.geometry.representative_point())
diff --git a/nngt/plot/plt_networks.py b/nngt/plot/plt_networks.py
index 4adefed..9b8915e 100755
--- a/nngt/plot/plt_networks.py
+++ b/nngt/plot/plt_networks.py
@@ -204,6 +204,8 @@ def draw_network(network, nsize="total-degree", ncolor=None, nshape="o",
    else:
        fig = axis.get_figure()

    fig.patch.set_visible(False)

    # projections for geographic plots

    proj = kwargs.get("proj", None)
@@ -476,9 +478,9 @@ def draw_network(network, nsize="total-degree", ncolor=None, nshape="o",

                for i in ids:
                    scatter.append(axis.scatter(
                        pos[i, 0], pos[i, 1], color=c[i], ms=0.5*nsize[i],
                        marker=nshape[i], zorder=2, mec=nborder_color[i],
                        mew=nborder_width, alpha=nalpha))
                        pos[i, 0], pos[i, 1], color=c[i], s=0.5*nsize[i],
                        marker=nshape[i], zorder=2, edgecolors=nborder_color[i],
                        linewidths=nborder_width, alpha=nalpha))
        else:
            scatter.append(axis.scatter(
                pos[:, 0], pos[:, 1], color=c, s=0.5*np.array(nsize),
@@ -1642,8 +1644,8 @@ def _node_edge_shape_size(network, nshape, nsize, max_nsize, min_nsize, esize,
                    shapes[ids] = m

            markers = list(shapes)
        if len(nshape) == network.node_nb() and restrict_nodes is not None:
            nshape = nshape[list(restrict_nodes)]
        elif len(nshape) == network.node_nb() and restrict_nodes is not None:
            markers = nshape[list(restrict_nodes)]
        elif len(nshape) != n:
            raise ValueError("When passing an array of markers to "
                             "`nshape`, one entry per node in the "
diff --git a/nngt/plot/plt_properties.py b/nngt/plot/plt_properties.py
index 775658f..c294fcc 100755
--- a/nngt/plot/plt_properties.py
+++ b/nngt/plot/plt_properties.py
@@ -317,18 +317,18 @@ def betweenness_distribution(
    if axes is None:
        fig, ax1 = plt.subplots()
        fig.patch.set_visible(False)
        ax1.grid(False, axis='y')

        axes = [ax1.twinx()]
        axes = [ax1]

        if num_axes == 2:
            ax2 = ax1.twiny()
            axes.append(ax2)
            ax1.grid(False, axis='x')
            ax1.yaxis.tick_right()
            ax1.yaxis.set_label_position("right")
            ax2.grid(False)
            ax2.yaxis.set_visible(True)
            ax2.yaxis.set_ticks_position("right")
            ax2.yaxis.set_label_position("right")
        else:
            ax1.set_yticks([])
            ax2 = ax1
    else:
        ax1 = axes[0]

@@ -374,19 +374,21 @@ def betweenness_distribution(

        _set_scale(ax1, nbins, np.min(ncounts[ncounts>0]), ncounts.max(),
                   logx, logy)

    if btype in ("edge", "both"):
        ax2.bar(
            ebins[:-1], ecounts, np.diff(ebins), color=colors[-1],
            align='edge', **kwargs)

        ax2.set_xlim([ebins.min(), ebins.max()])
        ax1.set_ylim([0, 1.1*ecounts.max()])
        ax2.set_ylim([0, 1.1*ecounts.max()])
        ax2.set_xlabel("Edge betweenness")
        ax1.set_ylabel("Edge count")
        ax2.set_ylabel("Edge count")
        ax2.ticklabel_format(axis='x', style='sci', scilimits=(-3, 2))

        _set_scale(ax2, ebins, np.min(ecounts[ecounts>0]), ecounts.max(),
                   logx, logy)

    if btype == "both":
        ax2.legend(
            ["Edge betweenness"], bbox_to_anchor=[x, 0.88],
@@ -1007,41 +1009,42 @@ def _set_new_plot(fignum=None, num_new_plots=1, names=None, sharex=None):
    import matplotlib.pyplot as plt
    # get the figure and compute the new number of rows and cols
    fig = plt.figure(num=fignum)

    num_axes = len(fig.axes) + num_new_plots

    if names is not None:
        num_axes = len(fig.axes) + len(names)
        num_new_plots = len(names)

    num_cols = max(int(np.ceil(np.sqrt(num_axes))), 1)
    ratio = num_axes/float(num_cols)
    num_rows = int(ratio)

    if int(ratio) != int(np.ceil(ratio)):
        num_rows += 1

    # change the geometry
    gs = fig.add_gridspec(num_rows, num_cols)

    for i in range(num_axes - num_new_plots):
        y = i // num_cols
        x = i - num_cols*y
        fig.axes[i].set_subplotspec(gs[y, x])

    lst_new_axes = []
    n_old = num_axes-num_new_plots+1

    for i in range(num_new_plots):
        if fig.axes:
            lst_new_axes.append(
                fig.add_subplot(num_rows, num_cols, n_old+i, sharex=sharex))
        else:
            lst_new_axes.append(fig.add_subplot(num_rows, num_cols, n_old+i))

        if names is not None:
            lst_new_axes[-1].name = names[i]
    return fig, lst_new_axes


def _log_format(y, pos):
    '''
    Needed to move log values by one, so first increment, then decrement
    '''
    # rounding err for 4 so add 0.4 to avoid it
    #~ return '{}'.format(int(np.e*np.e**(y-1) + 0.4)) if y > -1 else 0
    return y
    return fig, lst_new_axes


def _set_scale(ax1, xbins, mincounts, maxcounts, logx, logy):
diff --git a/setup.py b/setup.py
index b4dcf50..73ffd17 100755
--- a/setup.py
+++ b/setup.py
@@ -159,7 +159,7 @@ setup_params = dict(
    ]},

    # Requirements
    install_requires = ['numpy>=1.17', 'scipy>=0.11'],
    install_requires = ['numpy>=1.17', 'scipy>=0.11', 'cython'],
    python_requires = '>=3.5, <4',
    extras_require = {
        'matplotlib': 'matplotlib',
@@ -167,9 +167,8 @@ setup_params = dict(
        'ig': ['python-igraph'],
        'geometry': ['matplotlib', 'shapely', 'dxfgrabber', 'svg.path'],
        'geospatial': ['matplotlib', 'geopandas', 'descartes', 'cartopy'],
        'full': ['cython', 'networkx>=2.4', 'shapely', 'dxfgrabber',
                 'svg.path', 'matplotlib', 'geopandas', 'descartes', 'cartopy',
                 'lxml']
        'full': ['networkx>=2.4', 'shapely', 'dxfgrabber', 'svg.path',
                 'matplotlib', 'geopandas', 'descartes', 'cartopy', 'lxml']
    },

    # Cython module
@@ -181,7 +180,7 @@ setup_params = dict(
    author_email = 'tanguy.fardet@tuebingen.mpg.de',
    license = 'GPL3',
    keywords = 'network graph structure simulation neuron NEST DeNSE topology '
               'growth',
               'growth igraph graph-tool networkx geospatial',
    long_description = long_descr,
    classifiers = [
        'Development Status :: 5 - Production/Stable',
diff --git a/testing/__init__.py b/testing/__init__.py
index f35ff9d..fd249d6 100644
--- a/testing/__init__.py
+++ b/testing/__init__.py
@@ -26,15 +26,12 @@ Testing module
==============

This module tests the various functionalities of NNGT to make sure that all
implementations remain compatible with the graph libraries and versions 2.7 and
3.x of python.
implementations remain compatible with the graph libraries and versions 3.X of
python.

note ::
    When adding new tests, filename should be of the form `test_xxx.py` and the
    code should contain:
    * a ``TestXXX`` class,
    * a ``suite = unittest.TestLoader().loadTestsFromTestCase(TestXXX)``
      declaration.
    code should contain a list of functions called `test_yyy`
"""

# std imports
diff --git a/testing/test_examples.py b/testing/test_examples.py
index b609299..fc497cb 100644
--- a/testing/test_examples.py
+++ b/testing/test_examples.py
@@ -12,7 +12,7 @@ Check that the examples work.

import os
from os import environ
from os.path import dirname, abspath, isfile, join
from os.path import dirname, abspath, isfile, isdir, join
import unittest

import pytest
@@ -20,15 +20,32 @@ from scipy.special import lambertw

import nngt


''' Set state, paths, and global variables '''

with_plot, with_nest = None, None


def setup_module():
    ''' setup any state specific to the execution of the current module.'''
    with_plot = nngt.get_config("with_plot")
    with_nest = nngt.get_config("with_nest")

    nngt.set_config("with_plot", False)
    nngt.set_config("with_nest", False)


def teardown_module():
    ''' teardown any state that was previously setup with setup_module. '''
    nngt.set_config("with_plot", with_plot)
    nngt.set_config("with_nest", with_nest)


# set example dir
current_dir = dirname(abspath(__file__))
idx_testing = current_dir.find('testing')
example_dir = current_dir[:idx_testing] + 'doc/examples/'

# remove plotting and NEST
nngt.set_config("with_plot", False)
nngt.set_config("with_nest", False)

# set globals
glob = {"lambertw": lambertw}

@@ -39,15 +56,22 @@ glob = {"lambertw": lambertw}

@pytest.mark.mpi_skip
class TestExamples(unittest.TestCase):
    

    '''
    Class testing saving and loading functions.
    '''
    
    example_files = [
        example_dir + f for f in os.listdir(example_dir)
        if isfile(join(example_dir, f))
    ]

    example_files = []

    for f in os.listdir(example_dir):
        joint = join(example_dir, f)
        if joint.endswith(".py"):
            example_files.append(joint)
        elif isdir(joint):
            for f in os.listdir(joint):
                newjoint = join(joint, f)
                if newjoint.endswith(".py"):
                    example_files.append(newjoint)

    @classmethod
    def tearDownClass(cls):
@@ -55,7 +79,7 @@ class TestExamples(unittest.TestCase):
            os.remove("sp_graph.el")
        except:
            pass
    

    @property
    def test_name(self):
        return "test_examples"
-- 
2.32.0
NNGT/patches/.build.yml: FAILED in 25m53s

[Doc+Plot: updated doc and gallery plots][0] from [~tfardet][1]

[0]: https://lists.sr.ht/~tfardet/nngt-developers/patches/25710
[1]: mailto:tanguyfardet@protonmail.com

✗ #608173 FAILED NNGT/patches/.build.yml https://builds.sr.ht/~tfardet/job/608173